diff --git a/.changelog/10036.txt b/.changelog/10036.txt new file mode 100644 index 000000000000..34e5878e86b8 --- /dev/null +++ b/.changelog/10036.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_bucket_object: Existing resource can now be imported +``` \ No newline at end of file diff --git a/.changelog/10213.txt b/.changelog/10213.txt new file mode 100644 index 000000000000..350bfe6d3a89 --- /dev/null +++ b/.changelog/10213.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_acmpca_certificate +``` + +```release-note:new-data-source +aws_acmpca_certificate +``` diff --git a/.changelog/10539.txt b/.changelog/10539.txt new file mode 100644 index 000000000000..0b2a3ff42e82 --- /dev/null +++ b/.changelog/10539.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudformation_stack: Avoid conflicts with `on_failure` and `disable_rollback` +``` diff --git a/.changelog/10807.txt b/.changelog/10807.txt new file mode 100644 index 000000000000..7ebf0abaebd1 --- /dev/null +++ b/.changelog/10807.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Add support for configuration with Launch Template +``` diff --git a/.changelog/10817.txt b/.changelog/10817.txt new file mode 100644 index 000000000000..d27b50514d25 --- /dev/null +++ b/.changelog/10817.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_ec2_host +``` + +```release-note:new-data-source +aws_ec2_host +``` \ No newline at end of file diff --git a/.changelog/10969.txt b/.changelog/10969.txt new file mode 100644 index 000000000000..128ccf87e54a --- /dev/null +++ b/.changelog/10969.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Retry when `OperationInProgress` errors are returned from the AWS API +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set: Retry when `OperationInProgress` errors are returned from the AWS API +``` \ No newline at end of file diff --git a/.changelog/11522.txt b/.changelog/11522.txt new file mode 100644 index 000000000000..1eaade7f178a --- /dev/null +++ b/.changelog/11522.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_bucket_object: Add `source_hash` argument to compliment `etag`'s encryption limitations +``` \ No newline at end of file diff --git a/.changelog/11795.txt b/.changelog/11795.txt new file mode 100644 index 000000000000..32fe87533ac7 --- /dev/null +++ b/.changelog/11795.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_bucket: Retry on `PutBucketEncryption` HTTP 409 errors due to eventual consistency +``` \ No newline at end of file diff --git a/.changelog/11936.txt b/.changelog/11936.txt new file mode 100644 index 000000000000..0657ce4442c2 --- /dev/null +++ b/.changelog/11936.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_backend_environment +``` \ No newline at end of file diff --git a/.changelog/11937.txt b/.changelog/11937.txt new file mode 100644 index 000000000000..9da502923d73 --- /dev/null +++ b/.changelog/11937.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_amplify_branch +``` + +```release-note:bug +resource/aws_amplify_app: Mark the `enable_performance_mode` argument in the `auto_branch_creation_config` configuration block as `ForceNew` +``` \ No newline at end of file diff --git a/.changelog/11938.txt b/.changelog/11938.txt new file mode 100644 index 000000000000..297305946891 --- /dev/null +++ b/.changelog/11938.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_domain_association +``` \ No newline at end of file diff --git a/.changelog/11939.txt b/.changelog/11939.txt new file mode 100644 index 000000000000..7555c387a8ce --- /dev/null +++ b/.changelog/11939.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_webhook +``` \ No newline at end of file diff --git a/.changelog/11967.txt b/.changelog/11967.txt new file mode 100644 index 000000000000..c76920ac6e02 --- /dev/null +++ b/.changelog/11967.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ssm_parameter: Add support for `Intelligent-Tiering` +``` diff --git a/.changelog/12130.txt b/.changelog/12130.txt new file mode 100644 index 000000000000..8a47d1df19fa --- /dev/null +++ b/.changelog/12130.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_codebuild_project: Add `file_system_locations` argument +``` \ No newline at end of file diff --git a/.changelog/12370.txt b/.changelog/12370.txt new file mode 100644 index 000000000000..9a39c9e4cb6f --- /dev/null +++ b/.changelog/12370.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_rds_cluster_role_association +``` + +```release-note:enhancement +aws_rds_cluster: Set `iam_roles` as Computed to prevent drift when the `aws_rds_cluster_role_association` resource is used +``` \ No newline at end of file diff --git a/.changelog/12423.txt b/.changelog/12423.txt new file mode 100644 index 000000000000..cba00fca4652 --- /dev/null +++ b/.changelog/12423.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudformation_stack_set: Add `auto_deployment` configuration block and `permissions_model` arguments (support service managed permissions) +``` diff --git a/.changelog/12436.txt b/.changelog/12436.txt new file mode 100644 index 000000000000..eacbafd10380 --- /dev/null +++ b/.changelog/12436.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_role: Retry `assume_role_policy` updates for IAM eventual consistency +``` \ No newline at end of file diff --git a/.changelog/12482.txt b/.changelog/12482.txt new file mode 100644 index 000000000000..f8a17f140806 --- /dev/null +++ b/.changelog/12482.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dx_gateway_association: Changes to `proposal_id` do not force resource recreation +``` diff --git a/.changelog/12548.txt b/.changelog/12548.txt new file mode 100644 index 000000000000..83b4cd6d85af --- /dev/null +++ b/.changelog/12548.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_db_instance: Ignore allocated_storage for replica at creation time +``` \ No newline at end of file diff --git a/.changelog/12642.txt b/.changelog/12642.txt new file mode 100644 index 000000000000..a5019860475c --- /dev/null +++ b/.changelog/12642.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Add validation for `role_arn`, `policy_arns`, and `policy` +``` diff --git a/.changelog/12684.txt b/.changelog/12684.txt new file mode 100644 index 000000000000..e7717e02e2d5 --- /dev/null +++ b/.changelog/12684.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_invite_accepter +``` diff --git a/.changelog/12758.txt b/.changelog/12758.txt new file mode 100644 index 000000000000..1c3ab81007d2 --- /dev/null +++ b/.changelog/12758.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_mq_broker: Support updating broker engine version without recreating broker +``` diff --git a/.changelog/12787.txt b/.changelog/12787.txt new file mode 100644 index 000000000000..6d7c326f2c56 --- /dev/null +++ b/.changelog/12787.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +resource/aws_spot_instance_request: Add import support +``` +```release-note:enhancement +resource/aws_spot_instance_request: Add plan time validation for `spot_type` and `block_duration_minutes` +``` \ No newline at end of file diff --git a/.changelog/12817.txt b/.changelog/12817.txt new file mode 100644 index 000000000000..d092849d74b4 --- /dev/null +++ b/.changelog/12817.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_metric_alarm: Add plan time validation to `alarm_name`, `comparison_operator`, `metric_name`, `metric_query.id`, `metric_query.expression`, `metric_query.metric.metric_name`, `metric_query.metric.namespace`, `metric_query.metric.unit`, `namespace`, `period`, `statistic`, `alarm_description`, `insufficient_data_actions`, `ok_actions`, `unit`, and `extended_statistic` +``` diff --git a/.changelog/13127.txt b/.changelog/13127.txt new file mode 100644 index 000000000000..519a42cb1793 --- /dev/null +++ b/.changelog/13127.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_spot_fleet_request: Add `on_demand_allocation_strategy`, `on_demand_max_total_price`, and `on_demand_target_capacity` arguments +``` \ No newline at end of file diff --git a/.changelog/13371.txt b/.changelog/13371.txt new file mode 100644 index 000000000000..2725cd73be03 --- /dev/null +++ b/.changelog/13371.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `protocols` argument +``` + +```release-note:enhancement +data-source/aws_transfer_server: Add `certificate`, `endpoint_type`, `protocols` and `security_policy_name` attributes +``` \ No newline at end of file diff --git a/.changelog/13453.txt b/.changelog/13453.txt new file mode 100644 index 000000000000..919323a90fb1 --- /dev/null +++ b/.changelog/13453.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_route53_record: Support `set_identifier` values containing `_` +``` \ No newline at end of file diff --git a/.changelog/13474.txt b/.changelog/13474.txt new file mode 100644 index 000000000000..04eccad08e92 --- /dev/null +++ b/.changelog/13474.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_redshift_scheduled_action +``` \ No newline at end of file diff --git a/.changelog/13476.txt b/.changelog/13476.txt new file mode 100644 index 000000000000..16af42ba2bf3 --- /dev/null +++ b/.changelog/13476.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_dms_replication_task: Handle read-only attributes in `replication_task_settings` to avoid unnecessary diffs. +``` \ No newline at end of file diff --git a/.changelog/13554.txt b/.changelog/13554.txt new file mode 100644 index 000000000000..9fc170cd6eff --- /dev/null +++ b/.changelog/13554.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_subnet: Add `available_ip_address_count` attributes +``` \ No newline at end of file diff --git a/.changelog/13564.txt b/.changelog/13564.txt new file mode 100644 index 000000000000..d60a6321b496 --- /dev/null +++ b/.changelog/13564.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_eks_node_groups +``` + +```release-note:new-data-source +aws_eks_node_group +``` \ No newline at end of file diff --git a/.changelog/13783.txt b/.changelog/13783.txt new file mode 100644 index 000000000000..7815f4b62f8b --- /dev/null +++ b/.changelog/13783.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_dynamodb_tag +``` + +```release-note:new-resource +aws_ecs_tag +``` diff --git a/.changelog/13883.txt b/.changelog/13883.txt new file mode 100644 index 000000000000..d18bc2319da5 --- /dev/null +++ b/.changelog/13883.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_apigatewayv2_api +``` + +```release-note:new-data-source +aws_apigatewayv2_apis +``` diff --git a/.changelog/13938.txt b/.changelog/13938.txt new file mode 100644 index 000000000000..410820390f1a --- /dev/null +++ b/.changelog/13938.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_node_group: Add `node_group_name_prefix` argument +``` \ No newline at end of file diff --git a/.changelog/13944.txt b/.changelog/13944.txt new file mode 100644 index 000000000000..24bb93326e69 --- /dev/null +++ b/.changelog/13944.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_security_groups: Adds `arns` attribute +``` \ No newline at end of file diff --git a/.changelog/13971.txt b/.changelog/13971.txt new file mode 100644 index 000000000000..c954ca98b1a4 --- /dev/null +++ b/.changelog/13971.txt @@ -0,0 +1,14 @@ +```release-note:enhancement +resource/aws_ami: Add `usage_operation`, `platform_details`, `image_owner_alias`, `image_type`, `hypervisor`, `owner_id`, `platform`, `public` attributes +``` +```release-note:enhancement +resource/aws_ami_copy: Add `usage_operation`, `platform_details`, `image_owner_alias`, `image_type`, `hypervisor`, `owner_id`, `platform`, `public` attributes +``` + +```release-note:enhancement +resource/aws_ami_from_instance: Add `usage_operation`, `platform_details`, `image_owner_alias`, `image_type`, `hypervisor`, `owner_id`, `platform`, `public` attributes +``` + +```release-note:enhancement +data-source/aws_ami: Add `usage_operation`, `platform_details`, `ena_support` attributes +``` diff --git a/.changelog/14101.txt b/.changelog/14101.txt new file mode 100644 index 000000000000..17b056ebb9c3 --- /dev/null +++ b/.changelog/14101.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_sns_topic_subscription: Add plan time validation for `subscription_role_arn` and `topic_arn` +``` + +```release-note:bug +resource/aws_sns_topic_subscription: recreate subscription if topic is deleted +``` \ No newline at end of file diff --git a/.changelog/14123.txt b/.changelog/14123.txt new file mode 100644 index 000000000000..e3f256d6b84f --- /dev/null +++ b/.changelog/14123.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_sns_topic_policy: Add plan time validation to `arn` +``` + +```release-note:enhancement +resource/aws_sns_topic_policy: Add `owner` attribute +``` diff --git a/.changelog/14193.txt b/.changelog/14193.txt new file mode 100644 index 000000000000..9d588853c0d2 --- /dev/null +++ b/.changelog/14193.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecr_repository_policy: Add plan time validation for `policy` +``` diff --git a/.changelog/14247.txt b/.changelog/14247.txt new file mode 100644 index 000000000000..93eb2aa38328 --- /dev/null +++ b/.changelog/14247.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_config_configuration_aggregator: Allow name to have uppercase characters +``` diff --git a/.changelog/14255.txt b/.changelog/14255.txt new file mode 100644 index 000000000000..e4ff1a10ffd9 --- /dev/null +++ b/.changelog/14255.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_sns_topic_subscription: Fix to avoid `delivery_policy` always showing diff. +``` \ No newline at end of file diff --git a/.changelog/14319.txt b/.changelog/14319.txt new file mode 100644 index 000000000000..71d707d9f4df --- /dev/null +++ b/.changelog/14319.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_wafv2_web_acl_logging_configuration: Ensure `redacted_fields` are applied to the resource +``` + +```release-note:note +resource/aws_wafv2_web_acl_logging_configuration: The `redacted_fields` configuration block `all_query_arguments`, `body`, and `single_query_argument` arguments have been deprecated to match the WAF API documentation +``` \ No newline at end of file diff --git a/.changelog/14452.txt b/.changelog/14452.txt new file mode 100644 index 000000000000..dddbc40e49e8 --- /dev/null +++ b/.changelog/14452.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_datasync_task: Add plan time validation to `cloudwatch_log_group_arn`, `destination_location_arn` and `source_location_arn` +``` + +```release-note:enhancement +resource/aws_datasync_task: Add `schedule` argument +``` diff --git a/.changelog/14462.txt b/.changelog/14462.txt new file mode 100644 index 000000000000..68b0b2066f32 --- /dev/null +++ b/.changelog/14462.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_lb_listener: Add `alpn_policy` argument +``` + +```release-note:enhancement +data-source/aws_lb_listener: Add `alpn_policy` argument +``` \ No newline at end of file diff --git a/.changelog/14534.txt b/.changelog/14534.txt new file mode 100644 index 000000000000..f34b21a52cd0 --- /dev/null +++ b/.changelog/14534.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_codebuild_project: Add `build_batch_config` argument +``` diff --git a/.changelog/14578.txt b/.changelog/14578.txt new file mode 100644 index 000000000000..3768d6fdb929 --- /dev/null +++ b/.changelog/14578.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_lambda_function: Handle eventual consistency issues after publishing a version +``` \ No newline at end of file diff --git a/.changelog/14627.txt b/.changelog/14627.txt new file mode 100644 index 000000000000..eaa71e051b60 --- /dev/null +++ b/.changelog/14627.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_msk_cluster: Don't recreate cluster if order of `broker_node_group_info.client_subnets` or `broker_node_group_info.security_groups` entries change +``` \ No newline at end of file diff --git a/.changelog/14671.txt b/.changelog/14671.txt new file mode 100644 index 000000000000..a67cf0195dd6 --- /dev/null +++ b/.changelog/14671.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_iot_authorizer +``` \ No newline at end of file diff --git a/.changelog/14710.txt b/.changelog/14710.txt new file mode 100644 index 000000000000..014a2e845689 --- /dev/null +++ b/.changelog/14710.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_appsync_resolver: Mark `request_template` and `response_template` as optional (support Lambda) +``` diff --git a/.changelog/14714.txt b/.changelog/14714.txt new file mode 100644 index 000000000000..bf5967390590 --- /dev/null +++ b/.changelog/14714.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_standards_control +``` \ No newline at end of file diff --git a/.changelog/14905.txt b/.changelog/14905.txt new file mode 100644 index 000000000000..55bc3a980016 --- /dev/null +++ b/.changelog/14905.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_light_instance_public_ports: Add `cidrs` argument to `port_info` +``` \ No newline at end of file diff --git a/.changelog/14923.txt b/.changelog/14923.txt new file mode 100644 index 000000000000..ce57b47ce2bb --- /dev/null +++ b/.changelog/14923.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sns_topic_subscription: Add `email`, `email-json`, and `firehose` to protocol values. Add `subscription_role_arn` argument for Firehose support. Add `confirmation_was_authenticated`, `owner_id`, and `pending_confirmation` attributes. +``` \ No newline at end of file diff --git a/.changelog/14935.txt b/.changelog/14935.txt new file mode 100644 index 000000000000..e44d7120eea7 --- /dev/null +++ b/.changelog/14935.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_cognito_user_pool_client: Add plan time validation for `name`, `default_redirect_uri`, `supported_identity_providers` +``` + +```release-note:enhancement +resource/aws_cognito_user_pool_client: Add support for `access_token_validity` and `id_token_validity`, `token_validity_units` +``` + +```release-note:enhancement +resource/aws_cognito_user_pool: Add support for `configuration_set` in `email_configuration` +``` diff --git a/.changelog/15112.txt b/.changelog/15112.txt new file mode 100644 index 000000000000..3a79645ad697 --- /dev/null +++ b/.changelog/15112.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_listener: Fix `default_action.forward.target_group` to have minimum of 1. +``` \ No newline at end of file diff --git a/.changelog/15167.txt b/.changelog/15167.txt new file mode 100644 index 000000000000..1c81140929a8 --- /dev/null +++ b/.changelog/15167.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_cloudfront_log_delivery_canonical_user_id +``` \ No newline at end of file diff --git a/.changelog/15213.txt b/.changelog/15213.txt new file mode 100644 index 000000000000..5dae4dbff30d --- /dev/null +++ b/.changelog/15213.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_ssm_patch_group: Allow for a single patch group to be registered with multiple patch baselines +``` + +```release-note:bug +resource/aws_ssm_patch_group: Replace `Provider produced inconsistent result after apply` with actual error message +``` diff --git a/.changelog/15241.txt b/.changelog/15241.txt new file mode 100644 index 000000000000..906ce6da03fd --- /dev/null +++ b/.changelog/15241.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_guardduty_organization_configuration: Add `datasources` argument +``` \ No newline at end of file diff --git a/.changelog/15375.txt b/.changelog/15375.txt new file mode 100644 index 000000000000..7c838e27af23 --- /dev/null +++ b/.changelog/15375.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `security_policy_name` argument +``` \ No newline at end of file diff --git a/.changelog/15434.txt b/.changelog/15434.txt new file mode 100644 index 000000000000..dcfc3e6537c3 --- /dev/null +++ b/.changelog/15434.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_sfn_state_machine: Add `tracing_configuration` attribute +``` + +```release-note:bug +resource/aws_sfn_state_machine: Handle eventual consistency of state machine updates +``` \ No newline at end of file diff --git a/.changelog/15442.txt b/.changelog/15442.txt new file mode 100644 index 000000000000..81e63318ac51 --- /dev/null +++ b/.changelog/15442.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_codebuild_project: Add `build_status_config` attribute to `source` and `secondary_sources` configuration blocks +``` \ No newline at end of file diff --git a/.changelog/15461.txt b/.changelog/15461.txt new file mode 100644 index 000000000000..ad1c65d7841d --- /dev/null +++ b/.changelog/15461.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_s3_object_copy +``` \ No newline at end of file diff --git a/.changelog/15463.txt b/.changelog/15463.txt new file mode 100644 index 000000000000..31f66b4670ac --- /dev/null +++ b/.changelog/15463.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_timestreamwrite_database +``` \ No newline at end of file diff --git a/.changelog/15661.txt b/.changelog/15661.txt new file mode 100644 index 000000000000..a59549acef00 --- /dev/null +++ b/.changelog/15661.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_msk_cluster: Add `zookeeper_connect_string_tls` attribute +``` \ No newline at end of file diff --git a/.changelog/15785.txt b/.changelog/15785.txt new file mode 100644 index 000000000000..7d3e072f3dc8 --- /dev/null +++ b/.changelog/15785.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_launch_template: Add `placement` `host_resource_group_arn` attribute +``` + +```release-note:enhancement +resource/aws_launch_template: Add `placement` `host_resource_group_arn` argument +``` diff --git a/.changelog/15828.txt b/.changelog/15828.txt new file mode 100644 index 000000000000..abd56c6dd98f --- /dev/null +++ b/.changelog/15828.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sns_topic: Add `fifo_topic` and `content_based_deduplication` attributes +``` \ No newline at end of file diff --git a/.changelog/15885.txt b/.changelog/15885.txt new file mode 100644 index 000000000000..021aa6bc29e0 --- /dev/null +++ b/.changelog/15885.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_elasticache_global_replication_group +``` diff --git a/.changelog/15966.txt b/.changelog/15966.txt new file mode 100644 index 000000000000..feafab972218 --- /dev/null +++ b/.changelog/15966.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_app +``` \ No newline at end of file diff --git a/.changelog/16010.txt b/.changelog/16010.txt new file mode 100644 index 000000000000..9c68c0b91411 --- /dev/null +++ b/.changelog/16010.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_rds_cluster_parameter_group: Handle paginated response when reading parameters from RDS cluster parameter group. +``` diff --git a/.changelog/16049.txt b/.changelog/16049.txt new file mode 100644 index 000000000000..efd40e9764c2 --- /dev/null +++ b/.changelog/16049.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudfront_distribution: Add `connection_attempts`, `connection_timeout`, and `origin_shield`. +``` diff --git a/.changelog/16108.txt b/.changelog/16108.txt new file mode 100644 index 000000000000..8585c63642c7 --- /dev/null +++ b/.changelog/16108.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_mq_broker: Add RabbitMQ as option for `engine_type`, and new arguments `authentication_strategy`, `ldap_server_metadata`, and `storage_type`. Improve handling of eventual consistency. +``` \ No newline at end of file diff --git a/.changelog/16113.txt b/.changelog/16113.txt new file mode 100644 index 000000000000..277aebe0168c --- /dev/null +++ b/.changelog/16113.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_lambda_event_source_mapping: Support -1 (forever) as a valid value for `maximum_retry_attempts` +``` + +```release-note:bug +resource/aws_lambda_event_source_mapping: Support -1 (forever) as a valid value for `maximum_record_age_in_seconds` +``` \ No newline at end of file diff --git a/.changelog/16120.txt b/.changelog/16120.txt new file mode 100644 index 000000000000..17782b4dc4be --- /dev/null +++ b/.changelog/16120.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_batch_job_definition: Treat empty `container_properties.logConfiguration.secretOptions` array as `null` to prevent continual diffs +``` \ No newline at end of file diff --git a/.changelog/16192.txt b/.changelog/16192.txt new file mode 100644 index 000000000000..b8a0b3c4424b --- /dev/null +++ b/.changelog/16192.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_elasticsearch_domain: Add `domain_endpoint_options` configuration block `custom_endpoint`, `custom_endpoint_certificate_arn`, and `custom_endpoint_enabled` arguments +``` diff --git a/.changelog/16204.txt b/.changelog/16204.txt new file mode 100644 index 000000000000..dbb30234d482 --- /dev/null +++ b/.changelog/16204.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_task: Add `excludes` argument and `overwrite_mode`, `task_queueing`, and `transfer_mode` to the `options` configuration block +``` \ No newline at end of file diff --git a/.changelog/16207.txt b/.changelog/16207.txt new file mode 100644 index 000000000000..3b56c4c99ad5 --- /dev/null +++ b/.changelog/16207.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_option: Add `private_link_endpoint`, `security_group_arns`, `subnet_arns` and `vpc_endpoint_id` arguments +``` \ No newline at end of file diff --git a/.changelog/16325.txt b/.changelog/16325.txt new file mode 100644 index 000000000000..0f375a56b0b4 --- /dev/null +++ b/.changelog/16325.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_group: Added support Auto Scaling groups with multiple launch templates using a mixed instances policy +``` \ No newline at end of file diff --git a/.changelog/16373.txt b/.changelog/16373.txt new file mode 100644 index 000000000000..ccdf57978641 --- /dev/null +++ b/.changelog/16373.txt @@ -0,0 +1,3 @@ +```release-notes:new-resource +aws_ebs_snapshot_import +``` diff --git a/.changelog/16471.txt b/.changelog/16471.txt new file mode 100644 index 000000000000..9ff7f7d1626a --- /dev/null +++ b/.changelog/16471.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iot_topic_rule: Correctly update resource on `error_action` change +``` \ No newline at end of file diff --git a/.changelog/16502.txt b/.changelog/16502.txt new file mode 100644 index 000000000000..ec92efd940cb --- /dev/null +++ b/.changelog/16502.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_cognito_user_pool: Add `custom_domain`, `domain`, and `estimated_number_of_users` attributes +``` + +```release-note:enhancement +resource/aws_cognito_user_pool: Add plan time validation for `name` +``` + +```release-note:enhancement +resource/aws_cognito_user_pool: Add `custom_email_sender`, `custom_sms_sender`, and `kms_key_id` to `lambda_config` +``` \ No newline at end of file diff --git a/.changelog/16504.txt b/.changelog/16504.txt new file mode 100644 index 000000000000..8ad6f68b5775 --- /dev/null +++ b/.changelog/16504.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_elasticache_user_group +``` diff --git a/.changelog/16581.txt b/.changelog/16581.txt new file mode 100644 index 000000000000..e4e79453e2f9 --- /dev/null +++ b/.changelog/16581.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +data-source/aws_s3_bucket_object: Add `bucket_key_enabled` attribute (Support S3 Bucket Keys) +``` + +```release-note:enhancement +resource/aws_s3_bucket: Add `bucket_key_enabled` argument to `server_side_encryption_configuration` `rule` configuration block (Support S3 Bucket Keys) +``` + +```release-note:enhancement +resource/aws_s3_bucket_object: Add `bucket_key_enabled` attribute (Support S3 Bucket Keys) +``` diff --git a/.changelog/16616.txt b/.changelog/16616.txt new file mode 100644 index 000000000000..1915e1da9ea8 --- /dev/null +++ b/.changelog/16616.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_mwaa_environment +``` diff --git a/.changelog/16629.txt b/.changelog/16629.txt new file mode 100644 index 000000000000..720c9492c061 --- /dev/null +++ b/.changelog/16629.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_elasticache_user +``` + +```release-note:new-data-source +aws_elasticache_user +``` diff --git a/.changelog/16704.txt b/.changelog/16704.txt new file mode 100644 index 000000000000..5681b0180b54 --- /dev/null +++ b/.changelog/16704.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_ec2_instance_type_offerings: Add `locations` and `location_types` attributes +``` \ No newline at end of file diff --git a/.changelog/16709.txt b/.changelog/16709.txt new file mode 100644 index 000000000000..a0bc051c44c5 --- /dev/null +++ b/.changelog/16709.txt @@ -0,0 +1,8 @@ +```release-note:new-resource +aws_connect_instance +``` + +```release-note:new-data-source +aws_connect_instance +``` + diff --git a/.changelog/16734.txt b/.changelog/16734.txt new file mode 100644 index 000000000000..ee9a075fca9b --- /dev/null +++ b/.changelog/16734.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cognito_user_pool_client: Add support for `application_arn` in the `analytics_configuration` block. +``` diff --git a/.changelog/16743.txt b/.changelog/16743.txt new file mode 100644 index 000000000000..fb8b24184d00 --- /dev/null +++ b/.changelog/16743.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_dynamodb_kinesis_streaming_destination +``` diff --git a/.changelog/16815.txt b/.changelog/16815.txt new file mode 100644 index 000000000000..a7b99a6469ed --- /dev/null +++ b/.changelog/16815.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_cloudwatch_event_permission: Fix error in Event Bridge/CloudWatch Events bus name validation +``` + +```release-note:bug +resource/aws_cloudwatch_event_rule: Fix error in Event Bridge/CloudWatch Events bus name validation +``` + +```release-note:bug +resource/aws_cloudwatch_event_target: Fix error in Event Bridge/CloudWatch Events bus name validation +``` diff --git a/.changelog/16819.txt b/.changelog/16819.txt new file mode 100644 index 000000000000..6b544eb76b00 --- /dev/null +++ b/.changelog/16819.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_batch_compute_environment: Additional supported value `FARGATE` and `FARGATE_SPOT` for the `type` argument in the `compute_resources` configuration block +``` + +```release-note:enhancement +resource/aws_batch_compute_environment: The `instance_role`, `instance_type` and `min_vcpus` arguments in the `compute_resources` configuration block are now optional +``` + +```release-note:enhancement +resource/aws_batch_compute_environment: The `security_group_ids` and `subnets` arguments in the `compute_resources` configuration block can now be updated in-place for Fargate compute resources +``` \ No newline at end of file diff --git a/.changelog/16831.txt b/.changelog/16831.txt new file mode 100644 index 000000000000..19444cc3933e --- /dev/null +++ b/.changelog/16831.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ecr_registry_policy +``` diff --git a/.changelog/16850.txt b/.changelog/16850.txt new file mode 100644 index 000000000000..1976b664a307 --- /dev/null +++ b/.changelog/16850.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_batch_job_definition: Add `platform_capabilities` attribute +``` diff --git a/.changelog/16853.txt b/.changelog/16853.txt new file mode 100644 index 000000000000..34a91d2647a4 --- /dev/null +++ b/.changelog/16853.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ecr_replication_configuration +``` diff --git a/.changelog/16854.txt b/.changelog/16854.txt new file mode 100644 index 000000000000..ff1757e8fd71 --- /dev/null +++ b/.changelog/16854.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_connect_contact_flow +``` + +```release-note:new-data-source +aws_connect_contact_flow +``` diff --git a/.changelog/16865.txt b/.changelog/16865.txt new file mode 100644 index 000000000000..e46096675626 --- /dev/null +++ b/.changelog/16865.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ecrpublic_repository +``` \ No newline at end of file diff --git a/.changelog/16874.txt b/.changelog/16874.txt new file mode 100644 index 000000000000..01da77a8c907 --- /dev/null +++ b/.changelog/16874.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudwatch_event_bus_policy +``` \ No newline at end of file diff --git a/.changelog/16908.txt b/.changelog/16908.txt new file mode 100644 index 000000000000..5e161a9ee9e4 --- /dev/null +++ b/.changelog/16908.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Add `capacity_reservation_specification` argument +``` diff --git a/.changelog/16916.txt b/.changelog/16916.txt new file mode 100644 index 000000000000..2a56750ba77d --- /dev/null +++ b/.changelog/16916.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_lakeformation_permissions: Allow `principal` to be an AWS account ID or an IAM group, ou, or organization +``` diff --git a/.changelog/16918.txt b/.changelog/16918.txt new file mode 100644 index 000000000000..aad8a4a449c5 --- /dev/null +++ b/.changelog/16918.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_codestarconnections_host +``` diff --git a/.changelog/16930.txt b/.changelog/16930.txt new file mode 100644 index 000000000000..7ef478508e84 --- /dev/null +++ b/.changelog/16930.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_route: Validate route destination and target attributes +``` + +```release-note:bug +resource/aws_route: Correctly handle updates to the route target attributes (`egress_only_gateway_id`, `gateway_id`, `instance_id`, `local_gateway_id`, `nat_gateway_id`, `network_interface_id`, `transit_gateway_id`, `vpc_peering_connection_id`) +``` diff --git a/.changelog/16936.txt b/.changelog/16936.txt new file mode 100644 index 000000000000..c3d439350fa7 --- /dev/null +++ b/.changelog/16936.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_ecs_service: Add `deployment_circuit_breaker` +``` + +```release-note:bug +resource/aws_ecs_service: Improve handling of eventual consistency including security group dependency violations on deletion +``` \ No newline at end of file diff --git a/.changelog/16941.txt b/.changelog/16941.txt new file mode 100644 index 000000000000..d2a9da538547 --- /dev/null +++ b/.changelog/16941.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_capacity_provider: Add `managed_scaling` block `instance_warmup_period` argument +``` diff --git a/.changelog/16942.txt b/.changelog/16942.txt new file mode 100644 index 000000000000..c1aba454d3e2 --- /dev/null +++ b/.changelog/16942.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_capacity_provider: Allow updates to the `auto_scaling_group_provider` argument without recreating the resource +``` \ No newline at end of file diff --git a/.changelog/16961.txt b/.changelog/16961.txt new file mode 100644 index 000000000000..79b018a06168 --- /dev/null +++ b/.changelog/16961.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_route: Add `carrier_gateway_id` attribute +``` diff --git a/.changelog/16963.txt b/.changelog/16963.txt new file mode 100644 index 000000000000..b29a53dc5200 --- /dev/null +++ b/.changelog/16963.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_route: Add `carrier_gateway_id` attribute +``` diff --git a/.changelog/16972.txt b/.changelog/16972.txt new file mode 100644 index 000000000000..ed50922b9f05 --- /dev/null +++ b/.changelog/16972.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_eks_addon +``` + +```release-note:new-resource +aws_eks_addon +``` diff --git a/.changelog/16979.txt b/.changelog/16979.txt new file mode 100644 index 000000000000..ef3f5337416d --- /dev/null +++ b/.changelog/16979.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_default_route_table: Add `arn` attribute +``` + +```release-note:enhancement +resource/aws_route_table: Add `arn` attribute +``` + +```release-note:enhancement +resource/aws_route_table: Add `carrier_gateway_id` attribute to `route` configuration block +``` diff --git a/.changelog/17001.txt b/.changelog/17001.txt new file mode 100644 index 000000000000..6c752c43368c --- /dev/null +++ b/.changelog/17001.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_route_table: Add `arn` attribute +``` + +```release-note:enhancement +data-source/aws_route_table: Add `carrier_gateway_id` attribute to `routes` list +``` diff --git a/.changelog/17032.txt b/.changelog/17032.txt new file mode 100644 index 000000000000..8f900381d4fa --- /dev/null +++ b/.changelog/17032.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_ram_resource_share_accepter: Improve handling of eventual consistency +``` + +```release-note:bug +resource/aws_ram_resource_share: Improve handling of eventual consistency +``` + +```release-note:bug +resource/aws_ram_principal_association: Improve handling of eventual consistency +``` \ No newline at end of file diff --git a/.changelog/17040.txt b/.changelog/17040.txt new file mode 100644 index 000000000000..67fe6b1cf755 --- /dev/null +++ b/.changelog/17040.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_route53_record: Support `DS` value for `type` argument +``` diff --git a/.changelog/17041.txt b/.changelog/17041.txt new file mode 100644 index 000000000000..84097156d63c --- /dev/null +++ b/.changelog/17041.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudfront_key_group +``` diff --git a/.changelog/17046.txt b/.changelog/17046.txt new file mode 100644 index 000000000000..6ddffe5743ae --- /dev/null +++ b/.changelog/17046.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_ec2_coip_pool: Add `arn` attribute +``` \ No newline at end of file diff --git a/.changelog/17149.txt b/.changelog/17149.txt new file mode 100644 index 000000000000..8d6f8cf48f14 --- /dev/null +++ b/.changelog/17149.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_kinesis_stream_consumer +``` + +```release-note:new-resource +aws_kinesis_stream_consumer +``` diff --git a/.changelog/17151.txt b/.changelog/17151.txt new file mode 100644 index 000000000000..5e62c2f34585 --- /dev/null +++ b/.changelog/17151.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_cloudwatch_log_groups +``` \ No newline at end of file diff --git a/.changelog/17156.txt b/.changelog/17156.txt new file mode 100644 index 000000000000..686ddbcf2562 --- /dev/null +++ b/.changelog/17156.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_instance: Adds plan-time validation for `username` and `name` when `snapshot_identifier` is set +``` diff --git a/.changelog/17163.txt b/.changelog/17163.txt new file mode 100644 index 000000000000..0ec9cae8f439 --- /dev/null +++ b/.changelog/17163.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dms_certificate: Add `tags` argument +``` diff --git a/.changelog/17164.txt b/.changelog/17164.txt new file mode 100644 index 000000000000..bdbbee9f227a --- /dev/null +++ b/.changelog/17164.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_sqs_queue: Append `.fifo` suffix for Terraform-assigned FIFO queue names +``` \ No newline at end of file diff --git a/.changelog/17204.txt b/.changelog/17204.txt new file mode 100644 index 000000000000..5efb53945768 --- /dev/null +++ b/.changelog/17204.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_cookie_stickiness_policy: Allow zero value for `cookie_expiration_period` +``` diff --git a/.changelog/17219.txt b/.changelog/17219.txt new file mode 100644 index 000000000000..2cd8bafab50e --- /dev/null +++ b/.changelog/17219.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_emr_cluster: Adds support for multiple subnets +``` diff --git a/.changelog/17236.txt b/.changelog/17236.txt new file mode 100644 index 000000000000..c00165e6e3d0 --- /dev/null +++ b/.changelog/17236.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_s3_bucket_object: Handle read-after-create eventual consistency +``` diff --git a/.changelog/17241.txt b/.changelog/17241.txt new file mode 100644 index 000000000000..cc726257b851 --- /dev/null +++ b/.changelog/17241.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_target: Adds `retry_policy` attributes +``` + +```release-note:enhancement +resource/aws_cloudwatch_event_target: Adds `dead_letter_config` attributes +``` \ No newline at end of file diff --git a/.changelog/17251.txt b/.changelog/17251.txt new file mode 100644 index 000000000000..ef2174223bc1 --- /dev/null +++ b/.changelog/17251.txt @@ -0,0 +1,15 @@ +```release-note:new-resource +aws_sagemaker_app +``` + +```release-note:enhancement +resource/aws_sagemaker_domain: Make `default_resource_spec` optional for the `tensor_board_app_settings`, `jupyter_server_app_settings` and `kernel_gateway_app_settings` config blocks. +``` + +```release-note:bug +resource/aws_sagemaker_domain: Wait for update to finish. +``` + +```release-note:bug +resource/aws_sagemaker_user_profile: Wait for update to finish. +``` \ No newline at end of file diff --git a/.changelog/17270.txt b/.changelog/17270.txt new file mode 100644 index 000000000000..295207a09343 --- /dev/null +++ b/.changelog/17270.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudwatch_event_archive +``` diff --git a/.changelog/17291.txt b/.changelog/17291.txt new file mode 100644 index 000000000000..f7a68aecd72e --- /dev/null +++ b/.changelog/17291.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_route: `destination_prefix_list_id` attribute can be specified for managed prefix list destinations +``` diff --git a/.changelog/17295.txt b/.changelog/17295.txt new file mode 100644 index 000000000000..eb45eadef04e --- /dev/null +++ b/.changelog/17295.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_route: Add `destination_prefix_list_id` attribute +``` diff --git a/.changelog/17298.txt b/.changelog/17298.txt new file mode 100644 index 000000000000..dc491d05c226 --- /dev/null +++ b/.changelog/17298.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_config_organization_conformance_pack +``` \ No newline at end of file diff --git a/.changelog/17319.txt b/.changelog/17319.txt new file mode 100644 index 000000000000..73a72331214c --- /dev/null +++ b/.changelog/17319.txt @@ -0,0 +1,19 @@ +```release-note:enhancement +resource/aws_default_route_table: Add `destination_prefix_list_id` attribute +``` + +```release-note:enhancement +resource/aws_route_table: Add `destination_prefix_list_id` attribute +``` + +```release-note:bug +resource/aws_vpn_gateway_route_propagation: Improve eventual consistency handling and handling of out-of-band resource removal +``` + +```release-note:bug +resource/aws_route_table: Improve eventual consistency handling and handling of out-of-band resource removal +``` + +```release-note:bug +resource/aws_route_table_association: Improve eventual consistency handling and handling of out-of-band resource removal +``` diff --git a/.changelog/17347.txt b/.changelog/17347.txt new file mode 100644 index 000000000000..341f4ddda9ae --- /dev/null +++ b/.changelog/17347.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_route_table: Add `destination_prefix_list_id` attribute +``` diff --git a/.changelog/17387.txt b/.changelog/17387.txt new file mode 100644 index 000000000000..ad02895c6e8b --- /dev/null +++ b/.changelog/17387.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ecs_service: Re-create service when `service_registries` changes +``` diff --git a/.changelog/17447.txt b/.changelog/17447.txt new file mode 100644 index 000000000000..56027c53c079 --- /dev/null +++ b/.changelog/17447.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_msk_cluster: Support in-place `instance_type` updates +``` diff --git a/.changelog/17465.txt b/.changelog/17465.txt new file mode 100644 index 000000000000..79e78c672802 --- /dev/null +++ b/.changelog/17465.txt @@ -0,0 +1,3 @@ +```release-note:note +resource/aws_codebuild_project: The `source` and `secondary_sources` configuration block `auth` attributes have been deprecated to match the CodeBuild API documentation. Use the `aws_codebuild_source_credential` resource instead. +``` diff --git a/.changelog/17474.txt b/.changelog/17474.txt new file mode 100644 index 000000000000..86b25700c846 --- /dev/null +++ b/.changelog/17474.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_route53_hosted_zone_dnssec +``` diff --git a/.changelog/17488.txt b/.changelog/17488.txt new file mode 100644 index 000000000000..4b7708fa2b2e --- /dev/null +++ b/.changelog/17488.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_trigger: Support starting ON_DEMAND triggers via `enabled` flag. +``` diff --git a/.changelog/17498.txt b/.changelog/17498.txt new file mode 100644 index 000000000000..6978a6c54a9c --- /dev/null +++ b/.changelog/17498.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_service_discovery_instance +``` \ No newline at end of file diff --git a/.changelog/17534.txt b/.changelog/17534.txt new file mode 100644 index 000000000000..c3e39ab718c1 --- /dev/null +++ b/.changelog/17534.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_target_group: Use gRPC matcher when using gRPC protocol +``` diff --git a/.changelog/17539.txt b/.changelog/17539.txt new file mode 100644 index 000000000000..4ea42cc41558 --- /dev/null +++ b/.changelog/17539.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `security_group_ids` argument to `endpoint_details` configuration block. +``` diff --git a/.changelog/17571.txt b/.changelog/17571.txt new file mode 100644 index 000000000000..af769263bcc1 --- /dev/null +++ b/.changelog/17571.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_msk_configuration: `kafka_versions` argument is optional +``` \ No newline at end of file diff --git a/.changelog/17573.txt b/.changelog/17573.txt new file mode 100644 index 000000000000..3532b0a8319a --- /dev/null +++ b/.changelog/17573.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpn_connection: Allow `local_ipv4_network_cidr`, `remote_ipv4_network_cidr`, `local_ipv6_network_cidr`, and `remote_ipv6_network_cidr` to be CIDRs of any size +``` diff --git a/.changelog/17579.txt b/.changelog/17579.txt new file mode 100644 index 000000000000..1256043ef765 --- /dev/null +++ b/.changelog/17579.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_msk_cluster: Orders `bootstrap_brokers`, `bootstrap_brokers_sasl_scram`, `bootstrap_brokers_tls`, and `zookeeper_connect_string` +``` + +```release-note:enhancement +data-source/aws_msk_cluster: Orders `bootstrap_brokers`, `bootstrap_brokers_sasl_scram`, `bootstrap_brokers_tls`, and `zookeeper_connect_string` +``` diff --git a/.changelog/17582.txt b/.changelog/17582.txt new file mode 100644 index 000000000000..0ccf958d66cc --- /dev/null +++ b/.changelog/17582.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_document: Recreate resource on `name` update +``` diff --git a/.changelog/17585.txt b/.changelog/17585.txt new file mode 100644 index 000000000000..7621dc3fc654 --- /dev/null +++ b/.changelog/17585.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_outposts_outpost: `owner_id` is now an optional argument +``` + +```release-note:enhancement +data-source/aws_outposts_outposts: Add `owner_id` argument +``` \ No newline at end of file diff --git a/.changelog/17589.txt b/.changelog/17589.txt new file mode 100644 index 000000000000..17a28b7e405b --- /dev/null +++ b/.changelog/17589.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_ec2_transit_gateway_route_tables +``` \ No newline at end of file diff --git a/.changelog/17591.txt b/.changelog/17591.txt new file mode 100644 index 000000000000..7f5d2c6793b5 --- /dev/null +++ b/.changelog/17591.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dms_endpoint: Add `s3_settings.data_format`, `s3_settings.parquet_timestamp_in_millisecond`, `s3_settings.parquet_version`, `s3_settings.encryption_mode` and `s3_settings.server_side_encryption_kms_key_id` arguments. +``` \ No newline at end of file diff --git a/.changelog/17595.txt b/.changelog/17595.txt new file mode 100644 index 000000000000..17a2e2fd45b8 --- /dev/null +++ b/.changelog/17595.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_fms_policy: Update `resource_type_list` plan-time validation to include `AWS::EC2::VPC`. +``` diff --git a/.changelog/17596.txt b/.changelog/17596.txt new file mode 100644 index 000000000000..7847b72d61d6 --- /dev/null +++ b/.changelog/17596.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fms_admin_account: Extend creation timeout to 10 minutes +``` diff --git a/.changelog/17608.txt b/.changelog/17608.txt new file mode 100644 index 000000000000..e795b860f287 --- /dev/null +++ b/.changelog/17608.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ses_configuration_set: Adds `reputation_metrics_enabled` and `sending_enabled` arguments and `last_fresh_start` attribute +``` \ No newline at end of file diff --git a/.changelog/17610.txt b/.changelog/17610.txt new file mode 100644 index 000000000000..7f3bcaf1cbf6 --- /dev/null +++ b/.changelog/17610.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lambda_function: Prevents perpetual diff in `vpc_config` +``` diff --git a/.changelog/17611.txt b/.changelog/17611.txt new file mode 100644 index 000000000000..2ca9a137dec6 --- /dev/null +++ b/.changelog/17611.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/ses_receipt_rule_set: Add `arn` attribute +``` + +```release-note:enhancement +resource/ses_receipt_rule_set: Add plan time validation to `name` +``` diff --git a/.changelog/17612.txt b/.changelog/17612.txt new file mode 100644 index 000000000000..20ff384d3446 --- /dev/null +++ b/.changelog/17612.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eip: Tags are set on create +``` diff --git a/.changelog/17621.txt b/.changelog/17621.txt new file mode 100644 index 000000000000..e13911c5853e --- /dev/null +++ b/.changelog/17621.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_transfer_ssh_key: Corrects user_name validation +``` + +```release-note:bug +resource/aws_transfer_user: Corrects user_name validation +``` diff --git a/.changelog/17627.txt b/.changelog/17627.txt new file mode 100644 index 000000000000..113a502191cb --- /dev/null +++ b/.changelog/17627.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ses_receipt_rule: Fix name validation regex to include `.` (period) +``` diff --git a/.changelog/17641.txt b/.changelog/17641.txt new file mode 100644 index 000000000000..9b6da489e960 --- /dev/null +++ b/.changelog/17641.txt @@ -0,0 +1,7 @@ +```release-note:note +data-source/aws_vpc_endpoint_service: The `service_type` argument filtering has been switched from client-side to new EC2 API functionality +``` + +```release-note:bug +data-source/aws_vpc_endpoint_service: Prevent panic with incorrect `service_type` argument values +``` diff --git a/.changelog/17645.txt b/.changelog/17645.txt new file mode 100644 index 000000000000..fef7ea91cd38 --- /dev/null +++ b/.changelog/17645.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_listener_certificate: Prevent resource ID parsing error with IAM Server Certificate names containing underscores +``` diff --git a/.changelog/17646.txt b/.changelog/17646.txt new file mode 100644 index 000000000000..ad545ad6a4c3 --- /dev/null +++ b/.changelog/17646.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ebs_volume: Only specify throughput on update for `gp3` volumes +``` diff --git a/.changelog/17654.txt b/.changelog/17654.txt new file mode 100644 index 000000000000..9d0d62ee8417 --- /dev/null +++ b/.changelog/17654.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ses_receipt_rule: Add `encoding` argument to `sns_action` configuration block. +``` diff --git a/.changelog/17655.txt b/.changelog/17655.txt new file mode 100644 index 000000000000..f8532bdd8a48 --- /dev/null +++ b/.changelog/17655.txt @@ -0,0 +1,3 @@ +```release-note:note +provider: The default development, testing and building of the Terraform AWS Provider is now done with Go 1.16. This version of Go adds support for `darwin/arm64` (Apple Silicon) but deprecates support for macOS 10.12 (Sierra). +``` diff --git a/.changelog/17657.txt b/.changelog/17657.txt new file mode 100644 index 000000000000..2d9c7449b266 --- /dev/null +++ b/.changelog/17657.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_ssm_document: Add plan time validation for `attachments_source`, `attachments_source.name`, `attachments_source.values`, `target_type`, `version_name` +``` + +```release-note:enhancement +resource/aws_ssm_document: Wait for document to be created/update/deleted +``` + +```release-note:enhancement +resource/aws_ssm_document: Support all valid values for `document_type` +``` diff --git a/.changelog/17689.txt b/.changelog/17689.txt new file mode 100644 index 000000000000..bf39380376e1 --- /dev/null +++ b/.changelog/17689.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_appautoscaling_scheduled_action: Adds `timezone` support +``` + +```release-note:enhancement +resource/aws_appautoscaling_scheduled_action: Allows any timezone to be specified for `start_time` and `end_time` +``` diff --git a/.changelog/17692.txt b/.changelog/17692.txt new file mode 100644 index 000000000000..1bb9618df4a9 --- /dev/null +++ b/.changelog/17692.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_backup_plan: `backup_options` and `resource_type` attributes in `advanced_backup_setting` configuration block are both required +``` diff --git a/.changelog/17702.txt b/.changelog/17702.txt new file mode 100644 index 000000000000..02df2eafb36e --- /dev/null +++ b/.changelog/17702.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_glue_catalog_table: Prevent `schema_reference` attribute error +``` + +```release-note:enhancement +resource/aws_glue_partition: Add plan time validation for `partition_values` +``` diff --git a/.changelog/17704.txt b/.changelog/17704.txt new file mode 100644 index 000000000000..e5ccf784f629 --- /dev/null +++ b/.changelog/17704.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_synthetics_canary: Fix Canary Update when in running state +``` diff --git a/.changelog/17725.txt b/.changelog/17725.txt new file mode 100644 index 000000000000..3256fcaa2bdd --- /dev/null +++ b/.changelog/17725.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_elasticache_replication_group: Allows creating a Replication Group as part of a Global Replication Group +``` diff --git a/.changelog/17726.txt b/.changelog/17726.txt new file mode 100644 index 000000000000..7844c7363bda --- /dev/null +++ b/.changelog/17726.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_msk_cluster: Configurable Create, Update and Delete timeouts +``` \ No newline at end of file diff --git a/.changelog/17731.txt b/.changelog/17731.txt new file mode 100644 index 000000000000..127d47c32132 --- /dev/null +++ b/.changelog/17731.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_lb_target_group: Add preserve_client_ip target attribute support +``` \ No newline at end of file diff --git a/.changelog/17734.txt b/.changelog/17734.txt new file mode 100644 index 000000000000..cc8a6a247d7a --- /dev/null +++ b/.changelog/17734.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_instance: Prevent error with `iam_instance_profile` containing additional forward slashes from path +``` diff --git a/.changelog/17735.txt b/.changelog/17735.txt new file mode 100644 index 000000000000..ed697b895426 --- /dev/null +++ b/.changelog/17735.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ami_copy: Add `destination_outpost_arn` argument +``` \ No newline at end of file diff --git a/.changelog/17739.txt b/.changelog/17739.txt new file mode 100644 index 000000000000..4dd30f54bce9 --- /dev/null +++ b/.changelog/17739.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_globalaccelerator_accelerator: Allow update of flow log attribute for active flow logs +``` + +```release-note:enhancement +resource/aws_globalaccelerator_accelerator: Add plan time validation to `name`, `flow_logs_s3_bucket` and `flow_logs_s3_prefix` attributes +``` diff --git a/.changelog/17752.txt b/.changelog/17752.txt new file mode 100644 index 000000000000..f7589b6efc4b --- /dev/null +++ b/.changelog/17752.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_iam_policy_document: Keep empty conditions +``` diff --git a/.changelog/17754.txt b/.changelog/17754.txt new file mode 100644 index 000000000000..e204d94078fe --- /dev/null +++ b/.changelog/17754.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_archive: Fix retention_days type conversion on creation +``` diff --git a/.changelog/17755.txt b/.changelog/17755.txt new file mode 100644 index 000000000000..4387e6b0301f --- /dev/null +++ b/.changelog/17755.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_db_instance: Fix conflicting argument validation error +``` diff --git a/.changelog/17764.txt b/.changelog/17764.txt new file mode 100644 index 000000000000..363a7c769036 --- /dev/null +++ b/.changelog/17764.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_gamelift_build: Support all valid operating system values +``` diff --git a/.changelog/17784.txt b/.changelog/17784.txt new file mode 100644 index 000000000000..b794982a4761 --- /dev/null +++ b/.changelog/17784.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_kinesis_analytics_application: Add `start_application` attribute +``` + +```release-note:enhancement +resource/aws_kinesis_analytics_application: `starting_position_configuration` can be specified when starting an application +``` diff --git a/.changelog/17801.txt b/.changelog/17801.txt new file mode 100644 index 000000000000..57697fb0bf32 --- /dev/null +++ b/.changelog/17801.txt @@ -0,0 +1,3 @@ +```release-note:bug +provider: Underlying Terraform Plugin SDK update to ensure data source errors include configuration source (file and line) +``` diff --git a/.changelog/17804.txt b/.changelog/17804.txt new file mode 100644 index 000000000000..8efd4f8c8154 --- /dev/null +++ b/.changelog/17804.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_resourcegroupstaggingapi_resources +``` diff --git a/.changelog/17830.txt b/.changelog/17830.txt new file mode 100644 index 000000000000..ca76c19e6fc3 --- /dev/null +++ b/.changelog/17830.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_ssm_parameter: Add plan time validation to `name`, `description` and `allowed_pattern` +``` + +```release-note:enhancement +resource/aws_ssm_parameter: Tag on create +``` diff --git a/.changelog/17836.txt b/.changelog/17836.txt new file mode 100644 index 000000000000..d46b46d1c80b --- /dev/null +++ b/.changelog/17836.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_kms_grant: Adds support for operations on asymmetric keys +``` diff --git a/.changelog/17850.txt b/.changelog/17850.txt new file mode 100644 index 000000000000..ccda8dc4aa2f --- /dev/null +++ b/.changelog/17850.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_acmpca_certificate_authority_certificate +``` diff --git a/.changelog/17852.txt b/.changelog/17852.txt new file mode 100644 index 000000000000..f8cac85ef8a6 --- /dev/null +++ b/.changelog/17852.txt @@ -0,0 +1,19 @@ +```release-note:new-data-source +aws_dx_connection +``` + +```release-note:enhancement +resource/aws_dx_connection: Add `owner_account_id` attribute +``` + +```release-note:enhancement +resource/aws_dx_lag: Add `owner_account_id` attribute +``` + +```release-note:enhancement +resource/aws_dx_connection: Add `provider_name` argument +``` + +```release-note:enhancement +resource/aws_dx_lag: Add `provider_name` argument +``` \ No newline at end of file diff --git a/.changelog/17859.txt b/.changelog/17859.txt new file mode 100644 index 000000000000..4f9ac5a96c96 --- /dev/null +++ b/.changelog/17859.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_instance: Add `ipv6_addresses` attribute +``` diff --git a/.changelog/17864.txt b/.changelog/17864.txt new file mode 100644 index 000000000000..1d6ab1312738 --- /dev/null +++ b/.changelog/17864.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_instance: Add `customer_owned_ip_enabled` argument +``` \ No newline at end of file diff --git a/.changelog/17869.txt b/.changelog/17869.txt new file mode 100644 index 000000000000..fa7d1a775d9e --- /dev/null +++ b/.changelog/17869.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_acm_certificate: Trigger resource recreation with `VALIDATION_TIMED_OUT` status +``` diff --git a/.changelog/17872.txt b/.changelog/17872.txt new file mode 100644 index 000000000000..ebfdaba2ad49 --- /dev/null +++ b/.changelog/17872.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_neptune_cluster_parameter_group: Correctly update resource by `id` +``` diff --git a/.changelog/17876.txt b/.changelog/17876.txt new file mode 100644 index 000000000000..5c7ab8bd6028 --- /dev/null +++ b/.changelog/17876.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_waf_rule: Fix rule deletion when still referenced by a WebACL +``` diff --git a/.changelog/17880.txt b/.changelog/17880.txt new file mode 100644 index 000000000000..76a882ce3ba6 --- /dev/null +++ b/.changelog/17880.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_storagegateway_upload_buffer: Replace `Provider produced inconsistent result after apply` with actual error message +``` diff --git a/.changelog/17885.txt b/.changelog/17885.txt new file mode 100644 index 000000000000..146450dc5d3a --- /dev/null +++ b/.changelog/17885.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_maintenance_window_task: Prevent `ValidationException` error on update when priority is not set or 0 +``` diff --git a/.changelog/17897.txt b/.changelog/17897.txt new file mode 100644 index 000000000000..820002cda7c4 --- /dev/null +++ b/.changelog/17897.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ec2_client_vpn_endpoint: Add `self_service_portal` and `authentication_options.self_service_saml_provider_arn` arguments to support self-service portal +``` \ No newline at end of file diff --git a/.changelog/17899.txt b/.changelog/17899.txt new file mode 100644 index 000000000000..2fc115633d9a --- /dev/null +++ b/.changelog/17899.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudwatch_query_definition +``` \ No newline at end of file diff --git a/.changelog/17901.txt b/.changelog/17901.txt new file mode 100644 index 000000000000..cbd551a561b1 --- /dev/null +++ b/.changelog/17901.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_neptune_cluster_instance: Add "storage-optimization" to Neptune cluster instance create/update pending states +``` diff --git a/.changelog/17909.txt b/.changelog/17909.txt new file mode 100644 index 000000000000..3eb85dae46c7 --- /dev/null +++ b/.changelog/17909.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_parameter_group: Store all values in lowercase to prevent unexpected diffs +``` diff --git a/.changelog/17933.txt b/.changelog/17933.txt new file mode 100644 index 000000000000..9409ddff9e3e --- /dev/null +++ b/.changelog/17933.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Fix update of `batch_size` for MSK event source mappings +``` + +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Don't incorrectly update unspecified `maximum_batching_window_in_seconds`, `maximum_record_age_in_seconds` and `maximum_retry_attempts` arguments from their default values +``` diff --git a/.changelog/17934.txt b/.changelog/17934.txt new file mode 100644 index 000000000000..5a9434afc1bb --- /dev/null +++ b/.changelog/17934.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Support automatic region validation for `ap-northeast-3` +``` diff --git a/.changelog/17958.txt b/.changelog/17958.txt new file mode 100644 index 000000000000..810bc032a80f --- /dev/null +++ b/.changelog/17958.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_dms_certificate: Correctly base64 decode `certificate_wallet` value +``` diff --git a/.changelog/17959.txt b/.changelog/17959.txt new file mode 100644 index 000000000000..9b0b4cd67b69 --- /dev/null +++ b/.changelog/17959.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_eks_identity_provider_config +``` \ No newline at end of file diff --git a/.changelog/17962.txt b/.changelog/17962.txt new file mode 100644 index 000000000000..b906cde0dbec --- /dev/null +++ b/.changelog/17962.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_instance_profile: Add tagging support +``` \ No newline at end of file diff --git a/.changelog/17964.txt b/.changelog/17964.txt new file mode 100644 index 000000000000..f762457c64a5 --- /dev/null +++ b/.changelog/17964.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_iam_openid_connect_provider: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_openid_connect_provider: Add plan time validation for `client_id_list` and `thumbprint_list` +``` diff --git a/.changelog/17965.txt b/.changelog/17965.txt new file mode 100644 index 000000000000..9451408b5c63 --- /dev/null +++ b/.changelog/17965.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_iam_saml_provider: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_saml_provider: Add plan time validation for `name` and `saml_metadata_document` +``` diff --git a/.changelog/17967.txt b/.changelog/17967.txt new file mode 100644 index 000000000000..ff8a26838766 --- /dev/null +++ b/.changelog/17967.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_iam_server_certificate: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_server_certificate: Add `expiration` and `upload_date` attributes +``` diff --git a/.changelog/17968.txt b/.changelog/17968.txt new file mode 100644 index 000000000000..daf69ea76e6c --- /dev/null +++ b/.changelog/17968.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ami_from_instance: Tag on create. +``` \ No newline at end of file diff --git a/.changelog/17969.txt b/.changelog/17969.txt new file mode 100644 index 000000000000..40a3386fda36 --- /dev/null +++ b/.changelog/17969.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_efs_file_system: Add `number_of_mount_targets`, `size_in_bytes` and `owner_id` attributes +``` \ No newline at end of file diff --git a/.changelog/17974.txt b/.changelog/17974.txt new file mode 100644 index 000000000000..c854905099eb --- /dev/null +++ b/.changelog/17974.txt @@ -0,0 +1,15 @@ +```release-note:note +provider: New `default_tags` argument as a public preview for applying tags across all resources under a provider. Support for the functionality must be added to individual resources in the codebase and is only implemented for the `aws_subnet` and `aws_vpc` resources at this time. Until a general availability announcement, no compatibility promises are made with these provider arguments and their functionality. +``` + +```release-note:enhancement +provider: Add `default_tags` argument (in public preview, see note above) +``` + +```release-note:enhancement +resource/aws_subnet: Support provider-wide default tags (in public preview, see note above) +``` + +```release-note:enhancement +resource/aws_vpc: Support provider-wide default tags (in public preview, see note above) +``` diff --git a/.changelog/17982.txt b/.changelog/17982.txt new file mode 100644 index 000000000000..1b2c8470faf1 --- /dev/null +++ b/.changelog/17982.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_rds_cluster_instance: Add `configuring-iam-database-auth` pending state +``` diff --git a/.changelog/17985.txt b/.changelog/17985.txt new file mode 100644 index 000000000000..44f559b3325b --- /dev/null +++ b/.changelog/17985.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_globalaccelerator_accelerator: Correct length for `name` attribute validation +``` diff --git a/.changelog/17992.txt b/.changelog/17992.txt new file mode 100644 index 000000000000..04eaf7a7a6e5 --- /dev/null +++ b/.changelog/17992.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_servicequotas_service_quota: Add plan time validation to `quota_code` and `service_code` +``` diff --git a/.changelog/18006.txt b/.changelog/18006.txt new file mode 100644 index 000000000000..544efe7c5488 --- /dev/null +++ b/.changelog/18006.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_efs_backup_policy +``` \ No newline at end of file diff --git a/.changelog/18013.txt b/.changelog/18013.txt new file mode 100644 index 000000000000..39ec769cf435 --- /dev/null +++ b/.changelog/18013.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_instance: Allow `snapshot_identifier` to be removed from configuration without resource recreation +``` diff --git a/.changelog/18042.txt b/.changelog/18042.txt new file mode 100644 index 000000000000..a62ba19d4710 --- /dev/null +++ b/.changelog/18042.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudfront_distribution: Allow `forwarded_values` to be set to empty when values were previously set +``` diff --git a/.changelog/18056.txt b/.changelog/18056.txt new file mode 100644 index 000000000000..1bf777799e54 --- /dev/null +++ b/.changelog/18056.txt @@ -0,0 +1,19 @@ +```release-note:enhancement +resource/aws_kinesisanalyticsv2_application: Add `start_application` attribute +``` + +```release-note:enhancement +resource/aws_kinesisanalyticsv2_application: `starting_position_configuration` can be specified when starting a SQL application +``` + +```release-note:enhancement +resource/aws_kinesisanalyticsv2_application: Add `run_configuration` attribute for starting a Flink application +``` + +```release-note:enhancement +resource/aws_kinesisanalyticsv2_application: Add `force_stop` attribute +``` + +```release-note:new-resource +aws_kinesisanalyticsv2_application_snapshot +``` diff --git a/.changelog/18070.txt b/.changelog/18070.txt new file mode 100644 index 000000000000..b98c495dd801 --- /dev/null +++ b/.changelog/18070.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_mq_configuration: Add `ldap` as an `authentication_strategy` and `RabbitMQ` as an `engine_type` +``` diff --git a/.changelog/18076.txt b/.changelog/18076.txt new file mode 100644 index 000000000000..630f6f7c7e60 --- /dev/null +++ b/.changelog/18076.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_storagegateway_gateway: Add support for `smb_file_share_visibility`. +``` diff --git a/.changelog/18081.txt b/.changelog/18081.txt new file mode 100644 index 000000000000..98d80366abc7 --- /dev/null +++ b/.changelog/18081.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_rds_cluster: Database port is updated in-place +``` diff --git a/.changelog/18083.txt b/.changelog/18083.txt new file mode 100644 index 000000000000..7aff48adf89d --- /dev/null +++ b/.changelog/18083.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudfront_monitoring_subscription +``` diff --git a/.changelog/18102.txt b/.changelog/18102.txt new file mode 100644 index 000000000000..5f780f28d12b --- /dev/null +++ b/.changelog/18102.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_lb_target_group: Add support for `app_cookie` stickiness type and `cookie_name` argument +``` diff --git a/.changelog/18106.txt b/.changelog/18106.txt new file mode 100644 index 000000000000..1b5fb8bb0aa3 --- /dev/null +++ b/.changelog/18106.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_appmesh_virtual_gateway: Add `spec.backend_defaults.client_policy.tls.certificate`, `spec.backend_defaults.client_policy.tls.validation.subject_alternative_names`, `spec.listener.tls.certificate` and `spec.listener.tls.validation.subject_alternative_names` attributes to support mutual TLS authentication +``` + +```release-note:enhancement +resource/aws_appmesh_virtual_gateway: Add `spec.backend_defaults.client_policy.tls.validation.trust.sds` and `spec.listener.tls.validation.trust.sds` attributes to support Envoy Service Discovery Service certificates +``` diff --git a/.changelog/18115.txt b/.changelog/18115.txt new file mode 100644 index 000000000000..5e63da6ff5b5 --- /dev/null +++ b/.changelog/18115.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_appautoscaling_target: Ignore `ObjectNotFoundException` on deletion +``` diff --git a/.changelog/18117.txt b/.changelog/18117.txt new file mode 100644 index 000000000000..bbbc8e18b603 --- /dev/null +++ b/.changelog/18117.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_kms_key: Add `bypass_policy_lockout_safety_check` argument +``` + +```release-note:enhancement +resource/aws_kms_external_key: Add `bypass_policy_lockout_safety_check` argument +``` diff --git a/.changelog/18127.txt b/.changelog/18127.txt new file mode 100644 index 000000000000..aee384e3c9e3 --- /dev/null +++ b/.changelog/18127.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_appmesh_virtual_node: Add `spec.backend.virtual_service.client_policy.tls.certificate`, `spec.backend.virtual_service.client_policy.tls.validation.subject_alternative_names`, `spec.backend_defaults.client_policy.tls.certificate`, `spec.backend_defaults.client_policy.tls.validation.subject_alternative_names`, `spec.listener.tls.certificate` and `spec.listener.tls.validation.subject_alternative_names` attributes to support mutual TLS authentication +``` + +```release-note:enhancement +resource/aws_appmesh_virtual_node: Add `spec.backend.virtual_service.client_policy.tls.validation.trust.sds`, `spec.backend_defaults.client_policy.tls.validation.trust.sds` and `spec.listener.tls.validation.trust.sds` attributes to support Envoy Service Discovery Service certificates +``` diff --git a/.changelog/18129.txt b/.changelog/18129.txt new file mode 100644 index 000000000000..2c5d15a2e64d --- /dev/null +++ b/.changelog/18129.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_codestarconnections_connection +``` diff --git a/.changelog/18203.txt b/.changelog/18203.txt new file mode 100644 index 000000000000..4ea0164c8970 --- /dev/null +++ b/.changelog/18203.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lakeformation_permissions: Properly serialize SELECT permission for `permissions` and `permissions_with_grant_option` fields +``` diff --git a/.changelog/18215.txt b/.changelog/18215.txt new file mode 100644 index 000000000000..641ef1326182 --- /dev/null +++ b/.changelog/18215.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_dynamodb_table: Update Global Secondary Index provisioned throughput settings on new changes +``` diff --git a/.changelog/18276.txt b/.changelog/18276.txt new file mode 100644 index 000000000000..b94e07e663c4 --- /dev/null +++ b/.changelog/18276.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_iam_policy: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_policy: Add `policy_id` attribute +``` + +```release-note:enhancement +data-source/aws_iam_policy: Add `policy_id` and `tags` attributes +``` diff --git a/.changelog/18281.txt b/.changelog/18281.txt new file mode 100644 index 000000000000..d119487f700b --- /dev/null +++ b/.changelog/18281.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_pinpoint_sms_channel: Set all params on update +``` \ No newline at end of file diff --git a/.changelog/18305.txt b/.changelog/18305.txt new file mode 100644 index 000000000000..14fd969ad2b3 --- /dev/null +++ b/.changelog/18305.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_pinpoint_event_stream: Retry on eventual consistency error +``` + +```release-note:enhancement +resource/aws_pinpoint_event_stream: Plan time validations for `destination_stream_arn` and `role_arn` +``` diff --git a/.changelog/18313.txt b/.changelog/18313.txt new file mode 100644 index 000000000000..b64ff580aba7 --- /dev/null +++ b/.changelog/18313.txt @@ -0,0 +1,11 @@ +```release-note:note +resource/aws_storagegateway_upload_buffer: The Storage Gateway `ListLocalDisks` API operation has been implemented to support the `disk_path` attribute for Cached and VTL gateway types. Environments using restrictive IAM permissions may require updates. +``` + +```release-note:bug +data-source/aws_storagegateway_local_disk: Allow `disk_path` reference on `disk_node` lookup and vice-versa +``` + +```release-note:enhancement +resource/aws_storagegateway_upload_buffer: Add `disk_path` argument for Cached and VTL gateways +``` diff --git a/.changelog/18314.txt b/.changelog/18314.txt new file mode 100644 index 000000000000..579cda25c122 --- /dev/null +++ b/.changelog/18314.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_pinpoint_email_channel: Add `configuration_set` argument +``` + +```release-note:enhancement +resource/aws_pinpoint_email_channel: Add plan time validation for `identity` and `role_arn` +``` \ No newline at end of file diff --git a/.changelog/18315.txt b/.changelog/18315.txt new file mode 100644 index 000000000000..e9a60628eefd --- /dev/null +++ b/.changelog/18315.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_backup_plan: Add `enable_continuous_backup` argument +``` \ No newline at end of file diff --git a/.changelog/18319.txt b/.changelog/18319.txt new file mode 100644 index 000000000000..6e6e4eb0efc4 --- /dev/null +++ b/.changelog/18319.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_efs_file_system: Add `availability_zone_id` and `availability_zone_name` attributes +``` + +```release-note:enhancement +resource/aws_efs_file_system: Add `availability_zone_id` attribute and `availability_zone_name` argument +``` \ No newline at end of file diff --git a/.changelog/18320.txt b/.changelog/18320.txt new file mode 100644 index 000000000000..2126a3a01a81 --- /dev/null +++ b/.changelog/18320.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_codebuild_project: Add `concurrent_build_limit` argument to specify build concurrency. +``` \ No newline at end of file diff --git a/.changelog/18336.txt b/.changelog/18336.txt new file mode 100644 index 000000000000..9a2317c00e75 --- /dev/null +++ b/.changelog/18336.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_batch_job_definition: Add `propagate_tags` argument +``` \ No newline at end of file diff --git a/.changelog/18341.txt b/.changelog/18341.txt new file mode 100644 index 000000000000..e732da434026 --- /dev/null +++ b/.changelog/18341.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_securityhub_organization_admin_account: Retry on `ResourceConflictException` error during creation +``` \ No newline at end of file diff --git a/.changelog/18351.txt b/.changelog/18351.txt new file mode 100644 index 000000000000..e48f26687bbd --- /dev/null +++ b/.changelog/18351.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_apigatewayv2_domain_name: Allow update of mutual TLS S3 object version +``` diff --git a/.changelog/18361.txt b/.changelog/18361.txt new file mode 100644 index 000000000000..5e50400f3e63 --- /dev/null +++ b/.changelog/18361.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_replication_group: Prevents re-creation of secondary replication groups when encryption is enabled +``` diff --git a/.changelog/18373.txt b/.changelog/18373.txt new file mode 100644 index 000000000000..499d30ce431c --- /dev/null +++ b/.changelog/18373.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dynamodb_table: Add `kms_key_arn` argument to `replica` configuration block +``` diff --git a/.changelog/18382.txt b/.changelog/18382.txt new file mode 100644 index 000000000000..46a43e86818e --- /dev/null +++ b/.changelog/18382.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_api_gateway_vpc_link: Persist ID of failed VPC Link to state +``` diff --git a/.changelog/18384.txt b/.changelog/18384.txt new file mode 100644 index 000000000000..a1cf0941716a --- /dev/null +++ b/.changelog/18384.txt @@ -0,0 +1,4 @@ +```release-note:bug +resource/aws_wafv2_web_acl_logging_configuration: Remove deprecation warning for `redacted_fields` `single_header` argument +``` + diff --git a/.changelog/18388.txt b/.changelog/18388.txt new file mode 100644 index 000000000000..ea024aa9ef5a --- /dev/null +++ b/.changelog/18388.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_network_acl: Handle EC2 eventual consistency errors on creation +``` + +```release-note:bug +resource/aws_network_acl_rule: Handle EC2 eventual consistency errors on creation +``` diff --git a/.changelog/18391.txt b/.changelog/18391.txt new file mode 100644 index 000000000000..e7a0ee19eff5 --- /dev/null +++ b/.changelog/18391.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc: Handle EC2 eventual consistency errors on creation +``` diff --git a/.changelog/18392.txt b/.changelog/18392.txt new file mode 100644 index 000000000000..34bcc004200e --- /dev/null +++ b/.changelog/18392.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_subnet: Handle EC2 eventual consistency errors on creation +``` diff --git a/.changelog/18404.txt b/.changelog/18404.txt new file mode 100644 index 000000000000..43e2b28b2fe8 --- /dev/null +++ b/.changelog/18404.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_synthetics_canary: Handle asynchronous IAM eventual consistency error on creation +``` diff --git a/.changelog/18410.txt b/.changelog/18410.txt new file mode 100644 index 000000000000..a15b77ed2c2b --- /dev/null +++ b/.changelog/18410.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_apigatewayv2_route: Add `request_parameter` attribute +``` diff --git a/.changelog/18435.txt b/.changelog/18435.txt new file mode 100644 index 000000000000..5ea0a0f21d55 --- /dev/null +++ b/.changelog/18435.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_iam_role: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_role_policy: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_role_policy_attachment: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18445.txt b/.changelog/18445.txt new file mode 100644 index 000000000000..2197886c0f3a --- /dev/null +++ b/.changelog/18445.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_kinesis_firehose_delivery_stream +``` diff --git a/.changelog/18454.txt b/.changelog/18454.txt new file mode 100644 index 000000000000..a6676bb52fea --- /dev/null +++ b/.changelog/18454.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3outposts_endpoint: Extends creation timeout to 20 minutes +``` diff --git a/.changelog/18458.txt b/.changelog/18458.txt new file mode 100644 index 000000000000..8ddc097046f2 --- /dev/null +++ b/.changelog/18458.txt @@ -0,0 +1,23 @@ +```release-note:bug +resource/aws_iam_user: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_user_group_membership: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_user_login_profile: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_user_policy: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_user_policy_attachment: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_user_ssh_key: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18459.txt b/.changelog/18459.txt new file mode 100644 index 000000000000..ca878810f9f3 --- /dev/null +++ b/.changelog/18459.txt @@ -0,0 +1,15 @@ +```release-note:bug +resource/aws_iam_group: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_group_membership: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_group_policy: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_iam_group_policy_attachment: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18462.txt b/.changelog/18462.txt new file mode 100644 index 000000000000..537f8aba0957 --- /dev/null +++ b/.changelog/18462.txt @@ -0,0 +1,15 @@ +```release-note:bug +resource/aws_secretsmanager_secret: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_secretsmanager_secret_policy: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_secretsmanager_secret_rotation: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_secretsmanager_secret_version: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18464.txt b/.changelog/18464.txt new file mode 100644 index 000000000000..a7b5ea423180 --- /dev/null +++ b/.changelog/18464.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_ecr_lifecycle_policy: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_ecr_repository: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_ecr_repository_policy: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18465.txt b/.changelog/18465.txt new file mode 100644 index 000000000000..4c6f0c175de0 --- /dev/null +++ b/.changelog/18465.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc_endpoint_route_table_association: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18466.txt b/.changelog/18466.txt new file mode 100644 index 000000000000..595fc08ff4ab --- /dev/null +++ b/.changelog/18466.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_network_interface_sg_attachment: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18470.txt b/.changelog/18470.txt new file mode 100644 index 000000000000..3b7dec113471 --- /dev/null +++ b/.changelog/18470.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ec2_transit_gateway_route_table_propagation: Wait for enable and disable operations to complete +``` diff --git a/.changelog/18472.txt b/.changelog/18472.txt new file mode 100644 index 000000000000..8b79664af6ed --- /dev/null +++ b/.changelog/18472.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc_dhcp_options_association: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18473.txt b/.changelog/18473.txt new file mode 100644 index 000000000000..c5ad43b2ef97 --- /dev/null +++ b/.changelog/18473.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_spot_instance_request: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18475.txt b/.changelog/18475.txt new file mode 100644 index 000000000000..88a0b33593d3 --- /dev/null +++ b/.changelog/18475.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_sns_topic_subscription: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_sns_topic_subscription: Enforce lowercase `protocol` argument validation to match API and prevent resource errors +``` diff --git a/.changelog/18486.txt b/.changelog/18486.txt new file mode 100644 index 000000000000..1f5d097c0fc9 --- /dev/null +++ b/.changelog/18486.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_docdb_cluster_parameter_group: Read all user parameters and parameters specified in the configuration. +``` diff --git a/.changelog/18491.txt b/.changelog/18491.txt new file mode 100644 index 000000000000..225dc3f2efe5 --- /dev/null +++ b/.changelog/18491.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_target: Support partner event bus names +``` + +```release-note:enhancement +resource/aws_cloudwatch_event_rule: Support partner event bus names +``` diff --git a/.changelog/18494.txt b/.changelog/18494.txt new file mode 100644 index 000000000000..1cb4873fcb85 --- /dev/null +++ b/.changelog/18494.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_insight +``` \ No newline at end of file diff --git a/.changelog/18505.txt b/.changelog/18505.txt new file mode 100644 index 000000000000..8082c8f1bfc6 --- /dev/null +++ b/.changelog/18505.txt @@ -0,0 +1,3 @@ +```release-note:bug + resource/aws_lakeformation_permissions: Fix issues related to permissions not being revoked and attempts to revoke non-existent permissions + ``` \ No newline at end of file diff --git a/.changelog/18512.txt b/.changelog/18512.txt new file mode 100644 index 000000000000..c83dfcf94685 --- /dev/null +++ b/.changelog/18512.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cognito_user_pool: Allow schema items to be added without recreating resource. +``` diff --git a/.changelog/18529.txt b/.changelog/18529.txt new file mode 100644 index 000000000000..e9c77f076092 --- /dev/null +++ b/.changelog/18529.txt @@ -0,0 +1,27 @@ +```release-note:bug +resource/aws_appmesh_gateway_route: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_mesh: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_route: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_virtual_gateway: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_virtual_router: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_virtual_service: Handle read-after-create eventual consistency +``` + +```release-note:bug +resource/aws_appmesh_virtual_node: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18547.txt b/.changelog/18547.txt new file mode 100644 index 000000000000..eaac1d3a5a13 --- /dev/null +++ b/.changelog/18547.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_location_s3: Add `agent_arns` argument +``` \ No newline at end of file diff --git a/.changelog/18558.txt b/.changelog/18558.txt new file mode 100644 index 000000000000..28e093c5c48f --- /dev/null +++ b/.changelog/18558.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_route53_resolver_firewall_domain_list +``` diff --git a/.changelog/18562.txt b/.changelog/18562.txt new file mode 100644 index 000000000000..7d25f37b7a3c --- /dev/null +++ b/.changelog/18562.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_domain: Add support for `retention_policy` +``` \ No newline at end of file diff --git a/.changelog/18564.txt b/.changelog/18564.txt new file mode 100644 index 000000000000..33d6bc7eec06 --- /dev/null +++ b/.changelog/18564.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_codedeploy_app: Add `arn`, `linked_to_github`, `github_account_name`, `application_id` attributes +``` + +```release-note:enhancement +resource/aws_codedeploy_app: Add `tags` argument +``` + +```release-note:enhancement +resource/aws_codedeploy_app: Add plan time validation for `name` +``` \ No newline at end of file diff --git a/.changelog/18579.txt b/.changelog/18579.txt new file mode 100644 index 000000000000..adfc417b77cc --- /dev/null +++ b/.changelog/18579.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_cloudformation_type +``` + +```release-note:new-data-source +aws_cloudformation_type +``` diff --git a/.changelog/18580.txt b/.changelog/18580.txt new file mode 100644 index 000000000000..1b72b5724ff8 --- /dev/null +++ b/.changelog/18580.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudhsm_v2_hsm: Prevent orphaned HSM Instances by additionally matching on ENI identifier during lookup +``` diff --git a/.changelog/18585.txt b/.changelog/18585.txt new file mode 100644 index 000000000000..65437a125843 --- /dev/null +++ b/.changelog/18585.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_iam_roles +``` diff --git a/.changelog/18588.txt b/.changelog/18588.txt new file mode 100644 index 000000000000..605749dafbd8 --- /dev/null +++ b/.changelog/18588.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_parameter: Allow `allowed_pattern` and `description` arguments to be empty strings +``` diff --git a/.changelog/18598.txt b/.changelog/18598.txt new file mode 100644 index 000000000000..9ec0eb19b64c --- /dev/null +++ b/.changelog/18598.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_rds_global_cluster: Allow `engine_version` to be upgraded in place. +``` diff --git a/.changelog/18600.txt b/.changelog/18600.txt new file mode 100644 index 000000000000..89ecb2ab5dc2 --- /dev/null +++ b/.changelog/18600.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_fms_policy: Use API model regular expression for `resource_type` and `resource_type_list` argument plan time validation +``` diff --git a/.changelog/18611.txt b/.changelog/18611.txt new file mode 100644 index 000000000000..ae5c3ad4ba7d --- /dev/null +++ b/.changelog/18611.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_object_copy: Add `bucket_key_enabled` argument +``` diff --git a/.changelog/18634.txt b/.changelog/18634.txt new file mode 100644 index 000000000000..76abad7bc788 --- /dev/null +++ b/.changelog/18634.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_target_group: Handle read-after-create eventual consistency +``` diff --git a/.changelog/18635.txt b/.changelog/18635.txt new file mode 100644 index 000000000000..4bc2b1d8b138 --- /dev/null +++ b/.changelog/18635.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_replication_group: Remmoves incorrect plan-time validation for `automatic_failover_enabled` +``` diff --git a/.changelog/18640.txt b/.changelog/18640.txt new file mode 100644 index 000000000000..2978fdcdbf65 --- /dev/null +++ b/.changelog/18640.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_parameter: Allow `tags` to be applied to resource when `overwrite` is configured +``` diff --git a/.changelog/18644.txt b/.changelog/18644.txt new file mode 100644 index 000000000000..2b398be3fcae --- /dev/null +++ b/.changelog/18644.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudfront_distribution: Add `trusted_key_groups` argument +``` \ No newline at end of file diff --git a/.changelog/18667.txt b/.changelog/18667.txt new file mode 100644 index 000000000000..120c908fb7b7 --- /dev/null +++ b/.changelog/18667.txt @@ -0,0 +1,3 @@ +```release-note:bug + resource/aws_xray_sampling_rule: Change the maximum length of `rule_name` from 128 to 32 +``` diff --git a/.changelog/18712.txt b/.changelog/18712.txt new file mode 100644 index 000000000000..99d6b7d7de7c --- /dev/null +++ b/.changelog/18712.txt @@ -0,0 +1,4 @@ +```release-note:new-resource +aws_route53_resolver_firewall_rule +``` + diff --git a/.changelog/18716.txt b/.changelog/18716.txt new file mode 100644 index 000000000000..c2bf6e49db1c --- /dev/null +++ b/.changelog/18716.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_codedeploy_deployment_group: Add `arn`, `compute_platform`, and `deployment_group_id` attributes +``` + +```release-note:enhancement +resource/aws_codedeploy_deployment_group: Add `tags` argument +``` + +```release-note:enhancement +resource/aws_codedeploy_deployment_group: Add plan time validation for `terminate_blue_instances_on_deployment_success.termination_wait_time_in_minutes`, `service_role_arn`, `load_balancer_info.target_group_pair_info.prod_traffic_route.listener_arns`, `load_balancer_info.target_group_pair_info.test_traffic_route.listener_arns`, `trigger_configuration.trigger_target_arn` +``` + +```release-note:enhancement +resource/aws_codedeploy_deployment_group: Updating `deployment_group_name` doesnt recreate group +``` \ No newline at end of file diff --git a/.changelog/18733.txt b/.changelog/18733.txt new file mode 100644 index 000000000000..d75f326a0c9a --- /dev/null +++ b/.changelog/18733.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_route53_resolver_firewall_config +``` \ No newline at end of file diff --git a/.changelog/18734.txt b/.changelog/18734.txt new file mode 100644 index 000000000000..f03abd432bbb --- /dev/null +++ b/.changelog/18734.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_group: Add Warm Pool support +``` diff --git a/.changelog/18802.txt b/.changelog/18802.txt new file mode 100644 index 000000000000..aa95f71403f4 --- /dev/null +++ b/.changelog/18802.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_glue_connection +``` + +```release-note:new-data-source +aws_glue_data_catalog_encryption_settings +``` \ No newline at end of file diff --git a/.changelog/18803.txt b/.changelog/18803.txt new file mode 100644 index 000000000000..d948e6d6d823 --- /dev/null +++ b/.changelog/18803.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_subnets +``` \ No newline at end of file diff --git a/.changelog/18818.txt b/.changelog/18818.txt new file mode 100644 index 000000000000..508324a8c5f3 --- /dev/null +++ b/.changelog/18818.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_parameter_group: Allow parameter values to be mixed case, prioritize certain parameters when chunking, and avoid diffs with mixed-case parameter names +``` \ No newline at end of file diff --git a/.changelog/18841.txt b/.changelog/18841.txt new file mode 100644 index 000000000000..d86134777817 --- /dev/null +++ b/.changelog/18841.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_network_interface: Add `interface_type` argument +``` + +```release-note:enhancement +resource/aws_launch_template: Add `interface_type` argument to `network_interfaces` configuration block +``` \ No newline at end of file diff --git a/.changelog/18843.txt b/.changelog/18843.txt new file mode 100644 index 000000000000..593ccb752875 --- /dev/null +++ b/.changelog/18843.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_codebuild_project: Fix removing `secondary_sources` and `secondary_artifacts` +``` + +```release-note:bug +resource/aws_codebuild_project: Allow fetching submodules for bitbucket source types +``` + +```release-note:enhancement +resource/aws_codebuild_project: Add plan time validation for `secondary_artifacts`, `secondary_sources`, `service_role` +``` diff --git a/.changelog/18855.txt b/.changelog/18855.txt new file mode 100644 index 000000000000..1cab7149fc77 --- /dev/null +++ b/.changelog/18855.txt @@ -0,0 +1,3 @@ +```release-note:note +provider: The HTTP User-Agent header has been reordered so the AWS SDK Go product is last, except when using the TF_APPEND_USER_AGENT environment variable. Environments dependent on the previous User-Agent header ordering may require updates. +``` diff --git a/.changelog/18861.txt b/.changelog/18861.txt new file mode 100644 index 000000000000..b29191c0d4dc --- /dev/null +++ b/.changelog/18861.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_organizations_organizational_unit: Add `tags` argument +``` diff --git a/.changelog/18870.txt b/.changelog/18870.txt new file mode 100644 index 000000000000..f7916bd72fdb --- /dev/null +++ b/.changelog/18870.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudwatch_metric_stream +``` \ No newline at end of file diff --git a/.changelog/18873.txt b/.changelog/18873.txt new file mode 100644 index 000000000000..aeaa38d46b2f --- /dev/null +++ b/.changelog/18873.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_kms_public_key +``` diff --git a/.changelog/18880.txt b/.changelog/18880.txt new file mode 100644 index 000000000000..3299d7e032c8 --- /dev/null +++ b/.changelog/18880.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Make `instance_initiated_shutdown_behavior` also computed, allowing value to be read +``` diff --git a/.changelog/18881.txt b/.changelog/18881.txt new file mode 100644 index 000000000000..a87a64d88cf6 --- /dev/null +++ b/.changelog/18881.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_rds_proxy_endpoint +``` diff --git a/.changelog/18882.txt b/.changelog/18882.txt new file mode 100644 index 000000000000..373fcfa7a7fb --- /dev/null +++ b/.changelog/18882.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ecr_replication_configuration: Remove relication rules on resource deletion +``` diff --git a/.changelog/18905.txt b/.changelog/18905.txt new file mode 100644 index 000000000000..69e0051652bb --- /dev/null +++ b/.changelog/18905.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_cloudwatch_event_connection +``` + +```release-note:new-resource +aws_cloudwatch_event_api_destination +``` + +```release-note:new-data-source +aws_cloudwatch_event_connection +``` \ No newline at end of file diff --git a/.changelog/18909.txt b/.changelog/18909.txt new file mode 100644 index 000000000000..49636526ba62 --- /dev/null +++ b/.changelog/18909.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_eip: Tags are created for EIPs which default to vpc domain +``` diff --git a/.changelog/18918.txt b/.changelog/18918.txt new file mode 100644 index 000000000000..609a8e76370b --- /dev/null +++ b/.changelog/18918.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_efs_mount_target: Add `access_point_id`, `file_system_id` arguments +``` diff --git a/.changelog/18920.txt b/.changelog/18920.txt new file mode 100644 index 000000000000..b6856076dfe8 --- /dev/null +++ b/.changelog/18920.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_elasticache_cluster: Allows specifying Redis 6.x +``` + +```release-note:bug +resource/aws_elasticache_replication_group: Allows specifying Redis 6.x +``` + +```release-note:enhancement +resource/aws_elasticache_global_replication_group: Adds parameter `engine_version_actual` to match other ElastiCache resources +``` diff --git a/.changelog/18932.txt b/.changelog/18932.txt new file mode 100644 index 000000000000..b97353b452bb --- /dev/null +++ b/.changelog/18932.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_task: Add `options` `log_level` attribute +``` diff --git a/.changelog/18958.txt b/.changelog/18958.txt new file mode 100644 index 000000000000..f260b3e8f5c9 --- /dev/null +++ b/.changelog/18958.txt @@ -0,0 +1,3 @@ +```release-note:bug +provider: Prevent `Provider produced inconsistent final plan` errors when resource `tags` are not known until apply +``` diff --git a/.changelog/19051.txt b/.changelog/19051.txt new file mode 100644 index 000000000000..2eeea4daea64 --- /dev/null +++ b/.changelog/19051.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl_logging_configuration: Add `logging_filter` argument +``` diff --git a/.changelog/19059.txt b/.changelog/19059.txt new file mode 100644 index 000000000000..520b67421c9b --- /dev/null +++ b/.changelog/19059.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpc_endpoint: Fix auto_accept failing while waiting for the VPC Endpoint Connection acceptance +``` diff --git a/.changelog/19069.txt b/.changelog/19069.txt new file mode 100644 index 000000000000..7086bc6ae146 --- /dev/null +++ b/.changelog/19069.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_macie2_account +``` diff --git a/.changelog/19072.txt b/.changelog/19072.txt new file mode 100644 index 000000000000..6e7ceeb7c253 --- /dev/null +++ b/.changelog/19072.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_bus: Support partner event bus creation +``` diff --git a/.changelog/19077.txt b/.changelog/19077.txt new file mode 100644 index 000000000000..d5cfe5447e26 --- /dev/null +++ b/.changelog/19077.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_vpn_connection: Prevent flipped `tunnel1_*` and `tunnel2_*` ordering when `tunnel1_inside_cidr`, `tunnel1_inside_ipv6_cidr`, or `tunnel1_preshared_key` is configured +``` diff --git a/.changelog/19078.txt b/.changelog/19078.txt new file mode 100644 index 000000000000..edf7566ac0b5 --- /dev/null +++ b/.changelog/19078.txt @@ -0,0 +1,23 @@ +```release-note:enhancement +data/source_aws_eks_addon: added validation for `cluster_name` +``` + +```release-note:enhancement +data/source_aws_eks_cluster: added validation for `cluster_name` +``` + +```release-note:enhancement +resource/aws_eks_addon: added validation for `cluster_name` +``` + +```release-note:enhancement +resource/aws_eks_cluster: added validation for `name` +``` + +```release-note:enhancement +resource/aws_eks_fargate_profile: added validation for `cluster_name` +``` + +```release-note:enhancement +resource/aws_eks_node_group: added validation for `cluster_name` +``` diff --git a/.changelog/19084.txt b/.changelog/19084.txt new file mode 100644 index 000000000000..75c2f2a4b252 --- /dev/null +++ b/.changelog/19084.txt @@ -0,0 +1,3 @@ +```release-note:note +provider: `default_tags` support generally available to all provider resources that support `tags` with the exception of `aws_autoscaling_group` +``` \ No newline at end of file diff --git a/.changelog/19095.txt b/.changelog/19095.txt new file mode 100644 index 000000000000..83292381d379 --- /dev/null +++ b/.changelog/19095.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ec2_managed_prefix_list: Prevent `entry` `description` update errors +``` diff --git a/.changelog/19100.txt b/.changelog/19100.txt new file mode 100644 index 000000000000..471cee52e868 --- /dev/null +++ b/.changelog/19100.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_schemas_discoverer +``` + +```release-note:new-resource +aws_schemas_registry +``` + +```release-note:new-resource +aws_schemas_schema +``` \ No newline at end of file diff --git a/.changelog/19108.txt b/.changelog/19108.txt new file mode 100644 index 000000000000..f6266443b273 --- /dev/null +++ b/.changelog/19108.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_organization_configuration +``` diff --git a/.changelog/19116.txt b/.changelog/19116.txt new file mode 100644 index 000000000000..bb57b699117f --- /dev/null +++ b/.changelog/19116.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_storagegateway_gateway: Correctly handle additional error message returned in some regions +``` diff --git a/.changelog/19119.txt b/.changelog/19119.txt new file mode 100644 index 000000000000..0849343e6245 --- /dev/null +++ b/.changelog/19119.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_elasticache_subnet_group: Add `tags` argument +``` \ No newline at end of file diff --git a/.changelog/19121.txt b/.changelog/19121.txt new file mode 100644 index 000000000000..0a6a9b2beaf3 --- /dev/null +++ b/.changelog/19121.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_batch_job_queue: Recreate batch job queue if the `name` changes +``` \ No newline at end of file diff --git a/.changelog/19122.txt b/.changelog/19122.txt new file mode 100644 index 000000000000..f633553d08cc --- /dev/null +++ b/.changelog/19122.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_product +``` diff --git a/.changelog/19144.txt b/.changelog/19144.txt new file mode 100644 index 000000000000..2356f160f698 --- /dev/null +++ b/.changelog/19144.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_cluster: Allow updates to `encryption_config` +``` \ No newline at end of file diff --git a/.changelog/19160.txt b/.changelog/19160.txt new file mode 100644 index 000000000000..f588e4eeadb3 --- /dev/null +++ b/.changelog/19160.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_crawler: Allow '/' in `name` argument +``` diff --git a/.changelog/19164.txt b/.changelog/19164.txt new file mode 100644 index 000000000000..e0fa66fb0a6e --- /dev/null +++ b/.changelog/19164.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_route53_resolver_firewall_rule_group_association +``` \ No newline at end of file diff --git a/.changelog/19165.txt b/.changelog/19165.txt new file mode 100644 index 000000000000..ae3bd1c13300 --- /dev/null +++ b/.changelog/19165.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_macie2_classification_job +``` diff --git a/.changelog/19168.txt b/.changelog/19168.txt new file mode 100644 index 000000000000..51c89c04c98c --- /dev/null +++ b/.changelog/19168.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_shield_protection: Add `tags` argument +``` + +```release-note:enhancement +resource/aws_shield_protection: Add `arn` attribute +``` + +```release-note:enhancement +resource/aws_shield_protection: Missing resources are now detected and recreated +``` diff --git a/.changelog/19172.txt b/.changelog/19172.txt new file mode 100644 index 000000000000..397095471009 --- /dev/null +++ b/.changelog/19172.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_glue_connection: Add plan time validation for `connection_properties`, `description`, `match_criteria`, `name`, and `physical_connection_requirements.security_group_id_list` +``` \ No newline at end of file diff --git a/.changelog/19176.txt b/.changelog/19176.txt new file mode 100644 index 000000000000..519254b1fa8c --- /dev/null +++ b/.changelog/19176.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cognito_identity_pool: Add allow_classic_flow argument +``` diff --git a/.changelog/19190.txt b/.changelog/19190.txt new file mode 100644 index 000000000000..19c851f96a66 --- /dev/null +++ b/.changelog/19190.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_location_s3: Add `s3_storage_class` argument +``` \ No newline at end of file diff --git a/.changelog/19205.txt b/.changelog/19205.txt new file mode 100644 index 000000000000..b304309bb476 --- /dev/null +++ b/.changelog/19205.txt @@ -0,0 +1,7 @@ +```release-note:bug +aws_batch_compute_environment: `service_role` argument is optional +``` + +```release-note:bug +aws_batch_compute_environment: Allow update of just `service_role` for managed compute environments +``` \ No newline at end of file diff --git a/.changelog/19207.txt b/.changelog/19207.txt new file mode 100644 index 000000000000..da7a097aac52 --- /dev/null +++ b/.changelog/19207.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_batch_job_definition: Prevent diff with default value of `fargatePlatformConfiguration` +``` \ No newline at end of file diff --git a/.changelog/19216.txt b/.changelog/19216.txt new file mode 100644 index 000000000000..4d2e9d457e02 --- /dev/null +++ b/.changelog/19216.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssoadmin_managed_policy_attachment: Retry attachment/detachment when other permission-set attachment event was not yet propagated, to avoid ConflictException. +``` \ No newline at end of file diff --git a/.changelog/19219.txt b/.changelog/19219.txt new file mode 100644 index 000000000000..fc0acf46c4d2 --- /dev/null +++ b/.changelog/19219.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_cloudwatch_event_source +``` diff --git a/.changelog/19251.txt b/.changelog/19251.txt new file mode 100644 index 000000000000..835d575b4943 --- /dev/null +++ b/.changelog/19251.txt @@ -0,0 +1,3 @@ +```release-note:bug +provider: Prevent `Provider produced inconsistent final plan` errors when lifecycle arguments apply to resource `tags` not known until apply +``` \ No newline at end of file diff --git a/.changelog/19253.txt b/.changelog/19253.txt new file mode 100644 index 000000000000..a781a427e4e8 --- /dev/null +++ b/.changelog/19253.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Support reading `starting_position` and `starting_position_timestamp` attributes +``` diff --git a/.changelog/19254.txt b/.changelog/19254.txt new file mode 100644 index 000000000000..59395ea1a740 --- /dev/null +++ b/.changelog/19254.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_macie2_custom_data_identifier +``` diff --git a/.changelog/19266.txt b/.changelog/19266.txt new file mode 100644 index 000000000000..9fb84153fb81 --- /dev/null +++ b/.changelog/19266.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_mwaa_environment: Correctly apply `plugins_s3_object_version` change +``` \ No newline at end of file diff --git a/.changelog/19278.txt b/.changelog/19278.txt new file mode 100644 index 000000000000..2d83b0f17b79 --- /dev/null +++ b/.changelog/19278.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_servicecatalog_organizations_access +``` + +```release-note:new-resource +aws_servicecatalog_portfolio_share +``` \ No newline at end of file diff --git a/.changelog/19283.txt b/.changelog/19283.txt new file mode 100644 index 000000000000..b9eac0a42287 --- /dev/null +++ b/.changelog/19283.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_macie2_findings_filter +``` diff --git a/.changelog/19284.txt b/.changelog/19284.txt new file mode 100644 index 000000000000..969878abda54 --- /dev/null +++ b/.changelog/19284.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_codestarconnections_connection: Add `host_arn` argument +``` + +```release-note:enhancement +data-source/aws_codestarconnections_connection: Add `host_arn` attribute +``` \ No newline at end of file diff --git a/.changelog/19285.txt b/.changelog/19285.txt new file mode 100644 index 000000000000..abeef994f8ef --- /dev/null +++ b/.changelog/19285.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_lb_listener_rule: Add plan time validation to `listener_arn`, `action.target_group_arn`, `action.forward.target_group.arn`, `action.redirect.host`, `action.redirect.path`, `action.redirect.query`, `action.redirect.status_code`, `action.fixed_response.message_body`, `action.authenticate_cognito.user_pool_arn`. +``` + +```release-note:enhancement +resource/aws_lb_listener_rule: Add tagging support. +``` \ No newline at end of file diff --git a/.changelog/19286.txt b/.changelog/19286.txt new file mode 100644 index 000000000000..8d7392477034 --- /dev/null +++ b/.changelog/19286.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_lb_listener: Add `tags` argument & `tags_all` attribute. +``` + +```release-note:enhancement +data-source/aws_lb_listener: Add `tags` attribute. +``` diff --git a/.changelog/19300.txt b/.changelog/19300.txt new file mode 100644 index 000000000000..0d41d4d23f27 --- /dev/null +++ b/.changelog/19300.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_tag_option +``` \ No newline at end of file diff --git a/.changelog/19303.txt b/.changelog/19303.txt new file mode 100644 index 000000000000..fbad99c8dc5b --- /dev/null +++ b/.changelog/19303.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_macie2_organization_admin_account +``` diff --git a/.changelog/19304.txt b/.changelog/19304.txt new file mode 100644 index 000000000000..183dd6c33bcb --- /dev/null +++ b/.changelog/19304.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_macie2_member +``` + +```release-note:new-resource +aws_macie2_invitation_accepter +``` diff --git a/.changelog/19307.txt b/.changelog/19307.txt new file mode 100644 index 000000000000..2c375c1a0d90 --- /dev/null +++ b/.changelog/19307.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_appconfig_application +``` + +```release-note:new-resource +aws_appconfig_environment +``` diff --git a/.changelog/19315.txt b/.changelog/19315.txt new file mode 100644 index 000000000000..f0c08f0b7b35 --- /dev/null +++ b/.changelog/19315.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_cloudfront_distribution: Add `function_association` argument to `ordered_cache_behavior` and `default_cache_behavior` configuration blocks +``` + +```release-note:new-resource +aws_cloudfront_function +``` + +```release-note:new-data-source +aws_cloudfront_function +``` \ No newline at end of file diff --git a/.changelog/19316.txt b/.changelog/19316.txt new file mode 100644 index 000000000000..e29d6221bf7a --- /dev/null +++ b/.changelog/19316.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_provisioning_artifact +``` diff --git a/.changelog/19320.txt b/.changelog/19320.txt new file mode 100644 index 000000000000..c4cdbdca9d43 --- /dev/null +++ b/.changelog/19320.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appconfig_configuration_profile +``` diff --git a/.changelog/19323.txt b/.changelog/19323.txt new file mode 100644 index 000000000000..a4855edd4c65 --- /dev/null +++ b/.changelog/19323.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_bucket: Add the delete_marker_replication_status argument for V2 replication configurations +``` diff --git a/.changelog/19324.txt b/.changelog/19324.txt new file mode 100644 index 000000000000..14d91b6069a8 --- /dev/null +++ b/.changelog/19324.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appconfig_hosted_configuration_version +``` diff --git a/.changelog/19337.txt b/.changelog/19337.txt new file mode 100644 index 000000000000..07985056ae75 --- /dev/null +++ b/.changelog/19337.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_target: Add `http_target` argument +``` \ No newline at end of file diff --git a/.changelog/19354.txt b/.changelog/19354.txt new file mode 100644 index 000000000000..41e6864c4066 --- /dev/null +++ b/.changelog/19354.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_timestreamwrite_table +``` \ No newline at end of file diff --git a/.changelog/19359.txt b/.changelog/19359.txt new file mode 100644 index 000000000000..d79ead839742 --- /dev/null +++ b/.changelog/19359.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appconfig_deployment_strategy +``` diff --git a/.changelog/19361.txt b/.changelog/19361.txt new file mode 100644 index 000000000000..2233e399f0d6 --- /dev/null +++ b/.changelog/19361.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_pinpoint_email_channel: `role_arn` argument is optional +``` \ No newline at end of file diff --git a/.changelog/19368.txt b/.changelog/19368.txt new file mode 100644 index 000000000000..3854ab3d09b0 --- /dev/null +++ b/.changelog/19368.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudtrail: Add `advanced_event_selector` argument +``` \ No newline at end of file diff --git a/.changelog/19369.txt b/.changelog/19369.txt new file mode 100644 index 000000000000..faeb8d8bdd8b --- /dev/null +++ b/.changelog/19369.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_service_action +``` \ No newline at end of file diff --git a/.changelog/19371.txt b/.changelog/19371.txt new file mode 100644 index 000000000000..5bd2fce2fac8 --- /dev/null +++ b/.changelog/19371.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_glue_catalog_database: Add `target_database` argument +``` \ No newline at end of file diff --git a/.changelog/19372.txt b/.changelog/19372.txt new file mode 100644 index 000000000000..13989b45bbc8 --- /dev/null +++ b/.changelog/19372.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_glue_catalog_table: Add `target_table` argument +``` \ No newline at end of file diff --git a/.changelog/19375.txt b/.changelog/19375.txt new file mode 100644 index 000000000000..57e097781921 --- /dev/null +++ b/.changelog/19375.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_connection: `connection_properties` are optional +``` \ No newline at end of file diff --git a/.changelog/19385.txt b/.changelog/19385.txt new file mode 100644 index 000000000000..e9fc8b54cefa --- /dev/null +++ b/.changelog/19385.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_servicecatalog_constraint +``` + +```release-note:new-resource +aws_servicecatalog_product_portfolio_association +``` \ No newline at end of file diff --git a/.changelog/19386.txt b/.changelog/19386.txt new file mode 100644 index 000000000000..5394b687c13a --- /dev/null +++ b/.changelog/19386.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lambda_function: Wait for successful completion of function code update +``` \ No newline at end of file diff --git a/.changelog/19389.txt b/.changelog/19389.txt new file mode 100644 index 000000000000..e03772359b0c --- /dev/null +++ b/.changelog/19389.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_organizations_delegated_administrator +``` + +```release-note:new-data-source +aws_organizations_delegated_administrators +``` + +```release-note:new-data-source +aws_organizations_delegated_services +``` \ No newline at end of file diff --git a/.changelog/19391.txt b/.changelog/19391.txt new file mode 100644 index 000000000000..d9484020e8ae --- /dev/null +++ b/.changelog/19391.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_default_tags +``` diff --git a/.changelog/19394.txt b/.changelog/19394.txt new file mode 100644 index 000000000000..6804cefb3ccd --- /dev/null +++ b/.changelog/19394.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ec2_managed_prefix_list_entry +``` \ No newline at end of file diff --git a/.changelog/19404.txt b/.changelog/19404.txt new file mode 100644 index 000000000000..d48647b4860e --- /dev/null +++ b/.changelog/19404.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +data-source/aws_msk_cluster: Add `bootstrap_brokers_sasl_iam` attribute +``` + +```release-note:enhancement +resource/aws_msk_cluster: Add `bootstrap_brokers_sasl_iam` attribute +``` + +```release-note:enhancement +resource/aws_msk_cluster: Add `iam` argument to `client_authentication.sasl` configuration block +``` \ No newline at end of file diff --git a/.changelog/19407.txt b/.changelog/19407.txt new file mode 100644 index 000000000000..47e0a8ea76c5 --- /dev/null +++ b/.changelog/19407.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl: Support `scope_down_statement` on `managed_rule_group_statement` +``` \ No newline at end of file diff --git a/.changelog/19415.txt b/.changelog/19415.txt new file mode 100644 index 000000000000..03052da1f90a --- /dev/null +++ b/.changelog/19415.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `custom_request_handling` to `allow` and `count` default action and rule actions. +``` + +```release-note:enhancement +resource/aws_wafv2_web_acl: Add `custom_response` to `block` default action and rule actions. +``` + +```release-note:enhancement +resource/aws_wafv2_rule_group: Add `custom_request_handling` to `allow` and `count` rule actions. +``` + +```release-note:enhancement +resource/aws_wafv2_rule_group: Add `custom_response` to `block` rule actions. +``` diff --git a/.changelog/19423.txt b/.changelog/19423.txt new file mode 100644 index 000000000000..0731a70f0acf --- /dev/null +++ b/.changelog/19423.txt @@ -0,0 +1,3 @@ +```release-note:note +resource/aws_appmesh_virtual_node: Number of default backends per virtual node increased up to 50 +``` diff --git a/.changelog/19425.txt b/.changelog/19425.txt new file mode 100644 index 000000000000..7133279ad1f4 --- /dev/null +++ b/.changelog/19425.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Add `self_managed_event_source` and `source_access_configuration` arguments to support self-managed Apache Kafka event sources +``` + +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Add `tumbling_window_in_seconds` argument to support AWS Lambda streaming analytics calculations +``` + +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Add `function_response_types` argument to support AWS Lambda checkpointing +``` + +```release-note:enhancement +resource/aws_lambda_event_source_mapping: Add `queues` argument to support Amazon MQ for Apache ActiveMQ event sources +``` diff --git a/.changelog/19426.txt b/.changelog/19426.txt new file mode 100644 index 000000000000..912bdafd6b0f --- /dev/null +++ b/.changelog/19426.txt @@ -0,0 +1,23 @@ +```release-note:enhancement +resource/aws_route_table: Add retries when creating, deleting and replacing routes +``` + +```release-note:enhancement +resource/aws_route: Add retries when creating, deleting and replacing routes +``` + +```release-note:enhancement +resource/aws_default_route_table: Add retries when creating, deleting and replacing routes +``` + +```release-note:enhancement +resource/aws_default_route_table: Add retries when creating, deleting and replacing routes +``` + +```release-note:enhancement +resource/aws_main_route_table_association: Wait for association to reach the required state +``` + +```release-note:enhancement +resource/aws_route_table_association: Wait for association to reach the required state +``` \ No newline at end of file diff --git a/.changelog/19430.txt b/.changelog/19430.txt new file mode 100644 index 000000000000..2ceb245242d4 --- /dev/null +++ b/.changelog/19430.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_networkfirewall_rule_group: Correctly update resource on `rules` change +``` \ No newline at end of file diff --git a/.changelog/19432.txt b/.changelog/19432.txt new file mode 100644 index 000000000000..446155c940af --- /dev/null +++ b/.changelog/19432.txt @@ -0,0 +1,15 @@ +```release-note:new-resource +aws_apprunner_auto_scaling_configuration_version +``` + +```release-note:new-resource +aws_apprunner_connection +``` + +```release-note:new-resource +aws_apprunner_custom_domain_association +``` + +```release-note:new-resource +aws_apprunner_service +``` diff --git a/.changelog/19447.txt b/.changelog/19447.txt new file mode 100644 index 000000000000..bb9a46e08b10 --- /dev/null +++ b/.changelog/19447.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_policy: Add `PredictiveScaling` `policy_type` and `predictive_scaling_configuration` argument +``` diff --git a/.changelog/19448.txt b/.changelog/19448.txt new file mode 100644 index 000000000000..6b7ca1507972 --- /dev/null +++ b/.changelog/19448.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_tag_option_resource_association +``` \ No newline at end of file diff --git a/.changelog/19452.txt b/.changelog/19452.txt new file mode 100644 index 000000000000..970016a5d0e0 --- /dev/null +++ b/.changelog/19452.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_budget_resource_association +``` \ No newline at end of file diff --git a/.changelog/19454.txt b/.changelog/19454.txt new file mode 100644 index 000000000000..6ff5b428703b --- /dev/null +++ b/.changelog/19454.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_eks_addon: Use `service_account_role_arn`, if set, on updates +``` \ No newline at end of file diff --git a/.changelog/19459.txt b/.changelog/19459.txt new file mode 100644 index 000000000000..97b0e518160a --- /dev/null +++ b/.changelog/19459.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_provisioned_product +``` \ No newline at end of file diff --git a/.changelog/19470.txt b/.changelog/19470.txt new file mode 100644 index 000000000000..6d33e3739695 --- /dev/null +++ b/.changelog/19470.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_servicecatalog_principal_portfolio_association +``` \ No newline at end of file diff --git a/.changelog/19471.txt b/.changelog/19471.txt new file mode 100644 index 000000000000..820b41723fcc --- /dev/null +++ b/.changelog/19471.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_apprunner_service: Correctly configure `authentication_configuration`, `code_configuration`, and `image_configuration` nested arguments in API requests +``` diff --git a/.changelog/19482.txt b/.changelog/19482.txt new file mode 100644 index 000000000000..f43aebc58847 --- /dev/null +++ b/.changelog/19482.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_node_group: Add `taint` argument +``` \ No newline at end of file diff --git a/.changelog/19483.txt b/.changelog/19483.txt new file mode 100644 index 000000000000..be4cf576068e --- /dev/null +++ b/.changelog/19483.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_apprunner_service: Handle asynchronous IAM eventual consistency error on creation +``` + +```release-note:bug +resource/aws_apprunner_service: Suppress `instance_configuration` `cpu` and `memory` differences +``` diff --git a/.changelog/19492.txt b/.changelog/19492.txt new file mode 100644 index 000000000000..5b3b001b89d0 --- /dev/null +++ b/.changelog/19492.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_launch_template: Add `interface_type` to `network_interfaces` attribute +``` \ No newline at end of file diff --git a/.changelog/19496.txt b/.changelog/19496.txt new file mode 100644 index 000000000000..5bbf54b65d51 --- /dev/null +++ b/.changelog/19496.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_listener_rule: Allow blank string for `action.redirect.query` nested argument +``` \ No newline at end of file diff --git a/.changelog/19497.txt b/.changelog/19497.txt new file mode 100644 index 000000000000..4ccd259ca056 --- /dev/null +++ b/.changelog/19497.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_elasticsearch_domain_saml_options +``` diff --git a/.changelog/19499.txt b/.changelog/19499.txt new file mode 100644 index 000000000000..db1e6936e0ab --- /dev/null +++ b/.changelog/19499.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_servicecatalog_constraint +``` diff --git a/.changelog/19500.txt b/.changelog/19500.txt new file mode 100644 index 000000000000..46d3a1e003a5 --- /dev/null +++ b/.changelog/19500.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source + aws_servicecatalog_portfolio + ``` \ No newline at end of file diff --git a/.changelog/19502.txt b/.changelog/19502.txt new file mode 100644 index 000000000000..2176316b46d6 --- /dev/null +++ b/.changelog/19502.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_mq_broker: Correct type for `logs.audit` attribute +``` \ No newline at end of file diff --git a/.changelog/19503.txt b/.changelog/19503.txt new file mode 100644 index 000000000000..184824e42dec --- /dev/null +++ b/.changelog/19503.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source + aws_servicecatalog_product + ``` diff --git a/.changelog/19504.txt b/.changelog/19504.txt new file mode 100644 index 000000000000..268780f68da9 --- /dev/null +++ b/.changelog/19504.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector +``` \ No newline at end of file diff --git a/.changelog/19505.txt b/.changelog/19505.txt new file mode 100644 index 000000000000..89dbec57814b --- /dev/null +++ b/.changelog/19505.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_batch_job_definition: Don't crash when setting `timeout.attempt_duration_seconds` to `null` +``` \ No newline at end of file diff --git a/.changelog/19515.txt b/.changelog/19515.txt new file mode 100644 index 000000000000..f41d46513e5b --- /dev/null +++ b/.changelog/19515.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_synthetics_canary: Change minimum `timeout_in_seconds` in `run_config` from `60` to `3` +``` diff --git a/.changelog/19517.txt b/.changelog/19517.txt new file mode 100644 index 000000000000..21ad4c42fe8b --- /dev/null +++ b/.changelog/19517.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ec2_managed_prefix_list: Fix crash with multiple description-only updates +``` \ No newline at end of file diff --git a/.changelog/19528.txt b/.changelog/19528.txt new file mode 100644 index 000000000000..95babeb7b164 --- /dev/null +++ b/.changelog/19528.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_sns_topic: Add `firehose_success_feedback_role_arn`, `firehose_success_feedback_sample_rate` and `firehose_failure_feedback_role_arn` arguments. +``` + +```release-note:enhancement +resource/aws_sns_topic: Add plan time validation for `application_success_feedback_role_arn`, `application_failure_feedback_role_arn`, `http_success_feedback_role_arn`, `http_failure_feedback_role_arn`, `lambda_success_feedback_role_arn`, `lambda_failure_feedback_role_arn`, `sqs_success_feedback_role_arn`, `sqs_failure_feedback_role_arn`. +``` + +```release-note:enhancement +resource/aws_sns_topic: Add `owner` attribute. +``` \ No newline at end of file diff --git a/.changelog/19532.txt b/.changelog/19532.txt new file mode 100644 index 000000000000..7a0bf7efc142 --- /dev/null +++ b/.changelog/19532.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_role: Add plan time validation for `path`, `permissions_boundary`, `managed_policy_arns`. +``` \ No newline at end of file diff --git a/.changelog/19535.txt b/.changelog/19535.txt new file mode 100644 index 000000000000..50c62e684b85 --- /dev/null +++ b/.changelog/19535.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ec2_capacity_reservation: Add `outpost_arn` argument +``` diff --git a/.changelog/19551.txt b/.changelog/19551.txt new file mode 100644 index 000000000000..622165ffd204 --- /dev/null +++ b/.changelog/19551.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_elasticache_parameter_group: Add `tags` argument and `arn` and `tags_all` attributes +``` \ No newline at end of file diff --git a/.changelog/19554.txt b/.changelog/19554.txt new file mode 100644 index 000000000000..9696311995f0 --- /dev/null +++ b/.changelog/19554.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_budgets_budget_action +``` diff --git a/.changelog/19555.txt b/.changelog/19555.txt new file mode 100644 index 000000000000..161bc3f9034b --- /dev/null +++ b/.changelog/19555.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_target: Fix `ecs_target.launch_type` not allowing empty string values. +``` diff --git a/.changelog/19557.txt b/.changelog/19557.txt new file mode 100644 index 000000000000..f5d4696408b3 --- /dev/null +++ b/.changelog/19557.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_service: Add support for ECS Anywhere with the `launch_type` `EXTERNAL` +``` diff --git a/.changelog/19559.txt b/.changelog/19559.txt new file mode 100644 index 000000000000..92c8b1a21383 --- /dev/null +++ b/.changelog/19559.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudtrail: Add `AWS::DynamoDB::Table` as an option for `event_selector`.`data_resource`.`type` +``` \ No newline at end of file diff --git a/.changelog/19568.txt b/.changelog/19568.txt new file mode 100644 index 000000000000..b369c83530bd --- /dev/null +++ b/.changelog/19568.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_lustre_filesystem: Allow updating `storage_capacity`. +``` \ No newline at end of file diff --git a/.changelog/19571.txt b/.changelog/19571.txt new file mode 100644 index 000000000000..b2ba1cf5694e --- /dev/null +++ b/.changelog/19571.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_metric_alarm: Add plan time validation to `metric_query.metric.stat`. +``` \ No newline at end of file diff --git a/.changelog/19572.txt b/.changelog/19572.txt new file mode 100644 index 000000000000..9a3b532f9318 --- /dev/null +++ b/.changelog/19572.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_servicecatalog_launch_paths +``` diff --git a/.changelog/19574.txt b/.changelog/19574.txt new file mode 100644 index 000000000000..be8af655535f --- /dev/null +++ b/.changelog/19574.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_devicefarm_project: Add `default_job_timeout_minutes` and `tags` argument +``` + +```release-note:enhancement +resource/aws_devicefarm_project: Add plan time validation for `name` +``` \ No newline at end of file diff --git a/.changelog/19577.txt b/.changelog/19577.txt new file mode 100644 index 000000000000..11ca4ee70159 --- /dev/null +++ b/.changelog/19577.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_appmesh_mesh +``` diff --git a/.changelog/19578.txt b/.changelog/19578.txt new file mode 100644 index 000000000000..c02aad104ece --- /dev/null +++ b/.changelog/19578.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_acmpca_certificate_authority: Add `s3_object_acl` argument to `revocation_configuration.crl_configuration` configuration block +``` \ No newline at end of file diff --git a/.changelog/19579.txt b/.changelog/19579.txt new file mode 100644 index 000000000000..909b9e825e08 --- /dev/null +++ b/.changelog/19579.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_access_key: Add encrypted SES SMTP password +``` \ No newline at end of file diff --git a/.changelog/19594.txt b/.changelog/19594.txt new file mode 100644 index 000000000000..3c8b2b5ca13c --- /dev/null +++ b/.changelog/19594.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_api_destination: Reduce the maximum allowed value for the `invocation_rate_limit_per_second` argument to `300` +``` diff --git a/.changelog/19606.txt b/.changelog/19606.txt new file mode 100644 index 000000000000..a41995893b13 --- /dev/null +++ b/.changelog/19606.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iam_access_key: Fix status not defaulting to Active +``` diff --git a/.changelog/19615.txt b/.changelog/19615.txt new file mode 100644 index 000000000000..109278c53372 --- /dev/null +++ b/.changelog/19615.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_cluster: Fix provider-level `default_tags` support for resource +``` diff --git a/.changelog/19625.txt b/.changelog/19625.txt new file mode 100644 index 000000000000..4c3e1cc1ddac --- /dev/null +++ b/.changelog/19625.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_log_metric_filter: Add `dimensions` argument to `metric_transformation` configuration block +``` \ No newline at end of file diff --git a/.changelog/19632.txt b/.changelog/19632.txt new file mode 100644 index 000000000000..12cd701def3b --- /dev/null +++ b/.changelog/19632.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_launch_configuration: Add `throughput` argument to `ebs_block_device` and `root_block_device` configuration blocks to support GP3 volumes +``` + +```release-note:enhancement +data-source/aws_launch_configuration: Add `throughput` attribute to `ebs_block_device` and `root_block_device` configuration blocks to support GP3 volumes +``` \ No newline at end of file diff --git a/.changelog/19639.txt b/.changelog/19639.txt new file mode 100644 index 000000000000..607d7deb3671 --- /dev/null +++ b/.changelog/19639.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_sqs_queue: Add `deduplication_scope` and `fifo_throughput_limit` arguments +``` + +```release-note:enhancement +resource/aws_sqs_queue: Add `url` attribute +``` + +```release-note:bug +resource/aws_sqs_queue: Allow `visibility_timeout_seconds` to be `0` when creating queue +``` + +```release-note:bug +resource/aws_sqs_queue: Ensure that queue attributes propagate completely during Create and Update +``` \ No newline at end of file diff --git a/.changelog/19647.txt b/.changelog/19647.txt new file mode 100644 index 000000000000..e063263e54a5 --- /dev/null +++ b/.changelog/19647.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_globalaccelerator_accelerator +``` \ No newline at end of file diff --git a/.changelog/19654.txt b/.changelog/19654.txt new file mode 100644 index 000000000000..3e8303887fbc --- /dev/null +++ b/.changelog/19654.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_api_destination: Fix crash on resource update +``` \ No newline at end of file diff --git a/.changelog/19656.txt b/.changelog/19656.txt new file mode 100644 index 000000000000..32399c4fe364 --- /dev/null +++ b/.changelog/19656.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_default_vpc_dhcp_options: Add `owner_id` argument. +``` \ No newline at end of file diff --git a/.changelog/19664.txt b/.changelog/19664.txt new file mode 100644 index 000000000000..96a024a5014b --- /dev/null +++ b/.changelog/19664.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_lustre_filesystem: Add `data_compression_type` argument. +``` \ No newline at end of file diff --git a/.changelog/19666.txt b/.changelog/19666.txt new file mode 100644 index 000000000000..c22c8c477975 --- /dev/null +++ b/.changelog/19666.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_batch_job_definition: Suppress differences for empty `linuxParameters.devices` and `linuxParameters.tmpfs` arrays in the `container_properties` argument +``` \ No newline at end of file diff --git a/.changelog/19668.txt b/.changelog/19668.txt new file mode 100644 index 000000000000..f1c4e66da493 --- /dev/null +++ b/.changelog/19668.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_metric_alarm: Allow extended statistics in the `stat` argument of the `metric` configuration block +``` \ No newline at end of file diff --git a/.changelog/19670.txt b/.changelog/19670.txt new file mode 100644 index 000000000000..aaa44144676e --- /dev/null +++ b/.changelog/19670.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_ecs_task_definition: Add plan time validation for `family` and `requires_compatibilities`. +``` + +```release-note:enhancement +resource/aws_ecs_task_definition: Add support for `fsx_windows_file_server_volume_configuration`. +``` \ No newline at end of file diff --git a/.changelog/19677.txt b/.changelog/19677.txt new file mode 100644 index 000000000000..04feae802aff --- /dev/null +++ b/.changelog/19677.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iot_topic_rule: Allow tags containing `@` character +``` diff --git a/.changelog/19681.txt b/.changelog/19681.txt new file mode 100644 index 000000000000..20b49f868039 --- /dev/null +++ b/.changelog/19681.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_acmpca_certificate_authority: Fix `error setting tags` +``` \ No newline at end of file diff --git a/.changelog/19691.txt b/.changelog/19691.txt new file mode 100644 index 000000000000..832d0c032589 --- /dev/null +++ b/.changelog/19691.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `domain` argument. +``` + +```release-note:enhancement +data-source/aws_transfer_server: Add `domain` attribute. +``` \ No newline at end of file diff --git a/.changelog/19693.txt b/.changelog/19693.txt new file mode 100644 index 000000000000..4a1fe974b82c --- /dev/null +++ b/.changelog/19693.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_transfer_user: Add `posix_profile` argument. +``` \ No newline at end of file diff --git a/.changelog/19694.txt b/.changelog/19694.txt new file mode 100644 index 000000000000..1bc3a085782d --- /dev/null +++ b/.changelog/19694.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_task_definition: Add support for `ephemeral_storage`. +``` \ No newline at end of file diff --git a/.changelog/19702.txt b/.changelog/19702.txt new file mode 100644 index 000000000000..dedfe075e980 --- /dev/null +++ b/.changelog/19702.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_cognito_user_pool_client: Add plan time validation for `id_token_validity` and `access_token_validity`. +``` + +```release-note:bug +resource/aws_cognito_user_pool_client: Fix plan time validation for `refresh_token_validity` +``` \ No newline at end of file diff --git a/.changelog/19703.txt b/.changelog/19703.txt new file mode 100644 index 000000000000..99a61761500d --- /dev/null +++ b/.changelog/19703.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_target: Increase the maximum allowed value for the `input_transformer` `input_paths` argument to 100 +``` \ No newline at end of file diff --git a/.changelog/19704.txt b/.changelog/19704.txt new file mode 100644 index 000000000000..1f47903d762c --- /dev/null +++ b/.changelog/19704.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cognito_user_pool: Suppress diff for empty `account_recovery_setting`. +``` \ No newline at end of file diff --git a/.changelog/19705.txt b/.changelog/19705.txt new file mode 100644 index 000000000000..2471579cb3df --- /dev/null +++ b/.changelog/19705.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_cur_report_definition: Add `arn` attribute. +``` + +```release-note:enhancement +resource/aws_cur_report_definition: Support updating definition. +``` + +```release-note:enhancement +resource/aws_cur_report_definition: Add plan time validation for `report_name`. +``` \ No newline at end of file diff --git a/.changelog/19718.txt b/.changelog/19718.txt new file mode 100644 index 000000000000..10a50d191fa9 --- /dev/null +++ b/.changelog/19718.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ram_resource_share_accepter: Allow destroy even where AWS API provides no way to disassociate +``` diff --git a/.changelog/19722.txt b/.changelog/19722.txt new file mode 100644 index 000000000000..c92a5a36e587 --- /dev/null +++ b/.changelog/19722.txt @@ -0,0 +1,7 @@ +```release-note:bug +data-source/aws_servicequotas_service_quota: Correctly handle errors embedded in API struct +``` + +```release-note:bug +resource/aws_servicequotas_service_quota: Correctly handle errors embedded in API struct +``` diff --git a/.changelog/19741.txt b/.changelog/19741.txt new file mode 100644 index 000000000000..83d9fc756e95 --- /dev/null +++ b/.changelog/19741.txt @@ -0,0 +1,3 @@ +```release-note:note +resource/aws_dx_gateway_association_proposal: If an accepted Proposal reaches end-of-life and is removed by AWS do not recreate the resource, instead refreshing Terraform state from the resource's Direct Connect Gateway ID and Associated Gateway ID. +``` \ No newline at end of file diff --git a/.changelog/19742.txt b/.changelog/19742.txt new file mode 100644 index 000000000000..4f5d5b2f91a0 --- /dev/null +++ b/.changelog/19742.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_catalog_table: Fix updating `schema_reference` when columns are present. +``` \ No newline at end of file diff --git a/.changelog/19743.txt b/.changelog/19743.txt new file mode 100644 index 000000000000..0a392951498f --- /dev/null +++ b/.changelog/19743.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_catalog_database: Set `location_uri` as compute to prevent drift when `target_table` has `location_uri` set. +``` \ No newline at end of file diff --git a/.changelog/19749.txt b/.changelog/19749.txt new file mode 100644 index 000000000000..6eb11135d139 --- /dev/null +++ b/.changelog/19749.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_backup_vault_policy: Correctly handle reading policy of deleted vault +``` \ No newline at end of file diff --git a/.changelog/19753.txt b/.changelog/19753.txt new file mode 100644 index 000000000000..50c2a41b0ce8 --- /dev/null +++ b/.changelog/19753.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_datasync_location_smb: Add support for updating. +``` + +```release-note:enhancement +resource/aws_datasync_location_smb: Add plan time validation for `domain`, `agent_arns`, `password`, `server_hostname`, `subdirectory`, and `user`. +``` \ No newline at end of file diff --git a/.changelog/19758.txt b/.changelog/19758.txt new file mode 100644 index 000000000000..783b2aa12a41 --- /dev/null +++ b/.changelog/19758.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_nat_gateway: Add `connectivity_type` attribute +``` + +```release-note:enhancement +resource/aws_nat_gateway: Add `connectivity_type` argument +``` diff --git a/.changelog/19765.txt b/.changelog/19765.txt new file mode 100644 index 000000000000..1a70a8036785 --- /dev/null +++ b/.changelog/19765.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_athena_database: Read the database name from the `AwsDataCatalog` +``` \ No newline at end of file diff --git a/.changelog/19767.txt b/.changelog/19767.txt new file mode 100644 index 000000000000..512a178dfcb6 --- /dev/null +++ b/.changelog/19767.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_datasync_location_nfs: Add support for updating. +``` + +```release-note:enhancement +resource/aws_datasync_location_nfs: Add `mount_options` argument. +``` + +```release-note:enhancement +resource/aws_datasync_location_nfs: Add plan time validation for `on_prem_config.agent_arns`, `server_hostname`, and `subdirectory`. +``` \ No newline at end of file diff --git a/.changelog/19772.txt b/.changelog/19772.txt new file mode 100644 index 000000000000..922b6c78b31e --- /dev/null +++ b/.changelog/19772.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_redshift_parameter_group: Make Redshift parameters case sensitive. +``` diff --git a/.changelog/19774.txt b/.changelog/19774.txt new file mode 100644 index 000000000000..a4268e1737ad --- /dev/null +++ b/.changelog/19774.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_appmesh_virtual_service +``` diff --git a/.changelog/19785.txt b/.changelog/19785.txt new file mode 100644 index 000000000000..1cd991f23063 --- /dev/null +++ b/.changelog/19785.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_ecs_cluster: Add support for `configuration`. +``` + +```release-note:enhancement +resource/aws_ecs_cluster: Add plan time validation for `name`. +``` \ No newline at end of file diff --git a/.changelog/19804.txt b/.changelog/19804.txt new file mode 100644 index 000000000000..1cddbbd9d6ea --- /dev/null +++ b/.changelog/19804.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_log_metric_filter: Add support for `unit` in the `metric_transformation` block. +``` \ No newline at end of file diff --git a/.changelog/19810.txt b/.changelog/19810.txt new file mode 100644 index 000000000000..738092c4cd3e --- /dev/null +++ b/.changelog/19810.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_node_group: Allow minimum value of `0` for `desired_size` and `min_size` in the `scaling_config` configuration block +``` \ No newline at end of file diff --git a/.changelog/19813.txt b/.changelog/19813.txt new file mode 100644 index 000000000000..7d1458ae4830 --- /dev/null +++ b/.changelog/19813.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_servicecatalog_portfolio_constraints +``` diff --git a/.changelog/19817.txt b/.changelog/19817.txt new file mode 100644 index 000000000000..4ef139133bc3 --- /dev/null +++ b/.changelog/19817.txt @@ -0,0 +1,15 @@ +```release-note:bug +resource/aws_lakeformation_permissions: Fix bug preventing updates (inconsistent result) +``` + +```release-note:bug +resource/aws_lakeformation_permissions: Fix bug where resource is not properly removed from state +``` + +```release-note:bug +resource/aws_lakeformation_permissions: Fix diffs resulting only from order of column names and exclude column names +``` + +```release-note:bug +data-source/aws_lakeformation_permissions: Fix diffs resulting from order of column names and exclude column names +``` \ No newline at end of file diff --git a/.changelog/19819.txt b/.changelog/19819.txt new file mode 100644 index 000000000000..dd629b76c046 --- /dev/null +++ b/.changelog/19819.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cognito_identity_provider: Fix updating `idp_identifiers` crash. +``` \ No newline at end of file diff --git a/.changelog/19820.txt b/.changelog/19820.txt new file mode 100644 index 000000000000..2f08ee6f8a29 --- /dev/null +++ b/.changelog/19820.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_directory_service_directory: Check VpcSettings and ConnectSettings for nil values +``` diff --git a/.changelog/19827.txt b/.changelog/19827.txt new file mode 100644 index 000000000000..ba9eb20c6f8b --- /dev/null +++ b/.changelog/19827.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_glue_trigger: Fix default timeouts for Create and Delete operations +``` \ No newline at end of file diff --git a/.changelog/19829.txt b/.changelog/19829.txt new file mode 100644 index 000000000000..e70a902e30aa --- /dev/null +++ b/.changelog/19829.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_autoscaling_schedule: Add `time_zone` argument +``` diff --git a/.changelog/19831.txt b/.changelog/19831.txt new file mode 100644 index 000000000000..9b745f3e76a6 --- /dev/null +++ b/.changelog/19831.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lambda_event_source_mapping: Enhance handling of IAM eventual consistency errors during create +``` \ No newline at end of file diff --git a/.changelog/19834.txt b/.changelog/19834.txt new file mode 100644 index 000000000000..1ed731803c54 --- /dev/null +++ b/.changelog/19834.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_sqs_queue: Correctly handle the default `kms_data_key_reuse_period_seconds` value of `300` for unencrypted queues +``` \ No newline at end of file diff --git a/.changelog/19854.txt b/.changelog/19854.txt new file mode 100644 index 000000000000..6e8f01e19ca9 --- /dev/null +++ b/.changelog/19854.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_backup_vault_policy: Correctly handle deleting policy of deleted vault +``` \ No newline at end of file diff --git a/.changelog/19859.txt b/.changelog/19859.txt new file mode 100644 index 000000000000..92976607dfcf --- /dev/null +++ b/.changelog/19859.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_datasync_location_s3: Correctly parse S3 on Outposts location URI +``` \ No newline at end of file diff --git a/.changelog/19898.txt b/.changelog/19898.txt new file mode 100644 index 000000000000..698d9aaaebfb --- /dev/null +++ b/.changelog/19898.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_neptune_cluster_endpoint +``` diff --git a/.changelog/19899.txt b/.changelog/19899.txt new file mode 100644 index 000000000000..23fadb9c9891 --- /dev/null +++ b/.changelog/19899.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_neptune_cluster: Add `copy_snapshot_to_tags` argument +``` \ No newline at end of file diff --git a/.changelog/19944.txt b/.changelog/19944.txt new file mode 100644 index 000000000000..c75c04dbb420 --- /dev/null +++ b/.changelog/19944.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_kms_public_key: Correctly base64 encode `public_key` value +``` \ No newline at end of file diff --git a/.changelog/19946.txt b/.changelog/19946.txt new file mode 100644 index 000000000000..c84317ee59a3 --- /dev/null +++ b/.changelog/19946.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cloudwatch_event_target: Don't crash if `sqs_target` configuration block is empty. +``` \ No newline at end of file diff --git a/.changelog/19954.txt b/.changelog/19954.txt new file mode 100644 index 000000000000..1823e0216e5b --- /dev/null +++ b/.changelog/19954.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_guardduty_detector: Add `datasources` argument +``` \ No newline at end of file diff --git a/.changelog/19957.txt b/.changelog/19957.txt new file mode 100644 index 000000000000..e4b1f3b3064e --- /dev/null +++ b/.changelog/19957.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_iam_session_context +``` \ No newline at end of file diff --git a/.changelog/19967.txt b/.changelog/19967.txt new file mode 100644 index 000000000000..98b176d6c44e --- /dev/null +++ b/.changelog/19967.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_kms_key: Add plan time validation to `description`. +``` diff --git a/.changelog/19970.txt b/.changelog/19970.txt new file mode 100644 index 000000000000..ceff50805b97 --- /dev/null +++ b/.changelog/19970.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_windows_file_system: Add `audit_log_configuration` argument. +``` diff --git a/.changelog/19975.txt b/.changelog/19975.txt new file mode 100644 index 000000000000..af62320796df --- /dev/null +++ b/.changelog/19975.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_target: Add `enable_ecs_managed_tags`, `enable_execute_command`, `placement_constraints`, `propagate_tags`, and `tags` arguments to `ecs_target` block. +``` \ No newline at end of file diff --git a/.changelog/19986.txt b/.changelog/19986.txt new file mode 100644 index 000000000000..0638db645e14 --- /dev/null +++ b/.changelog/19986.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_eks_cluster: Don't associate an `encryption_config` if there's already one +``` \ No newline at end of file diff --git a/.changelog/19994.txt b/.changelog/19994.txt new file mode 100644 index 000000000000..8aa3871bb24c --- /dev/null +++ b/.changelog/19994.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_mwaa_environment: Changes to the `kms_key` argument force resource recreation +``` \ No newline at end of file diff --git a/.changelog/20009.txt b/.changelog/20009.txt new file mode 100644 index 000000000000..2b162e60ddd4 --- /dev/null +++ b/.changelog/20009.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_autoscaling_group_tag +``` diff --git a/.changelog/20031.txt b/.changelog/20031.txt new file mode 100644 index 000000000000..833dab213509 --- /dev/null +++ b/.changelog/20031.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_cognito_user_pool_client: Retry on `ConcurrentModificationException` +``` + +```release-note:bug +resource/aws_cognito_user_pool_client: Allow the `default_redirect_uri` argument value to be an empty string +``` + +```release-note:enhancement +resource/aws_cognito_user_pool_client: Add the `enable_token_revocation` argument to support targeted sign out +``` \ No newline at end of file diff --git a/.changelog/20054.txt b/.changelog/20054.txt new file mode 100644 index 000000000000..158d54ba1be1 --- /dev/null +++ b/.changelog/20054.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_windows_file_system: Add `aliases` argument +``` diff --git a/.changelog/20058.txt b/.changelog/20058.txt new file mode 100644 index 000000000000..af5af0cf701d --- /dev/null +++ b/.changelog/20058.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sagemaker_device_fleet +``` \ No newline at end of file diff --git a/.changelog/20065.txt b/.changelog/20065.txt new file mode 100644 index 000000000000..33b6be09f6bd --- /dev/null +++ b/.changelog/20065.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_sagemaker_workforce +``` + +```release-note:enhancement +resource/aws_cognito_user_pool_client: Set `callback_urls` and `logout_urls` as computed. +``` \ No newline at end of file diff --git a/.changelog/20066.txt b/.changelog/20066.txt new file mode 100644 index 000000000000..675a3ca28791 --- /dev/null +++ b/.changelog/20066.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_model: Add `inference_execution_config`. +``` diff --git a/.changelog/20082.txt b/.changelog/20082.txt new file mode 100644 index 000000000000..ad38d52e211b --- /dev/null +++ b/.changelog/20082.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_storagegateway_file_system_association +``` + +```release-note:enhancement +resource/aws_storagegateway_gateway: Add new option for gateway_type, `FILE_FSX_SMB`, to be used with `aws_storagegateway_file_system_association` +``` diff --git a/.changelog/20108.txt b/.changelog/20108.txt new file mode 100644 index 000000000000..5edc7273ce89 --- /dev/null +++ b/.changelog/20108.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_lakeformation_permissions: Fix various problems with permissions including select-only +``` + +```release-note:bug +data-source/aws_lakeformation_permissions: Fix various problems with permissions including select-only +``` \ No newline at end of file diff --git a/.changelog/20111.txt b/.changelog/20111.txt new file mode 100644 index 000000000000..5768419145be --- /dev/null +++ b/.changelog/20111.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_replication_group: Cannot set `cluster_mode.replicas_per_node_group` when member of Global Replication Group +``` diff --git a/.changelog/20122.txt b/.changelog/20122.txt new file mode 100644 index 000000000000..f01f4efcce05 --- /dev/null +++ b/.changelog/20122.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sagemaker_workteam +``` \ No newline at end of file diff --git a/.changelog/20137.txt b/.changelog/20137.txt new file mode 100644 index 000000000000..fc1580fcd832 --- /dev/null +++ b/.changelog/20137.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_node_group: Add `update_config` argument to support parallel node upgrades +``` \ No newline at end of file diff --git a/.changelog/20172.txt b/.changelog/20172.txt new file mode 100644 index 000000000000..912d55d588fb --- /dev/null +++ b/.changelog/20172.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appconfig_deployment +``` diff --git a/.changelog/20203.txt b/.changelog/20203.txt new file mode 100644 index 000000000000..2875cfb524e9 --- /dev/null +++ b/.changelog/20203.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_glue_crawler: Add `sample_size` argument in `s3_target` block. +``` \ No newline at end of file diff --git a/.changelog/20207.txt b/.changelog/20207.txt new file mode 100644 index 000000000000..868f0d7e4061 --- /dev/null +++ b/.changelog/20207.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_instance: Use engine_version and engine_version_actual to set and track engine versions +``` diff --git a/.changelog/20211.txt b/.changelog/20211.txt new file mode 100644 index 000000000000..009096dfa1c6 --- /dev/null +++ b/.changelog/20211.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_rds_cluster: Use engine_version and engine_version_actual to set and track engine versions +``` + +```release-note:enhancement +resource/aws_rds_cluster_instance: Use engine_version and engine_version_actual to set and track engine versions +``` diff --git a/.changelog/20229.txt b/.changelog/20229.txt new file mode 100644 index 000000000000..36326020b2bf --- /dev/null +++ b/.changelog/20229.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_lambda_event_source_mapping: Ignore `InvalidParameterValueException` error caused by IAM propagation when creating Lambda event source mapping with Kinesis stream source +``` diff --git a/.changelog/20232.txt b/.changelog/20232.txt new file mode 100644 index 000000000000..8edd78c71eec --- /dev/null +++ b/.changelog/20232.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_acm_certificate: Add status attribute +``` diff --git a/.changelog/20234.txt b/.changelog/20234.txt new file mode 100644 index 000000000000..b7e2bf601afb --- /dev/null +++ b/.changelog/20234.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_storagegateway_smb_file_share: Add `bucket_region`, `oplocks_enabled` and `vpc_endpoint_dns_name` arguments +``` \ No newline at end of file diff --git a/.changelog/20254.txt b/.changelog/20254.txt new file mode 100644 index 000000000000..dfa9935d1ef3 --- /dev/null +++ b/.changelog/20254.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_servicecatalog_provisioned_product: Increase timeouts to align with CloudFormation (30 min.) +``` \ No newline at end of file diff --git a/.changelog/20256.txt b/.changelog/20256.txt new file mode 100644 index 000000000000..96dd9ca5e474 --- /dev/null +++ b/.changelog/20256.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_event_target: Add support for Redshift event target. +``` diff --git a/.changelog/20265.txt b/.changelog/20265.txt new file mode 100644 index 000000000000..b4e859770930 --- /dev/null +++ b/.changelog/20265.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_route_table_association: Correctly handle `associated` as a pending state when waiting for deletion of an association +``` \ No newline at end of file diff --git a/.changelog/20288.txt b/.changelog/20288.txt new file mode 100644 index 000000000000..272c8c2d4346 --- /dev/null +++ b/.changelog/20288.txt @@ -0,0 +1,7 @@ +```release-note:bug +aws/resource_aws_appconfig_deployment: Remove internal waiter after start of deployment +``` + +```release-note:enhancement +aws/resource_aws_appconfig_deployment: Add `state` attribute +``` \ No newline at end of file diff --git a/.changelog/20293.txt b/.changelog/20293.txt new file mode 100644 index 000000000000..0e6fce12bf4a --- /dev/null +++ b/.changelog/20293.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_secretsmanager_secret: Add replica support +``` \ No newline at end of file diff --git a/.changelog/20302.txt b/.changelog/20302.txt new file mode 100644 index 000000000000..52109340ebb4 --- /dev/null +++ b/.changelog/20302.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_qldb_ledger: Add `permissions_mode` support +``` + +```release-note:enhancement +data-source/aws_qldb_ledger: Add `permissions_mode` attribute +``` \ No newline at end of file diff --git a/.changelog/20312.txt b/.changelog/20312.txt new file mode 100644 index 000000000000..32e20365f6de --- /dev/null +++ b/.changelog/20312.txt @@ -0,0 +1,7 @@ +```release-note:bug +aws/resource_aws_cloudwatch_event_rule: Correctly handle ARN in `event_bus_name` argument +``` + +```release-note:bug +aws/resource_aws_cloudwatch_event_target: Correctly handle ARN in `event_bus_name` argument +``` \ No newline at end of file diff --git a/.changelog/20315.txt b/.changelog/20315.txt new file mode 100644 index 000000000000..f16e86092121 --- /dev/null +++ b/.changelog/20315.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_eks_clusters +``` \ No newline at end of file diff --git a/.changelog/20336.txt b/.changelog/20336.txt new file mode 100644 index 000000000000..f6695d5f38c6 --- /dev/null +++ b/.changelog/20336.txt @@ -0,0 +1,11 @@ +```release-note:bug +aws/resource_aws_lex_bot: Fix computed `version` for dependent resources +``` + +```release-note:bug +aws/resource_aws_lex_intent: Fix computed `version` for dependent resources +``` + +```release-note:bug +aws/resource_aws_lex_slot_type: Fix computed `version` for dependent resources +``` \ No newline at end of file diff --git a/.changelog/20339.txt b/.changelog/20339.txt new file mode 100644 index 000000000000..71ddbe49c983 --- /dev/null +++ b/.changelog/20339.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_elasticache_user: Correctly handle user modifications and deletion +``` \ No newline at end of file diff --git a/.changelog/20342.txt b/.changelog/20342.txt new file mode 100644 index 000000000000..d0b5ca5082c8 --- /dev/null +++ b/.changelog/20342.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `directory_id` argument to support Microsoft Active Directory (AD) authentication +``` + +```release-note:new-resource +aws_transfer_access +``` \ No newline at end of file diff --git a/.changelog/20357.txt b/.changelog/20357.txt new file mode 100644 index 000000000000..af9f5f145259 --- /dev/null +++ b/.changelog/20357.txt @@ -0,0 +1,7 @@ +```release-note:bug +aws/resource_aws_instance: Fix state refresh when launch template was deleted +``` + +```release-note:bug +aws/resource_aws_instance: Fix running `terraform plan` with with `skip_credentials_validation=true` +``` diff --git a/.changelog/20364.txt b/.changelog/20364.txt new file mode 100644 index 000000000000..d0f8a1aeac69 --- /dev/null +++ b/.changelog/20364.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_dx_connection: Add support for `100Gbps` `bandwidth` +``` + +```release-note:enhancement +resource/aws_dx_lag: Add support for `100Gbps` `connections_bandwidth` +``` \ No newline at end of file diff --git a/.changelog/20409.txt b/.changelog/20409.txt new file mode 100644 index 000000000000..19d549796d6a --- /dev/null +++ b/.changelog/20409.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_launch_template: Allow all supported resource types `tag_specifications.resource_type` +``` diff --git a/.changelog/20420.txt b/.changelog/20420.txt new file mode 100644 index 000000000000..e47ecd30f1fe --- /dev/null +++ b/.changelog/20420.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_appconfig_deployment: Include predefined strategies in plan time validation of `deployment_strategy_id` +``` \ No newline at end of file diff --git a/.changelog/20426.txt b/.changelog/20426.txt new file mode 100644 index 000000000000..d14edd58e902 --- /dev/null +++ b/.changelog/20426.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_amplify_branch: Correctly handle branch names that contain '/' +``` \ No newline at end of file diff --git a/.changelog/20437.txt b/.changelog/20437.txt new file mode 100644 index 000000000000..f7bd1b768c2f --- /dev/null +++ b/.changelog/20437.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_db_instance: Add `nchar_character_set_name` argument +``` \ No newline at end of file diff --git a/.changelog/20441.txt b/.changelog/20441.txt new file mode 100644 index 000000000000..9a2620eafe99 --- /dev/null +++ b/.changelog/20441.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_apigateway_vpc_link: Ensure deletion does not return an error when resource is not found +``` \ No newline at end of file diff --git a/.changelog/20457.txt b/.changelog/20457.txt new file mode 100644 index 000000000000..79d6cd11ddc4 --- /dev/null +++ b/.changelog/20457.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_athena_workgroup: Add `requester_pays_enabled` argument +``` diff --git a/.changelog/20462.txt b/.changelog/20462.txt new file mode 100644 index 000000000000..d6edf92aaf8e --- /dev/null +++ b/.changelog/20462.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_workspaces_directory: Add `workspace_access_properties.device_type_linux` attribute +``` + +```release-note:enhancement +resource/aws_workspaces_directory: Add `workspace_access_properties.device_type_linux` argument +``` \ No newline at end of file diff --git a/.changelog/20463.txt b/.changelog/20463.txt new file mode 100644 index 000000000000..bc4688bc6b0f --- /dev/null +++ b/.changelog/20463.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_synthetics_canary: Correctly report any resource creation errors +``` \ No newline at end of file diff --git a/.changelog/20464.txt b/.changelog/20464.txt new file mode 100644 index 000000000000..4fad6ed9d7b8 --- /dev/null +++ b/.changelog/20464.txt @@ -0,0 +1,3 @@ +```release-note:bug +aws/resource_aws_imagebuilder_infrastructure_configuration: Always set `terminate_instance_on_failure` on create and update +``` \ No newline at end of file diff --git a/.changelog/20467.txt b/.changelog/20467.txt new file mode 100644 index 000000000000..3e0809028a9a --- /dev/null +++ b/.changelog/20467.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iot_topic_rule: Enhance handling of IAM eventual consistency errors during create +``` \ No newline at end of file diff --git a/.changelog/20480.txt b/.changelog/20480.txt new file mode 100644 index 000000000000..e1a9bd76b3ee --- /dev/null +++ b/.changelog/20480.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_codebuild_webhook: Add support for `build_type` +``` diff --git a/.changelog/20491.txt b/.changelog/20491.txt new file mode 100644 index 000000000000..ed38fde47dfd --- /dev/null +++ b/.changelog/20491.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_shield_protection_group +``` \ No newline at end of file diff --git a/.changelog/20526.txt b/.changelog/20526.txt new file mode 100644 index 000000000000..d173a323eba7 --- /dev/null +++ b/.changelog/20526.txt @@ -0,0 +1,15 @@ +```release-note:new-resource +aws_route53recoveryreadiness_cell +``` + +```release-note:new-resource +aws_route53recoveryreadiness_recovery_group +``` + +```release-note:new-resource +aws_route53recoveryreadiness_resource_set +``` + +```release-note:new-resource +aws_route53recoveryreadiness_readiness_check +``` \ No newline at end of file diff --git a/.changelog/20530.txt b/.changelog/20530.txt new file mode 100644 index 000000000000..9a9a73d54b48 --- /dev/null +++ b/.changelog/20530.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_user: Correctly update `passwords` +``` \ No newline at end of file diff --git a/.changelog/20541.txt b/.changelog/20541.txt new file mode 100644 index 000000000000..3b7f20b1e135 --- /dev/null +++ b/.changelog/20541.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_cloudwatch_metric_alarm: Add support for `account_id` +``` diff --git a/.changelog/20543.txt b/.changelog/20543.txt new file mode 100755 index 000000000000..bb75acf566f0 --- /dev/null +++ b/.changelog/20543.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appstream_fleet +``` \ No newline at end of file diff --git a/.changelog/20547.txt b/.changelog/20547.txt new file mode 100644 index 000000000000..99c8c3aee3cb --- /dev/null +++ b/.changelog/20547.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appstream_stack +``` \ No newline at end of file diff --git a/.changelog/20555.txt b/.changelog/20555.txt new file mode 100644 index 000000000000..46e2cde573f9 --- /dev/null +++ b/.changelog/20555.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lambda_function: fix Osaka ap-northeast-3 lambda function creation, failing due to code signer service not available in the region. +``` \ No newline at end of file diff --git a/.changelog/20560.txt b/.changelog/20560.txt new file mode 100644 index 000000000000..d3b9d3590305 --- /dev/null +++ b/.changelog/20560.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_config_organization_conformance_pack: Add configurable timeouts +``` diff --git a/.changelog/20561.txt b/.changelog/20561.txt new file mode 100644 index 000000000000..c2941fd39560 --- /dev/null +++ b/.changelog/20561.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_eks_identity_provider_config: Increase Create and Delete timeouts to 40 minutes +``` \ No newline at end of file diff --git a/.changelog/20562.txt b/.changelog/20562.txt new file mode 100644 index 000000000000..14b46beeb52d --- /dev/null +++ b/.changelog/20562.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_eks_addon: Treat `DEGRADED` as a pending state during creation +``` \ No newline at end of file diff --git a/.changelog/20564.txt b/.changelog/20564.txt new file mode 100644 index 000000000000..b00cd3058f2a --- /dev/null +++ b/.changelog/20564.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_wafv2: Add missing values to `text_transformation` argument for `aws_wafv2_web_acl` and `aws_wafv2_rule_group` resources +``` diff --git a/.changelog/20565.txt b/.changelog/20565.txt new file mode 100644 index 000000000000..24f612da9669 --- /dev/null +++ b/.changelog/20565.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector_group +``` \ No newline at end of file diff --git a/.changelog/20568.txt b/.changelog/20568.txt new file mode 100644 index 000000000000..27e8dc024f72 --- /dev/null +++ b/.changelog/20568.txt @@ -0,0 +1,15 @@ +```release-note:new-resource +aws_route53recoverycontrolconfig_cluster +``` + +```release-note:new-resource +aws_route53recoverycontrolconfig_control_panel +``` + +```release-note:new-resource +aws_route53recoverycontrolconfig_routing_control +``` + +```release-note:new-resource +aws_route53recoverycontrolconfig_safety_rule +``` \ No newline at end of file diff --git a/.changelog/20569.txt b/.changelog/20569.txt new file mode 100644 index 000000000000..bb5efca36192 --- /dev/null +++ b/.changelog/20569.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_fsx_backup +``` \ No newline at end of file diff --git a/.changelog/20570.txt b/.changelog/20570.txt new file mode 100644 index 000000000000..a264a20d864e --- /dev/null +++ b/.changelog/20570.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sagemaker_human_task_ui +``` \ No newline at end of file diff --git a/.changelog/20575.txt b/.changelog/20575.txt new file mode 100644 index 000000000000..18f5f0ed95d0 --- /dev/null +++ b/.changelog/20575.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lambda_function: Fix `handler`, `runtime` attribute validation for `package_type` is `Zip` +``` diff --git a/.changelog/20579.txt b/.changelog/20579.txt new file mode 100644 index 000000000000..4c5848cf6d93 --- /dev/null +++ b/.changelog/20579.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_storagegateway_smb_file_share: Only set `oplocks_enabled` if a value is specified in configuration +``` \ No newline at end of file diff --git a/.changelog/20593.txt b/.changelog/20593.txt new file mode 100644 index 000000000000..5e799c6e8b50 --- /dev/null +++ b/.changelog/20593.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_lambda_function: Add support for `python3.9` `runtime` value +``` + +```release-note:enhancement +resource/aws_lambda_layer_version: Add support for `python3.9` `compatible_runtimes` value +``` \ No newline at end of file diff --git a/.changelog/20600.txt b/.changelog/20600.txt new file mode 100644 index 000000000000..e0e2b2d99958 --- /dev/null +++ b/.changelog/20600.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_config_delivery_channel: Add `s3_kms_key_arn` argument +``` \ No newline at end of file diff --git a/.changelog/20614.txt b/.changelog/20614.txt new file mode 100644 index 000000000000..dcb32a6a4b42 --- /dev/null +++ b/.changelog/20614.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_lustre_filesystem: Allow creating filesystem from backup using `backup_id`. +``` \ No newline at end of file diff --git a/.changelog/20615.txt b/.changelog/20615.txt new file mode 100644 index 000000000000..3e3a1e988040 --- /dev/null +++ b/.changelog/20615.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_msk_broker_nodes +``` \ No newline at end of file diff --git a/.changelog/20629.txt b/.changelog/20629.txt new file mode 100644 index 000000000000..b4bb94edfc7f --- /dev/null +++ b/.changelog/20629.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_elasticache_user: Mark `passwords` argument as sensitive. +``` + +```release-note:enhancement +data-source/aws_elasticache_user: Mark `passwords` attribute as sensitive. +``` \ No newline at end of file diff --git a/.changelog/20638.txt b/.changelog/20638.txt new file mode 100644 index 000000000000..3d68605fd40e --- /dev/null +++ b/.changelog/20638.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_msk_kafka_version +``` diff --git a/.changelog/20642.txt b/.changelog/20642.txt new file mode 100644 index 000000000000..49bc31f50a78 --- /dev/null +++ b/.changelog/20642.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_route53_resolver_rule: Fix lack of pagination when listing rules +``` \ No newline at end of file diff --git a/.changelog/20643.txt b/.changelog/20643.txt new file mode 100644 index 000000000000..3c5464eac167 --- /dev/null +++ b/.changelog/20643.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fsx_windows_filesystem: Allow creating filesystem from backup using `backup_id`. +``` \ No newline at end of file diff --git a/.changelog/20652.txt b/.changelog/20652.txt new file mode 100644 index 000000000000..260b63fb6c40 --- /dev/null +++ b/.changelog/20652.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_route53_zone: Add `arn` attribute +``` + +```release-note:enhancement +resource/aws_route53_zone: Add plan time validation for `comment` +``` + +```release-note:enhancement +data-source/aws_route53_zone: Add `arn` attribute +``` \ No newline at end of file diff --git a/.changelog/20653.txt b/.changelog/20653.txt new file mode 100644 index 000000000000..3e78d41a558d --- /dev/null +++ b/.changelog/20653.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_route53_health_check: Add plan time validation for `failure_threshold`, `ip_address`, `fqdn`, `port`, `resource_path`, `search_string`, `child_healthchecks`. +``` + +```release-note:enhancement +resource/aws_route53_health_check: Add `arn` attribute. +``` \ No newline at end of file diff --git a/.changelog/20658.txt b/.changelog/20658.txt new file mode 100644 index 000000000000..61c9defa7d79 --- /dev/null +++ b/.changelog/20658.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_bucket_inventory: Add missing values to `optional_fields` argument +``` \ No newline at end of file diff --git a/.changelog/20664.txt b/.changelog/20664.txt new file mode 100644 index 000000000000..a6c0251cff76 --- /dev/null +++ b/.changelog/20664.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_route53_delegation_set: Add `arn` attribute +``` + +```release-note:enhancement +resource/aws_route53_delegation_set: Add plan time validation for `reference_name` +``` + +```release-note:enhancement +data-source/aws_route53_delegation_set: Add `arn` attribute +``` + +```release-note:bug +resource/aws_route53_delegation_set: Properly remove from state when resource does not exist +``` diff --git a/.changelog/20666.txt b/.changelog/20666.txt new file mode 100644 index 000000000000..29b53e3dc1d0 --- /dev/null +++ b/.changelog/20666.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_route53_query_log: Add `arn` attribute. +``` + +```release-note:bug +resource/aws_route53_query_log: Properly remove from state when resource does not exist +``` \ No newline at end of file diff --git a/.changelog/20667.txt b/.changelog/20667.txt new file mode 100644 index 000000000000..b84ccdb1e70c --- /dev/null +++ b/.changelog/20667.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector_termination +``` \ No newline at end of file diff --git a/.changelog/20671.txt b/.changelog/20671.txt new file mode 100644 index 000000000000..bd6192c50534 --- /dev/null +++ b/.changelog/20671.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_codebuild_webhook: Only update `build_type` if a value is specified +``` \ No newline at end of file diff --git a/.changelog/20676.txt b/.changelog/20676.txt new file mode 100644 index 000000000000..1c1c78c698d1 --- /dev/null +++ b/.changelog/20676.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector_origination +``` \ No newline at end of file diff --git a/.changelog/20687.txt b/.changelog/20687.txt new file mode 100644 index 000000000000..4ac3b7d40657 --- /dev/null +++ b/.changelog/20687.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_quicksight_group_membership +``` \ No newline at end of file diff --git a/.changelog/20691.txt b/.changelog/20691.txt new file mode 100644 index 000000000000..46f2c4206e2e --- /dev/null +++ b/.changelog/20691.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_pinpoint_email_channel: When specifying the `configuration_set` parameter, use the name of the set instead of the ARN. +``` diff --git a/.changelog/20710.txt b/.changelog/20710.txt new file mode 100644 index 000000000000..aa15b354d6e2 --- /dev/null +++ b/.changelog/20710.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_quicksight_data_source +``` diff --git a/.changelog/20711.txt b/.changelog/20711.txt new file mode 100644 index 000000000000..e00658170193 --- /dev/null +++ b/.changelog/20711.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_notebook_instance: Add `platform_identifier` argument +``` \ No newline at end of file diff --git a/.changelog/20720.txt b/.changelog/20720.txt new file mode 100644 index 000000000000..0d7518d08af3 --- /dev/null +++ b/.changelog/20720.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ecs_cluster: Ensure that `setting` attribute is set consistently +``` \ No newline at end of file diff --git a/.changelog/20731.txt b/.changelog/20731.txt new file mode 100644 index 000000000000..7cb006c53aa7 --- /dev/null +++ b/.changelog/20731.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource_aws_route53_health_check: Add `RECOVERY_CONTROL` health check type and `routing_control_arn` argument +``` \ No newline at end of file diff --git a/.changelog/20775.txt b/.changelog/20775.txt new file mode 100644 index 000000000000..081f4cf298b5 --- /dev/null +++ b/.changelog/20775.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource_vpn_connection: Handle paginated response when reading Transit Gateway Attachments +``` \ No newline at end of file diff --git a/.changelog/20779.txt b/.changelog/20779.txt new file mode 100644 index 000000000000..8011b4201d89 --- /dev/null +++ b/.changelog/20779.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_db_instance: Fix updating `license_model`. +``` \ No newline at end of file diff --git a/.changelog/20785.txt b/.changelog/20785.txt new file mode 100644 index 000000000000..3b358d97b01c --- /dev/null +++ b/.changelog/20785.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_iam_role: Change `name_prefix` validation to a range of 1 to 38 characters +``` + +```release-note:enhancement +resource/aws_iam_role: `name_prefix` is now Computed +``` \ No newline at end of file diff --git a/.changelog/20791.txt b/.changelog/20791.txt new file mode 100644 index 000000000000..068015f6b519 --- /dev/null +++ b/.changelog/20791.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cognito_user_pool: Fix continual diff on `email_configuration.configuration_set` +``` \ No newline at end of file diff --git a/.changelog/20795.txt b/.changelog/20795.txt new file mode 100644 index 000000000000..4aaf1f5f1285 --- /dev/null +++ b/.changelog/20795.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_route53_health_check: Add plan time validation for `regions` +``` + +```release-note:bug +resource/aws_route53_health_check: Fix update for `ip_address` +``` \ No newline at end of file diff --git a/.changelog/20796.txt b/.changelog/20796.txt new file mode 100644 index 000000000000..f17ea5de8d21 --- /dev/null +++ b/.changelog/20796.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_launch_template: add support for `http_protocol_ipv6` to `metadata_options`. +``` + +```release-note:enhancement +resource/aws_launch_template: add plan time validation to `spot_options.block_duration_minutes` +``` \ No newline at end of file diff --git a/.changelog/20797.txt b/.changelog/20797.txt new file mode 100644 index 000000000000..b8150582f05f --- /dev/null +++ b/.changelog/20797.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ec2_managed_prefix_list: allow updating `max_entries`. +``` \ No newline at end of file diff --git a/.changelog/20809.txt b/.changelog/20809.txt new file mode 100644 index 000000000000..22b161423df8 --- /dev/null +++ b/.changelog/20809.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_endpoint_configuration: Add `async_inference_config` argument +``` diff --git a/.changelog/20825.txt b/.changelog/20825.txt new file mode 100644 index 000000000000..e3cd4fd39e4f --- /dev/null +++ b/.changelog/20825.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sagemaker_flow_definition +``` \ No newline at end of file diff --git a/.changelog/20838.txt b/.changelog/20838.txt new file mode 100644 index 000000000000..f5bf9a9115ee --- /dev/null +++ b/.changelog/20838.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_efs_file_system_policy: Add `bypass_policy_lockout_safety_check` argument +``` \ No newline at end of file diff --git a/.changelog/20842.txt b/.changelog/20842.txt new file mode 100644 index 000000000000..273dbfbb5ff7 --- /dev/null +++ b/.changelog/20842.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_imagebuilder_distribution_configuration: Improve validation error message of `name` argument +``` \ No newline at end of file diff --git a/.changelog/20861.txt b/.changelog/20861.txt new file mode 100644 index 000000000000..f464c31ad5f2 --- /dev/null +++ b/.changelog/20861.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_mwaa_environment: Increase resource creation timeout to 2 hours +``` \ No newline at end of file diff --git a/.changelog/20863.txt b/.changelog/20863.txt new file mode 100644 index 000000000000..9869938d80f5 --- /dev/null +++ b/.changelog/20863.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector_logging +``` \ No newline at end of file diff --git a/.changelog/20874.txt b/.changelog/20874.txt new file mode 100644 index 000000000000..feeca618d56e --- /dev/null +++ b/.changelog/20874.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_efs_file_system: Add `lifecycle_policy.transition_to_primary_storage_class` argument to support Intelligent-Tiering +``` \ No newline at end of file diff --git a/.changelog/20877.txt b/.changelog/20877.txt new file mode 100644 index 000000000000..61ff19f27ff5 --- /dev/null +++ b/.changelog/20877.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_iam_users +``` diff --git a/.changelog/20914.txt b/.changelog/20914.txt new file mode 100644 index 000000000000..c5504083d2b8 --- /dev/null +++ b/.changelog/20914.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_kms_key: Extends timeouts for policy and tag propagation to 5 minutes each +``` diff --git a/.changelog/20933.txt b/.changelog/20933.txt new file mode 100644 index 000000000000..379a6793e94f --- /dev/null +++ b/.changelog/20933.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_chime_voice_connector_streaming +``` \ No newline at end of file diff --git a/.changelog/20951.txt b/.changelog/20951.txt new file mode 100644 index 000000000000..ee0d6f9b7f2b --- /dev/null +++ b/.changelog/20951.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_fsx_ontap_filesystem +``` \ No newline at end of file diff --git a/.changelog/20952.txt b/.changelog/20952.txt new file mode 100644 index 000000000000..897050fecaac --- /dev/null +++ b/.changelog/20952.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_cognito_user_pool: Fix removal of `lambda_config` +``` \ No newline at end of file diff --git a/.changelog/20971.txt b/.changelog/20971.txt new file mode 100644 index 000000000000..601d97f58f92 --- /dev/null +++ b/.changelog/20971.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/efs_file_system: Allow multiple lifecycle policies. +``` + +```release-note:enhancement +data-source/efs_file_system: Add `transition_to_primary_storage_class` to `lifecycle_policy`. +``` \ No newline at end of file diff --git a/.changelog/21008.txt b/.changelog/21008.txt new file mode 100644 index 000000000000..6219147eac20 --- /dev/null +++ b/.changelog/21008.txt @@ -0,0 +1,3 @@ +```release-note:bug +data-source/aws_launch_template: Fix `error setting metadata_options` +``` \ No newline at end of file diff --git a/.changelog/21036.txt b/.changelog/21036.txt new file mode 100644 index 000000000000..fbdbdace43f0 --- /dev/null +++ b/.changelog/21036.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appstream_image_builder +``` \ No newline at end of file diff --git a/.changelog/21037.txt b/.changelog/21037.txt new file mode 100644 index 000000000000..0e622959dd4b --- /dev/null +++ b/.changelog/21037.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sagemaker_app_image_config: Add tagging support. +``` \ No newline at end of file diff --git a/.changelog/21041.txt b/.changelog/21041.txt new file mode 100644 index 000000000000..d804086541b2 --- /dev/null +++ b/.changelog/21041.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_sagemaker_studio_lifecycle_config +``` + +```release-note:enhancement +resource/aws_sagemaker_domain: Add `default_user_settings.jupyter_server_app_settings.lifecycle_config_arns` and `default_user_settings.kernel_gateway_app_settings.lifecycle_config_arns` arguments +``` + +```release-note:enhancement +resource/aws_user_profile: Add `user_settings.jupyter_server_app_settings.lifecycle_config_arns` and `user_settings.kernel_gateway_app_settings.lifecycle_config_arns` arguments +``` \ No newline at end of file diff --git a/.changelog/21053.txt b/.changelog/21053.txt new file mode 100644 index 000000000000..99f7e16ea85e --- /dev/null +++ b/.changelog/21053.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_db_proxy +``` diff --git a/.changelog/21062.txt b/.changelog/21062.txt new file mode 100644 index 000000000000..1e4eb981b351 --- /dev/null +++ b/.changelog/21062.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_route_table_association: Wait for up to 40 not found checks when creating a new route table association +``` diff --git a/.changelog/21077.txt b/.changelog/21077.txt new file mode 100644 index 000000000000..ac68c6cf37fc --- /dev/null +++ b/.changelog/21077.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Add parameter `http_proxy` to provider configuration +``` diff --git a/.changelog/21085.txt b/.changelog/21085.txt new file mode 100644 index 000000000000..743edc222aff --- /dev/null +++ b/.changelog/21085.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_dx_connection: Mark `provider_name` as Computed to avoid resource recreation with pre-v3.56.0 configurations +``` + +```release-note:bug +resource/aws_dx_lag: Mark `provider_name` as Computed to avoid resource recreation with pre-v3.56.0 configurations +``` \ No newline at end of file diff --git a/.changelog/21091.txt b/.changelog/21091.txt new file mode 100644 index 000000000000..6d05c1eee21e --- /dev/null +++ b/.changelog/21091.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_lambda_function: Add support for Graviton2 with `architectures` argument +``` + +```release-note:enhancement +resource/aws_lambda_layer_version: Add support for Graviton2 with `compatible_architectures` argument +``` + +```release-note:enhancement +data-source/aws_lambda_function: Add support for Graviton2 with `architectures` attribute +``` + +```release-note:enhancement +data-source/aws_lambda_layer_version: Add support for Graviton2 with `compatible_architectures` attribute +``` diff --git a/.changelog/21110.txt b/.changelog/21110.txt new file mode 100644 index 000000000000..e53ccb7613d9 --- /dev/null +++ b/.changelog/21110.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_cloudcontrolapi_resource +``` + +```release-note:new-data-source +aws_cloudcontrolapi_resource +``` diff --git a/.changelog/3538.txt b/.changelog/3538.txt new file mode 100644 index 000000000000..e3b8bb173dbf --- /dev/null +++ b/.changelog/3538.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_service_discovery_service: Add `force_destroy` argument +``` \ No newline at end of file diff --git a/.changelog/4563.txt b/.changelog/4563.txt new file mode 100644 index 000000000000..5bc4e9326675 --- /dev/null +++ b/.changelog/4563.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_redshift_cluster: Add `cluster_nodes` attribute +``` \ No newline at end of file diff --git a/.changelog/5904.txt b/.changelog/5904.txt new file mode 100644 index 000000000000..cc516468999f --- /dev/null +++ b/.changelog/5904.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_role: Add support for exclusive policy management `inline_policy` and `managed_policy_arns` arguments +``` \ No newline at end of file diff --git a/.changelog/6084.txt b/.changelog/6084.txt new file mode 100644 index 000000000000..d9dc33888159 --- /dev/null +++ b/.changelog/6084.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_iam_policy: Add support for lookup by `arn`, `name`, and/or `path_prefix` +``` diff --git a/.changelog/6458.txt b/.changelog/6458.txt new file mode 100644 index 000000000000..0362ca66dcff --- /dev/null +++ b/.changelog/6458.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_lb: Add ability to filter results by `tags` +``` \ No newline at end of file diff --git a/.changelog/6856.txt b/.changelog/6856.txt new file mode 100644 index 000000000000..92c8c44e452c --- /dev/null +++ b/.changelog/6856.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_service_discovery_dns_namespace +``` \ No newline at end of file diff --git a/.changelog/8114.txt b/.changelog/8114.txt new file mode 100644 index 000000000000..4538eb4af9aa --- /dev/null +++ b/.changelog/8114.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cognito_user_pool_ui_customization +``` diff --git a/.changelog/8538.txt b/.changelog/8538.txt new file mode 100644 index 000000000000..b30451f570bd --- /dev/null +++ b/.changelog/8538.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_target_group_attachment: Retry InvalidTarget errors when creating +``` diff --git a/.changelog/8611.txt b/.changelog/8611.txt new file mode 100644 index 000000000000..350021e3c1b3 --- /dev/null +++ b/.changelog/8611.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_lightsail_instance_public_ports +``` \ No newline at end of file diff --git a/.changelog/8777.txt b/.changelog/8777.txt new file mode 100644 index 000000000000..94d259a302f4 --- /dev/null +++ b/.changelog/8777.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_appautoscaling_scheduled_action: No longer re-creates when changes can be updated in-place. +``` + +```release-note:enhancement +resource/aws_appautoscaling_scheduled_action: Allows setting leaving `min_capacity` or `max_capacity` unset. +``` diff --git a/.changelog/8876.txt b/.changelog/8876.txt new file mode 100644 index 000000000000..a07821ab2ffc --- /dev/null +++ b/.changelog/8876.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eip: Add `address` argument to recover or an IPv4 address from an address pool, supporting BYOIP +``` \ No newline at end of file diff --git a/.changelog/9092.txt b/.changelog/9092.txt new file mode 100644 index 000000000000..679397328fd7 --- /dev/null +++ b/.changelog/9092.txt @@ -0,0 +1,19 @@ +```release-note:enhancement +resource/aws_budgets_budget: Add the `cost_filter` argument which allows multiple `values` to be specified per filter. This new argument will eventually replace the `cost_filters` argument +``` + +```release-note:enhancement +resource/aws_budgets_budget: Change `time_period_start` to an optional argument. If you don't specify a start date, AWS defaults to the start of your chosen time period +``` + +```release-note:bug +resource/aws_budgets_budget: Change the service name in the `arn` attribute from `budgetservice` to `budgets` +``` + +```release-note:bug +resource/aws_budgets_budget: Suppress plan differences with trailing zeroes for `limit_amount` +``` + +```release-note:bug +resource/aws_budgets_budget_action: Change the service name in the `arn` attribute from `budgetservice` to `budgets` +``` \ No newline at end of file diff --git a/.changelog/9615.txt b/.changelog/9615.txt new file mode 100644 index 000000000000..3ff712876e6c --- /dev/null +++ b/.changelog/9615.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_ssm_parameters_by_path +``` \ No newline at end of file diff --git a/.changelog/9735.txt b/.changelog/9735.txt new file mode 100644 index 000000000000..531c49875bd5 --- /dev/null +++ b/.changelog/9735.txt @@ -0,0 +1,7 @@ +```release-note:new-data-source +aws_dx_location +``` + +```release-note:new-data-source +aws_dx_locations +``` diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1aa33f206ff2..c6a62a4d54e2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,42 +1,50 @@ -# Default owner for all pull requests -* @hashicorp/terraform-aws +# There is no default owner for pull requests as reviews are scheduled based on community popularity. # Service specific owners -/aws/*apigatewayv2* @ewbankkit @hashicorp/terraform-aws -/website/**/apigatewayv2* @ewbankkit @hashicorp/terraform-aws +/aws/*apigatewayv2* @ewbankkit +/website/**/apigatewayv2* @ewbankkit -/aws/*appmesh* @ewbankkit @hashicorp/terraform-aws -/website/**/appmesh* @ewbankkit @hashicorp/terraform-aws +/aws/*appmesh* @ewbankkit +/website/**/appmesh* @ewbankkit -/aws/*backup* @ewbankkit @hashicorp/terraform-aws -/website/**/backup* @ewbankkit @hashicorp/terraform-aws +/aws/*backup* @ewbankkit +/website/**/backup* @ewbankkit -/aws/*_aws_codeartifact_*.go @DrFaust92 @hashicorp/terraform-aws -/website/**/codeartifact* @DrFaust92 @hashicorp/terraform-aws +/aws/*_aws_codeartifact_*.go @DrFaust92 +/website/**/codeartifact* @DrFaust92 -/aws/*_aws_fsx_*.go @DrFaust92 @hashicorp/terraform-aws -/website/**/fsx* @DrFaust92 @hashicorp/terraform-aws +/aws/*_aws_fsx_*.go @DrFaust92 +/website/**/fsx* @DrFaust92 -/aws/*globalaccelerator* @ewbankkit @hashicorp/terraform-aws -/website/**/globalaccelerator* @ewbankkit @hashicorp/terraform-aws +/aws/*globalaccelerator* @ewbankkit +/website/**/globalaccelerator* @ewbankkit -/aws/*_aws_glue_*.go @DrFaust92 @hashicorp/terraform-aws -/website/**/glue* @DrFaust92 @hashicorp/terraform-aws +/aws/*_aws_glue_*.go @DrFaust92 +/website/**/glue* @DrFaust92 -/aws/*kinesis_analytics* @ewbankkit @hashicorp/terraform-aws -/website/**/kinesis_analytics* @ewbankkit @hashicorp/terraform-aws +/aws/*kinesis_analytics* @ewbankkit +/website/**/kinesis_analytics* @ewbankkit -/aws/*kinesisanalyticsv2* @ewbankkit @hashicorp/terraform-aws -/website/**/kinesisanalyticsv2* @ewbankkit @hashicorp/terraform-aws +/aws/*kinesisanalyticsv2* @ewbankkit +/website/**/kinesisanalyticsv2* @ewbankkit -/aws/*route53_resolver* @ewbankkit @hashicorp/terraform-aws -/website/**/route53_resolver* @ewbankkit @hashicorp/terraform-aws +/aws/*route53_resolver* @ewbankkit +/website/**/route53_resolver* @ewbankkit -/aws/*_aws_storagegateway_*.go @DrFaust92 @hashicorp/terraform-aws -/website/**/storagegateway* @DrFaust92 @hashicorp/terraform-aws +/aws/*_aws_storagegateway_*.go @DrFaust92 +/website/**/storagegateway* @DrFaust92 -/aws/*_aws_sagemaker_*.go @DrFaust92 @hashicorp/terraform-aws -/website/**/sagemaker* @DrFaust92 @hashicorp/terraform-aws +/aws/*_aws_sagemaker_*.go @DrFaust92 +/website/**/sagemaker* @DrFaust92 -/aws/*_aws_workspaces_*.go @Tensho @hashicorp/terraform-aws -/website/**/workspaces* @Tensho @hashicorp/terraform-aws +/aws/*_aws_workspaces_*.go @Tensho +/website/**/workspaces* @Tensho + +/aws/*_aws_mwaa_*.go @shuheiktgw +/website/**/mwaa* @shuheiktgw + +/aws/*_aws_emr_*.go @shuheiktgw +/website/**/emr* @shuheiktgw + +/aws/*aws_cloudwatch_event_*.go @heitorlessa @sthulb +/website/**/cloudwatch_event* @heitorlessa @sthulb diff --git a/.github/labeler-issue-needs-triage.yml b/.github/labeler-issue-needs-triage.yml new file mode 100644 index 000000000000..1aade66eb34d --- /dev/null +++ b/.github/labeler-issue-needs-triage.yml @@ -0,0 +1,2 @@ +needs-triage: + - '.*' diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml new file mode 100644 index 000000000000..8e5e7a01cc70 --- /dev/null +++ b/.github/labeler-issue-triage.yml @@ -0,0 +1,376 @@ +bug: + # General: + # panic: + # AWS SDK Go: + # ValidationException:.*failed to satisfy constraint: Member must not be null + # Terraform CLI: + # Provider produced inconsistent final plan + # Provider produced inconsistent result after apply + # produced an invalid new value + # produced an unexpected new value + # Terraform Plugin SDK: + # doesn't support update + # Invalid address to set + - "(doesn't support update|failed to satisfy constraint: Member must not be null|Invalid address to set|panic:|produced an (invalid|unexpected) new value|Provider produced inconsistent (final plan|result after apply))" +crash: + - 'panic:' +sweeper: + - 'sweeper' +# +# AWS Per-Service Labeling +# +# Catch the following in issues to prevent false positives: +# *aws_XXX +# * aws_XXX +# * `aws_XXX` +# -aws_XXX +# - aws_XXX +# - `aws_XXX` +# data aws_XXX +# data "aws_XXX" +# resource aws_XXX +# resource "aws_XXX" +service/accessanalyzer: + - '((\*|-) ?`?|(data|resource) "?)aws_accessanalyzer_' +service/acm: + - '((\*|-) ?`?|(data|resource) "?)aws_acm_' +service/acmpca: + - '((\*|-) ?`?|(data|resource) "?)aws_acmpca_' +service/alexaforbusiness: + - '((\*|-) ?`?|(data|resource) "?)aws_alexaforbusiness_' +service/amplify: + - '((\*|-) ?`?|(data|resource) "?)aws_amplify_' +service/apigateway: + - '((\*|-) ?`?|(data|resource) "?)aws_api_gateway_' +service/apigatewayv2: + - '((\*|-) ?`?|(data|resource) "?)aws_apigatewayv2_' +service/appconfig: + - '((\*|-) ?`?|(data|resource) "?)aws_appconfig_' +service/applicationautoscaling: + - '((\*|-) ?`?|(data|resource) "?)aws_appautoscaling_' +service/applicationdiscoveryservice: + - '((\*|-) ?`?|(data|resource) "?)aws_applicationdiscoveryservice_' +service/applicationinsights: + - '((\*|-) ?`?|(data|resource) "?)aws_applicationinsights_' +service/appmesh: + - '((\*|-) ?`?|(data|resource) "?)aws_appmesh_' +service/apprunner: + - '((\*|-) ?`?|(data|resource) "?)aws_apprunner_' +service/appstream: + - '((\*|-) ?`?|(data|resource) "?)aws_appstream_' +service/appsync: + - '((\*|-) ?`?|(data|resource) "?)aws_appsync_' +service/athena: + - '((\*|-) ?`?|(data|resource) "?)aws_athena_' +service/auditmanager: + - '((\*|-) ?`?|(data|resource) "?)aws_auditmanager_' +service/autoscaling: + - '((\*|-) ?`?|(data|resource) "?)aws_(autoscaling_|launch_configuration)' +service/autoscalingplans: + - '((\*|-) ?`?|(data|resource) "?)aws_autoscalingplans_' +service/backup: + - '((\*|-) ?`?|(data|resource) "?)aws_backup_' +service/batch: + - '((\*|-) ?`?|(data|resource) "?)aws_batch_' +service/budgets: + - '((\*|-) ?`?|(data|resource) "?)aws_budgets_' +service/chime: + - '((\*|-) ?`?|(data|resource) "?)aws_chime_' +service/cloud9: + - '((\*|-) ?`?|(data|resource) "?)aws_cloud9_' +service/cloudcontrolapi: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudcontrolapi_' +service/clouddirectory: + - '((\*|-) ?`?|(data|resource) "?)aws_clouddirectory_' +service/cloudformation: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudformation_' +service/cloudfront: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudfront_' +service/cloudhsmv2: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudhsm_v2_' +service/cloudsearch: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudsearch_' +service/cloudtrail: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudtrail' +service/cloudwatch: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudwatch_(?!(event_|log_|query_))' +service/cloudwatchevents: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudwatch_event_' +service/cloudwatchlogs: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudwatch_(log_|query_)' +service/codeartifact: + - '((\*|-) ?`?|(data|resource) "?)aws_codeartifact_' +service/codebuild: + - '((\*|-) ?`?|(data|resource) "?)aws_codebuild_' +service/codecommit: + - '((\*|-) ?`?|(data|resource) "?)aws_codecommit_' +service/codedeploy: + - '((\*|-) ?`?|(data|resource) "?)aws_codedeploy_' +service/codepipeline: + - '((\*|-) ?`?|(data|resource) "?)aws_codepipeline' +service/codestar: + - '((\*|-) ?`?|(data|resource) "?)aws_codestar_' +service/codestarconnections: + - '((\*|-) ?`?|(data|resource) "?)aws_codestarconnections_' +service/codestarnotifications: + - '((\*|-) ?`?|(data|resource) "?)aws_codestarnotifications_' +service/cognito: + - '((\*|-) ?`?|(data|resource) "?)aws_cognito_' +service/configservice: + - '((\*|-) ?`?|(data|resource) "?)aws_config_' +service/connect: + - '((\*|-) ?`?|(data|resource) "?)aws_connect_' +service/databasemigrationservice: + - '((\*|-) ?`?|(data|resource) "?)aws_dms_' +service/dataexchange: + - '((\*|-) ?`?|(data|resource) "?)aws_dataexchange_' +service/datapipeline: + - '((\*|-) ?`?|(data|resource) "?)aws_datapipeline_' +service/datasync: + - '((\*|-) ?`?|(data|resource) "?)aws_datasync_' +service/dax: + - '((\*|-) ?`?|(data|resource) "?)aws_dax_' +service/detective: + - '((\*|-) ?`?|(data|resource) "?)aws_detective' +service/devicefarm: + - '((\*|-) ?`?|(data|resource) "?)aws_devicefarm_' +service/directconnect: + - '((\*|-) ?`?|(data|resource) "?)aws_dx_' +service/directoryservice: + - '((\*|-) ?`?|(data|resource) "?)aws_directory_service_' +service/dlm: + - '((\*|-) ?`?|(data|resource) "?)aws_dlm_' +service/docdb: + - '((\*|-) ?`?|(data|resource) "?)aws_docdb_' +service/dynamodb: + - '((\*|-) ?`?|(data|resource) "?)aws_dynamodb_' +service/ec2: + - '((\*|-) ?`?|(data|resource) "?)aws_(ami|availability_zone|customer_gateway|(default_)?(network_acl|route_table|security_group|subnet|vpc)|ebs_|ec2_|egress_only_internet_gateway|eip|flow_log|instance|internet_gateway|key_pair|launch_template|main_route_table_association|nat_gateway|network_interface|placement_group|prefix_list|route(\"|`|$)|snapshot_create_volume_permission|spot|volume_attachment|vpn_)' +service/ecr: + - '((\*|-) ?`?|(data|resource) "?)aws_ecr_' +service/ecrpublic: + - '((\*|-) ?`?|(data|resource) "?)aws_ecrpublic_' +service/ecs: + - '((\*|-) ?`?|(data|resource) "?)aws_ecs_' +service/efs: + - '((\*|-) ?`?|(data|resource) "?)aws_efs_' +service/eks: + - '((\*|-) ?`?|(data|resource) "?)aws_eks_' +elastic-transcoder: + - '((\*|-) ?`?|(data|resource) "?)aws_elastictranscoder_' +service/elasticache: + - '((\*|-) ?`?|(data|resource) "?)aws_elasticache_' +service/elasticbeanstalk: + - '((\*|-) ?`?|(data|resource) "?)aws_elastic_beanstalk_' +service/elasticsearch: + - '((\*|-) ?`?|(data|resource) "?)aws_elasticsearch_' +service/elb: + - '((\*|-) ?`?|(data|resource) "?)aws_(app_cookie_stickiness_policy|elb|lb_cookie_stickiness_policy|lb_ssl_negotiation_policy|load_balancer_|proxy_protocol_policy)' +service/elbv2: + - '((\*|-) ?`?|(data|resource) "?)aws_(a)?lb(\"|`|_listener|_target_group|$)' +service/emr: + - '((\*|-) ?`?|(data|resource) "?)aws_emr_' +service/emrcontainers: + - '((\*|-) ?`?|(data|resource) "?)aws_emrcontainers_' +service/eventbridge: + - '((\*|-) ?`?|(data|resource) "?)aws_cloudwatch_event_' +service/firehose: + - '((\*|-) ?`?|(data|resource) "?)aws_kinesis_firehose_' +service/fms: + - '((\*|-) ?`?|(data|resource) "?)aws_fms_' +service/forecast: + - '((\*|-) ?`?|(data|resource) "?)aws_forecast_' +service/fsx: + - '((\*|-) ?`?|(data|resource) "?)aws_fsx_' +service/gamelift: + - '((\*|-) ?`?|(data|resource) "?)aws_gamelift_' +service/glacier: + - '((\*|-) ?`?|(data|resource) "?)aws_glacier_' +service/globalaccelerator: + - '((\*|-) ?`?|(data|resource) "?)aws_globalaccelerator_' +service/glue: + - '((\*|-) ?`?|(data|resource) "?)aws_glue_' +service/greengrass: + - '((\*|-) ?`?|(data|resource) "?)aws_greengrass_' +service/guardduty: + - '((\*|-) ?`?|(data|resource) "?)aws_guardduty_' +service/iam: + - '((\*|-) ?`?|(data|resource) "?)aws_iam_' +service/identitystore: + - '((\*|-) ?`?|(data|resource) "?)aws_identitystore_' +service/imagebuilder: + - '((\*|-) ?`?|(data|resource) "?)aws_imagebuilder_' +service/inspector: + - '((\*|-) ?`?|(data|resource) "?)aws_inspector_' +service/iot: + - '((\*|-) ?`?|(data|resource) "?)aws_iot_' +service/iotanalytics: + - '((\*|-) ?`?|(data|resource) "?)aws_iotanalytics_' +service/iotevents: + - '((\*|-) ?`?|(data|resource) "?)aws_iotevents_' +service/kafka: + - '((\*|-) ?`?|(data|resource) "?)aws_msk_' +service/kinesis: + - '((\*|-) ?`?|(data|resource) "?)aws_kinesis_stream' +service/kinesisanalytics: + - '((\*|-) ?`?|(data|resource) "?)aws_kinesis_analytics_' +service/kinesisanalyticsv2: + - '((\*|-) ?`?|(data|resource) "?)aws_kinesisanalyticsv2_' +service/kms: + - '((\*|-) ?`?|(data|resource) "?)aws_kms_' +service/lakeformation: + - '((\*|-) ?`?|(data|resource) "?)aws_lakeformation_' +service/lambda: + - '((\*|-) ?`?|(data|resource) "?)aws_lambda_' +service/lexmodelbuildingservice: + - '((\*|-) ?`?|(data|resource) "?)aws_lex_' +service/licensemanager: + - '((\*|-) ?`?|(data|resource) "?)aws_licensemanager_' +service/lightsail: + - '((\*|-) ?`?|(data|resource) "?)aws_lightsail_' +service/location: + - '((\*|-) ?`?|(data|resource) "?)aws_location_' +service/machinelearning: + - '((\*|-) ?`?|(data|resource) "?)aws_machinelearning_' +service/macie: + - '((\*|-) ?`?|(data|resource) "?)aws_macie_' +service/macie2: + - '((\*|-) ?`?|(data|resource) "?)aws_macie2_' +service/marketplacecatalog: + - '((\*|-) ?`?|(data|resource) "?)aws_marketplace_catalog_' +service/mediaconnect: + - '((\*|-) ?`?|(data|resource) "?)aws_media_connect_' +service/mediaconvert: + - '((\*|-) ?`?|(data|resource) "?)aws_media_convert_' +service/medialive: + - '((\*|-) ?`?|(data|resource) "?)aws_media_live_' +service/mediapackage: + - '((\*|-) ?`?|(data|resource) "?)aws_media_package_' +service/mediastore: + - '((\*|-) ?`?|(data|resource) "?)aws_media_store_' +service/mediatailor: + - '((\*|-) ?`?|(data|resource) "?)aws_media_tailor_' +service/memorydb: + - '((\*|-) ?`?|(data|resource) "?)aws_memorydb_' +service/mobile: + - '((\*|-) ?`?|(data|resource) "?)aws_mobile_' +service/mq: + - '((\*|-) ?`?|(data|resource) "?)aws_mq_' +service/mwaa: + - '((\*|-) ?`?|(data|resource) "?)aws_mwaa_' +service/neptune: + - '((\*|-) ?`?|(data|resource) "?)aws_neptune_' +service/networkfirewall: + - '((\*|-) ?`?|(data|resource) "?)aws_networkfirewall_' +service/networkmanager: + - '((\*|-) ?`?|(data|resource) "?)aws_networkmanager_' +service/opsworks: + - '((\*|-) ?`?|(data|resource) "?)aws_opsworks_' +service/organizations: + - '((\*|-) ?`?|(data|resource) "?)aws_organizations_' +service/outposts: + - '((\*|-) ?`?|(data|resource) "?)aws_outposts_' +service/personalize: + - '((\*|-) ?`?|(data|resource) "?)aws_personalize_' +service/pinpoint: + - '((\*|-) ?`?|(data|resource) "?)aws_pinpoint_' +service/polly: + - '((\*|-) ?`?|(data|resource) "?)aws_polly_' +service/pricing: + - '((\*|-) ?`?|(data|resource) "?)aws_pricing_' +service/prometheusservice: + - '((\*|-) ?`?|(data|resource) "?)aws_prometheus_' +service/qldb: + - '((\*|-) ?`?|(data|resource) "?)aws_qldb_' +service/quicksight: + - '((\*|-) ?`?|(data|resource) "?)aws_quicksight_' +service/ram: + - '((\*|-) ?`?|(data|resource) "?)aws_ram_' +service/rds: + - '((\*|-) ?`?|(data|resource) "?)aws_(db_|rds_)' +service/redshift: + - '((\*|-) ?`?|(data|resource) "?)aws_redshift_' +service/resourcegroups: + - '((\*|-) ?`?|(data|resource) "?)aws_resourcegroups_' +service/resourcegroupstaggingapi: + - '((\*|-) ?`?|(data|resource) "?)aws_resourcegroupstaggingapi_' +service/robomaker: + - '((\*|-) ?`?|(data|resource) "?)aws_robomaker_' +service/route53: + - '((\*|-) ?`?|(data|resource) "?)aws_route53_(?!resolver_)' +service/route53domains: + - '((\*|-) ?`?|(data|resource) "?)aws_route53domains_' +service/route53recoverycontrolconfig: + - '((\*|-) ?`?|(data|resource) "?)aws_route53recoverycontrolconfig_' +service/route53recoveryreadiness: + - '((\*|-) ?`?|(data|resource) "?)aws_route53recoveryreadiness_' +service/route53resolver: + - '((\*|-) ?`?|(data|resource) "?)aws_route53_resolver_' +service/s3: + - '((\*|-) ?`?|(data|resource) "?)aws_(canonical_user_id|s3_bucket|s3_object)' +service/s3control: + - '((\*|-) ?`?|(data|resource) "?)aws_(s3_account_|s3control_)' +service/s3outposts: + - '((\*|-) ?`?|(data|resource) "?)aws_s3outposts_' +service/sagemaker: + - '((\*|-) ?`?|(data|resource) "?)aws_sagemaker_' +service/schemas: + - '((\*|-) ?`?|(data|resource) "?)aws_schemas_' +service/secretsmanager: + - '((\*|-) ?`?|(data|resource) "?)aws_secretsmanager_' +service/securityhub: + - '((\*|-) ?`?|(data|resource) "?)aws_securityhub_' +service/serverlessapplicationrepository: + - '((\*|-) ?`?|(data|resource) "?)aws_serverlessapplicationrepository_' +service/servicecatalog: + - '((\*|-) ?`?|(data|resource) "?)aws_servicecatalog_' +service/servicediscovery: + - '((\*|-) ?`?|(data|resource) "?)aws_service_discovery_' +service/servicequotas: + - '((\*|-) ?`?|(data|resource) "?)aws_servicequotas_' +service/ses: + - '((\*|-) ?`?|(data|resource) "?)aws_ses_' +service/sfn: + - '((\*|-) ?`?|(data|resource) "?)aws_sfn_' +service/shield: + - '((\*|-) ?`?|(data|resource) "?)aws_shield_' +service/signer: + - '((\*|-) ?`?|(data|resource) "?)aws_signer_' +service/simpledb: + - '((\*|-) ?`?|(data|resource) "?)aws_simpledb_' +service/snowball: + - '((\*|-) ?`?|(data|resource) "?)aws_snowball_' +service/sns: + - '((\*|-) ?`?|(data|resource) "?)aws_sns_' +service/sqs: + - '((\*|-) ?`?|(data|resource) "?)aws_sqs_' +service/ssm: + - '((\*|-) ?`?|(data|resource) "?)aws_ssm_' +service/ssoadmin: + - '((\*|-) ?`?|(data|resource) "?)aws_ssoadmin_' +service/storagegateway: + - '((\*|-) ?`?|(data|resource) "?)aws_storagegateway_' +service/sts: + - '((\*|-) ?`?|(data|resource) "?)aws_caller_identity' +service/swf: + - '((\*|-) ?`?|(data|resource) "?)aws_swf_' +service/synthetics: + - '((\*|-) ?`?|(data|resource) "?)aws_synthetics_' +service/timestreamwrite: + - '((\*|-) ?`?|(data|resource) "?)aws_timestreamwrite_' +service/transfer: + - '((\*|-) ?`?|(data|resource) "?)aws_transfer_' +service/waf: + - '((\*|-) ?`?|(data|resource) "?)aws_waf(regional)?_' +service/wafv2: + - '((\*|-) ?`?|(data|resource) "?)aws_wafv2_' +service/workdocs: + - '((\*|-) ?`?|(data|resource) "?)aws_workdocs_' +service/worklink: + - '((\*|-) ?`?|(data|resource) "?)aws_worklink_' +service/workmail: + - '((\*|-) ?`?|(data|resource) "?)aws_workmail_' +service/workspaces: + - '((\*|-) ?`?|(data|resource) "?)aws_workspaces_' +service/xray: + - '((\*|-) ?`?|(data|resource) "?)aws_xray_' diff --git a/.github/labeler-needs-triage.yml b/.github/labeler-pr-needs-triage.yml similarity index 100% rename from .github/labeler-needs-triage.yml rename to .github/labeler-pr-needs-triage.yml diff --git a/.github/labeler-pr-triage.yml b/.github/labeler-pr-triage.yml new file mode 100644 index 000000000000..3ce36577f39e --- /dev/null +++ b/.github/labeler-pr-triage.yml @@ -0,0 +1,838 @@ +dependencies: + - '.github/dependabot.yml' +documentation: + - 'docs/**/*' + - 'website/**/*' + - '*.md' +examples: + - 'examples/**/*' +provider: + - '*.md' + - '.github/**/*' + - '.gitignore' + - '.go-version' + - 'aws/auth_helpers.go' + - 'aws/awserr.go' + - 'aws/config.go' + - 'aws/*_aws_arn*' + - 'aws/*_aws_ip_ranges*' + - 'aws/*_aws_partition*' + - 'aws/*_aws_region*' + - 'aws/internal/flatmap/**/*' + - 'aws/internal/keyvaluetags/**/*' + - 'aws/internal/naming/**/*' + - 'aws/provider.go' + - 'aws/utils.go' + - 'docs/*.md' + - 'docs/contributing/**/*' + - 'GNUmakefile' + - 'infrastructure/**/*' + - 'main.go' + - 'website/docs/index.html.markdown' + - 'website/**/arn*' + - 'website/**/ip_ranges*' + - 'website/**/partition*' + - 'website/**/region*' +service/accessanalyzer: + - 'aws/internal/service/accessanalyzer/**/*' + - '**/*_accessanalyzer_*' + - '**/accessanalyzer_*' +service/acm: + - 'aws/internal/service/acm/**/*' + - '**/*_acm_*' + - '**/acm_*' +service/acmpca: + - 'aws/internal/service/acmpca/**/*' + - '**/*_acmpca_*' + - '**/acmpca_*' +service/alexaforbusiness: + - 'aws/internal/service/alexaforbusiness/**/*' + - '**/*_alexaforbusiness_*' + - '**/alexaforbusiness_*' +service/amplify: + - 'aws/internal/service/amplify/**/*' + - '**/*_amplify_*' + - '**/amplify_*' +service/apigateway: + - 'aws/internal/service/apigateway/**/*' + - '**/*_api_gateway_[^v][^2][^_]*' + - '**/*_api_gateway_vpc_link*' + - '**/api_gateway_[^v][^2][^_]*' + - '**/api_gateway_vpc_link*' +service/apigatewayv2: + - 'aws/internal/service/apigatewayv2/**/*' + - '**/*_api_gateway_v2_*' + - '**/*_apigatewayv2_*' + - '**/api_gateway_v2_*' + - '**/apigatewayv2_*' +service/appconfig: + - 'aws/internal/service/appconfig/**/*' + - '**/*_appconfig_*' + - '**/appconfig_*' +service/applicationautoscaling: + - 'aws/internal/service/applicationautoscaling/**/*' + - '**/*_appautoscaling_*' + - '**/appautoscaling_*' +service/applicationinsights: + - 'aws/internal/service/applicationinsights/**/*' + - '**/*_applicationinsights_*' + - '**/applicationinsights_*' +service/appmesh: + - 'aws/internal/service/appmesh/**/*' + - '**/*_appmesh_*' + - '**/appmesh_*' +service/apprunner: + - 'aws/internal/service/apprunner/**/*' + - '**/*_apprunner_*' + - '**/apprunner_*' +service/appstream: + - 'aws/internal/service/appstream/**/*' + - '**/*_appstream_*' + - '**/appstream_*' +service/appsync: + - 'aws/internal/service/appsync/**/*' + - '**/*_appsync_*' + - '**/appsync_*' +service/athena: + - 'aws/internal/service/athena/**/*' + - '**/*_athena_*' + - '**/athena_*' +service/auditmanager: + - 'aws/internal/service/auditmanager/**/*' + - '**/*_auditmanager_*' + - '**/auditmanager_*' +service/autoscaling: + - 'aws/internal/service/autoscaling/**/*' + - '**/*_autoscaling_*' + - '**/autoscaling_*' + - 'aws/*_aws_launch_configuration*' + - 'website/**/launch_configuration*' +service/autoscalingplans: + - 'aws/internal/service/autoscalingplans/**/*' + - '**/*_autoscalingplans_*' + - '**/autoscalingplans_*' +service/backup: + - 'aws/internal/service/backup/**/*' + - '**/*backup_*' + - '**/backup_*' +service/batch: + - 'aws/internal/service/batch/**/*' + - '**/*_batch_*' + - '**/batch_*' +service/budgets: + - 'aws/internal/service/budgets/**/*' + - '**/*_budgets_*' + - '**/budgets_*' +service/chime: + - 'aws/internal/service/chime/**/*' + - '**/*_chime_*' + - '**/chime_*' +service/cloud9: + - 'aws/internal/service/cloud9/**/*' + - '**/*_cloud9_*' + - '**/cloud9_*' +service/cloudcontrolapi: + - 'aws/internal/service/cloudcontrolapi/**/*' + - '**/*_cloudcontrolapi_*' + - '**/cloudcontrolapi_*' +service/clouddirectory: + - 'aws/internal/service/clouddirectory/**/*' + - '**/*_clouddirectory_*' + - '**/clouddirectory_*' +service/cloudformation: + - 'aws/internal/service/cloudformation/**/*' + - '**/*_cloudformation_*' + - '**/cloudformation_*' +service/cloudfront: + - 'aws/internal/service/cloudfront/**/*' + - '**/*_cloudfront_*' + - '**/cloudfront_*' +service/cloudhsmv2: + - 'aws/internal/service/cloudhsmv2/**/*' + - '**/*_cloudhsm_v2_*' + - '**/cloudhsm_v2_*' +service/cloudsearch: + - 'aws/internal/service/cloudsearch/**/*' + - '**/*_cloudsearch_*' + - '**/cloudsearch_*' +service/cloudtrail: + - 'aws/internal/service/cloudtrail/**/*' + - '**/*_cloudtrail*' + - '**/cloudtrail*' +service/cloudwatch: + - 'aws/internal/service/cloudwatch/**/*' + - '**/*_cloudwatch_dashboard*' + - '**/*_cloudwatch_metric_alarm*' + - '**/cloudwatch_dashboard*' + - '**/cloudwatch_metric_alarm*' +service/cloudwatchevents: + - 'aws/internal/service/cloudwatchevents/**/*' + - '**/*_cloudwatch_event_*' + - '**/cloudwatch_event_*' +service/cloudwatchlogs: + - 'aws/internal/service/cloudwatchlogs/**/*' + - '**/*_cloudwatch_log_*' + - '**/cloudwatch_log_*' + - '**/*_cloudwatch_query_definition*' + - '**/cloudwatch_query_definition*' +service/codeartifact: + - 'aws/internal/service/codeartifact/**/*' + - '**/*_codeartifact_*' + - '**/codeartifact_*' +service/codebuild: + - 'aws/internal/service/codebuild/**/*' + - '**/*_codebuild_*' + - '**/codebuild_*' +service/codecommit: + - 'aws/internal/service/codecommit/**/*' + - '**/*_codecommit_*' + - '**/codecommit_*' +service/codedeploy: + - 'aws/internal/service/codedeploy/**/*' + - '**/*_codedeploy_*' + - '**/codedeploy_*' +service/codepipeline: + - 'aws/internal/service/codepipeline/**/*' + - '**/*_codepipeline_*' + - '**/codepipeline_*' +service/codestar: + - 'aws/internal/service/codestar/**/*' + - '**/*_codestar_*' + - '**/codestar_*' +service/codestarconnections: + - 'aws/internal/service/codestarconnections/**/*' + - '**/*_codestarconnections_*' + - '**/codestarconnections_*' +service/codestarnotifications: + - 'aws/internal/service/codestarnotifications/**/*' + - '**/*_codestarnotifications_*' + - '**/codestarnotifications_*' +service/cognito: + - 'aws/internal/service/cognitoidentity/**/*' + - 'aws/internal/service/cognitoidentityprovider/**/*' + - '**/*_cognito_*' + - '**/cognito_*' +service/comprehend: + - 'aws/internal/service/comprehend/**/*' + - '**/*_comprehend_*' + - '**/comprehend_*' +service/configservice: + - 'aws/internal/service/configservice/**/*' + - 'aws/*_aws_config_*' + - 'website/**/config_*' +service/connect: + - 'aws/internal/service/connect/**/*' + - 'aws/*_aws_connect_*' + - 'website/**/connect_*' +service/costandusagereportservice: + - 'aws/internal/service/costandusagereportservice/**/*' + - 'aws/*_aws_cur_*' + - 'website/**/cur_*' +service/databasemigrationservice: + - 'aws/internal/service/databasemigrationservice/**/*' + - '**/*_dms_*' + - '**/dms_*' +service/dataexchange: + - 'aws/internal/service/dataexchange/**/*' + - '**/*_dataexchange_*' + - '**/dataexchange_*' +service/datapipeline: + - 'aws/internal/service/datapipeline/**/*' + - '**/*_datapipeline_*' + - '**/datapipeline_*' +service/datasync: + - 'aws/internal/service/datasync/**/*' + - '**/*_datasync_*' + - '**/datasync_*' +service/dax: + - 'aws/internal/service/dax/**/*' + - '**/*_dax_*' + - '**/dax_*' +service/detective: + - 'aws/internal/service/detective/**/*' + - '**/*_detective_*' + - '**/detective_*' +service/devicefarm: + - 'aws/internal/service/devicefarm/**/*' + - '**/*_devicefarm_*' + - '**/devicefarm_*' +service/directconnect: + - 'aws/internal/service/directconnect/**/*' + - '**/*_dx_*' + - '**/dx_*' +service/directoryservice: + - 'aws/internal/service/directoryservice/**/*' + - '**/*_directory_service_*' + - '**/directory_service_*' +service/dlm: + - 'aws/internal/service/dlm/**/*' + - '**/*_dlm_*' + - '**/dlm_*' +service/docdb: + - 'aws/internal/service/docdb/**/*' + - '**/*_docdb_*' + - '**/docdb_*' +service/dynamodb: + - 'aws/internal/service/dynamodb/**/*' + - '**/*_dynamodb_*' + - '**/dynamodb_*' + # Special casing this one because the files aren't _ec2_ +service/ec2: + - 'aws/internal/service/ec2/**/*' + - '**/*_ec2_*' + - '**/ec2_*' + - 'aws/*_aws_ami*' + - 'aws/*_aws_availability_zone*' + - 'aws/*_aws_customer_gateway*' + - 'aws/*_aws_default_network_acl*' + - 'aws/*_aws_default_route_table*' + - 'aws/*_aws_default_security_group*' + - 'aws/*_aws_default_subnet*' + - 'aws/*_aws_default_vpc*' + - 'aws/*_aws_ebs_*' + - 'aws/*_aws_egress_only_internet_gateway*' + - 'aws/*_aws_eip*' + - 'aws/*_aws_flow_log*' + - 'aws/*_aws_instance*' + - 'aws/*_aws_internet_gateway*' + - 'aws/*_aws_key_pair*' + - 'aws/*_aws_launch_template*' + - 'aws/*_aws_main_route_table_association*' + - 'aws/*_aws_nat_gateway*' + - 'aws/*_aws_network_acl*' + - 'aws/*_aws_network_interface*' + - 'aws/*_aws_placement_group*' + - 'aws/*_aws_prefix_list*' + - 'aws/*_aws_route_table*' + - 'aws/*_aws_route.*' + - 'aws/*_aws_security_group*' + - 'aws/*_aws_snapshot_create_volume_permission*' + - 'aws/*_aws_spot*' + - 'aws/*_aws_subnet*' + - 'aws/*_aws_vpc*' + - 'aws/*_aws_vpn*' + - 'aws/*_aws_volume_attachment*' + - 'website/**/availability_zone*' + - 'website/**/customer_gateway*' + - 'website/**/default_network_acl*' + - 'website/**/default_route_table*' + - 'website/**/default_security_group*' + - 'website/**/default_subnet*' + - 'website/**/default_vpc*' + - 'website/**/ebs_*' + - 'website/**/egress_only_internet_gateway*' + - 'website/**/eip*' + - 'website/**/flow_log*' + - 'website/**/instance*' + - 'website/**/internet_gateway*' + - 'website/**/key_pair*' + - 'website/**/launch_template*' + - 'website/**/main_route_table_association*' + - 'website/**/nat_gateway*' + - 'website/**/network_acl*' + - 'website/**/network_interface*' + - 'website/**/placement_group*' + - 'website/**/prefix_list*' + - 'website/**/route_table*' + - 'website/**/route.*' + - 'website/**/security_group*' + - 'website/**/snapshot_create_volume_permission*' + - 'website/**/spot_*' + - 'website/**/subnet*' + - 'website/**/vpc*' + - 'website/**/vpn*' + - 'website/**/volume_attachment*' +service/ecr: + - 'aws/internal/service/ecr/**/*' + - '**/*_ecr_*' + - '**/ecr_*' +service/ecrpublic: + - 'aws/internal/service/ecrpublic/**/*' + - '**/*_ecrpublic_*' + - '**/ecrpublic_*' +service/ecs: + - 'aws/internal/service/ecs/**/*' + - '**/*_ecs_*' + - '**/ecs_*' +service/efs: + - 'aws/internal/service/efs/**/*' + - '**/*_efs_*' + - '**/efs_*' +service/eks: + - 'aws/internal/service/eks/**/*' + - '**/*_eks_*' + - '**/eks_*' +service/elastic-transcoder: + - 'aws/internal/service/elastictranscoder/**/*' + - '**/*_elastictranscoder_*' + - '**/elastictranscoder_*' + - '**/*_elastic_transcoder_*' + - '**/elastic_transcoder_*' +service/elasticache: + - 'aws/internal/service/elasticache/**/*' + - '**/*_elasticache_*' + - '**/elasticache_*' +service/elasticbeanstalk: + - 'aws/internal/service/elasticbeanstalk/**/*' + - '**/*_elastic_beanstalk_*' + - '**/elastic_beanstalk_*' +service/elasticsearch: + - 'aws/internal/service/elasticsearchservice/**/*' + - '**/*_elasticsearch_*' + - '**/elasticsearch_*' + - '**/*_elasticsearchservice*' +service/elb: + - 'aws/internal/service/elb/**/*' + - 'aws/*_aws_app_cookie_stickiness_policy*' + - 'aws/*_aws_elb*' + - 'aws/*_aws_lb_cookie_stickiness_policy*' + - 'aws/*_aws_lb_ssl_negotiation_policy*' + - 'aws/*_aws_load_balancer*' + - 'aws/*_aws_proxy_protocol_policy*' + - 'website/**/app_cookie_stickiness_policy*' + - 'website/**/elb*' + - 'website/**/lb_cookie_stickiness_policy*' + - 'website/**/lb_ssl_negotiation_policy*' + - 'website/**/load_balancer*' + - 'website/**/proxy_protocol_policy*' +service/elbv2: + - 'aws/internal/service/elbv2/**/*' + - 'aws/*_lb.*' + - 'aws/*_lb_listener*' + - 'aws/*_lb_target_group*' + - 'website/**/lb.*' + - 'website/**/lb_listener*' + - 'website/**/lb_target_group*' +service/emr: + - 'aws/internal/service/emr/**/*' + - '**/*_emr_*' + - '**/emr_*' +service/emrcontainers: + - 'aws/internal/service/emrcontainers/**/*' + - '**/*_emrcontainers_*' + - '**/emrcontainers_*' +service/eventbridge: + # EventBridge is rebranded CloudWatch Events + - 'aws/internal/service/cloudwatchevents/**/*' + - '**/*_cloudwatch_event_*' + - '**/cloudwatch_event_*' +service/firehose: + - 'aws/internal/service/firehose/**/*' + - '**/*_firehose_*' + - '**/firehose_*' +service/fms: + - 'aws/internal/service/fms/**/*' + - '**/*_fms_*' + - '**/fms_*' +service/fsx: + - 'aws/internal/service/fsx/**/*' + - '**/*_fsx_*' + - '**/fsx_*' +service/gamelift: + - 'aws/internal/service/gamelift/**/*' + - '**/*_gamelift_*' + - '**/gamelift_*' +service/glacier: + - 'aws/internal/service/glacier/**/*' + - '**/*_glacier_*' + - '**/glacier_*' +service/globalaccelerator: + - 'aws/internal/service/globalaccelerator/**/*' + - '**/*_globalaccelerator_*' + - '**/globalaccelerator_*' +service/glue: + - 'aws/internal/service/glue/**/*' + - '**/*_glue_*' + - '**/glue_*' +service/greengrass: + - 'aws/internal/service/greengrass/**/*' + - '**/*_greengrass_*' + - '**/greengrass_*' +service/guardduty: + - 'aws/internal/service/guardduty/**/*' + - '**/*_guardduty_*' + - '**/guardduty_*' +service/iam: + - 'aws/internal/service/iam/**/*' + - '**/*_iam_*' + - '**/iam_*' +service/identitystore: + - 'aws/internal/service/identitystore/**/*' + - '**/*_identitystore_*' + - '**/identitystore_*' +service/imagebuilder: + - 'aws/internal/service/imagebuilder/**/*' + - '**/*_imagebuilder_*' + - '**/imagebuilder_*' +service/inspector: + - 'aws/internal/service/inspector/**/*' + - '**/*_inspector_*' + - '**/inspector_*' +service/iot: + - 'aws/internal/service/iot/**/*' + - '**/*_iot_*' + - '**/iot_*' +service/iotanalytics: + - 'aws/internal/service/iotanalytics/**/*' + - '**/*_iotanalytics_*' + - '**/iotanalytics_*' +service/iotevents: + - 'aws/internal/service/iotevents/**/*' + - '**/*_iotevents_*' + - '**/iotevents_*' +service/kafka: + - 'aws/internal/service/kafka/**/*' + - '**/*_msk_*' + - '**/msk_*' +service/kinesis: + - 'aws/internal/service/kinesis/**/*' + - 'aws/*_aws_kinesis_stream*' + - 'website/kinesis_stream*' +service/kinesisanalytics: + - 'aws/internal/service/kinesisanalytics/**/*' + - '**/*_kinesis_analytics_*' + - '**/kinesis_analytics_*' +service/kinesisanalyticsv2: + - 'aws/internal/service/kinesisanalyticsv2/**/*' + - '**/*_kinesisanalyticsv2_*' + - '**/kinesisanalyticsv2_*' +service/kms: + - 'aws/internal/service/kms/**/*' + - '**/*_kms_*' + - '**/kms_*' +service/lakeformation: + - 'aws/internal/service/lakeformation/**/*' + - '**/*_lakeformation_*' + - '**/lakeformation_*' +service/lambda: + - 'aws/internal/service/lambda/**/*' + - '**/*_lambda_*' + - '**/lambda_*' +service/lexmodelbuildingservice: + - 'aws/internal/service/lexmodelbuildingservice/**/*' + - '**/*_lex_*' + - '**/lex_*' +service/licensemanager: + - 'aws/internal/service/licensemanager/**/*' + - '**/*_licensemanager_*' + - '**/licensemanager_*' +service/lightsail: + - 'aws/internal/service/lightsail/**/*' + - '**/*_lightsail_*' + - '**/lightsail_*' +service/location: + - 'aws/internal/service/location/**/*' + - '**/*_location_*' + - '**/location_*' +service/machinelearning: + - 'aws/internal/service/machinelearning/**/*' + - '**/*_machinelearning_*' + - '**/machinelearning_*' +service/macie: + - 'aws/internal/service/macie/**/*' + - '**/*_macie_*' + - '**/macie_*' +service/macie2: + - 'aws/internal/service/macie2/**/*' + - '**/*_macie2_*' + - '**/macie2_*' +service/marketplacecatalog: + - 'aws/internal/service/marketplacecatalog/**/*' + - '**/*_marketplace_catalog_*' + - '**/marketplace_catalog_*' +service/mediaconnect: + - 'aws/internal/service/mediaconnect/**/*' + - '**/*_media_connect_*' + - '**/media_connect_*' +service/mediaconvert: + - 'aws/internal/service/mediaconvert/**/*' + - '**/*_media_convert_*' + - '**/media_convert_*' +service/medialive: + - 'aws/internal/service/medialive/**/*' + - '**/*_media_live_*' + - '**/media_live_*' +service/mediapackage: + - 'aws/internal/service/mediapackage/**/*' + - '**/*_media_package_*' + - '**/media_package_*' +service/mediastore: + - 'aws/internal/service/mediastore/**/*' + - '**/*_media_store_*' + - '**/media_store_*' +service/mediatailor: + - 'aws/internal/service/mediatailor/**/*' + - '**/*_media_tailor_*' + - '**/media_tailor_*' +service/memorydb: + - 'aws/internal/service/memorydb/**/*' + - '**/*_memorydb_*' + - '**/memorydb_*' +service/mobile: + - 'aws/internal/service/mobile/**/*' + - '**/*_mobile_*' + - '**/mobile_*' +service/mq: + - 'aws/internal/service/mq/**/*' + - '**/*_mq_*' + - '**/mq_*' +service/mwaa: + - 'aws/internal/service/mwaa/**/*' + - '**/*_mwaa_*' + - '**/mwaa_*' +service/neptune: + - 'aws/internal/service/neptune/**/*' + - '**/*_neptune_*' + - '**/neptune_*' +service/networkfirewall: + - 'aws/internal/service/networkfirewall/**/*' + - '**/*_networkfirewall_*' + - '**/networkfirewall_*' +service/networkmanager: + - 'aws/internal/service/networkmanager/**/*' + - '**/*_networkmanager_*' + - '**/networkmanager_*' +service/opsworks: + - 'aws/internal/service/opsworks/**/*' + - '**/*_opsworks_*' + - '**/opsworks_*' +service/organizations: + - 'aws/internal/service/organizations/**/*' + - '**/*_organizations_*' + - '**/organizations_*' +service/outposts: + - 'aws/internal/service/outposts/**/*' + - '**/*_outposts_*' + - '**/outposts_*' +service/pinpoint: + - 'aws/internal/service/pinpoint/**/*' + - '**/*_pinpoint_*' + - '**/pinpoint_*' +service/polly: + - 'aws/internal/service/polly/**/*' + - '**/*_polly_*' + - '**/polly_*' +service/pricing: + - 'aws/internal/service/pricing/**/*' + - '**/*_pricing_*' + - '**/pricing_*' +service/prometheusservice: + - 'aws/internal/service/prometheus/**/*' + - '**/*_prometheus_*' + - '**/prometheus_*' +service/qldb: + - 'aws/internal/service/qldb/**/*' + - '**/*_qldb_*' + - '**/qldb_*' +service/quicksight: + - 'aws/internal/service/quicksight/**/*' + - '**/*_quicksight_*' + - '**/quicksight_*' +service/ram: + - 'aws/internal/service/ram/**/*' + - '**/*_ram_*' + - '**/ram_*' +service/rds: + - 'aws/internal/service/rds/**/*' + - 'aws/*_aws_db_*' + - 'aws/*_aws_rds_*' + - 'website/**/db_*' + - 'website/**/rds_*' +service/redshift: + - 'aws/internal/service/redshift/**/*' + - '**/*_redshift_*' + - '**/redshift_*' +service/resourcegroups: + - 'aws/internal/service/resourcegroups/**/*' + - '**/*_resourcegroups_*' + - '**/resourcegroups_*' +service/resourcegroupstaggingapi: + - 'aws/internal/service/resourcegroupstaggingapi/**/*' + - '**/*_resourcegroupstaggingapi_*' + - '**/resourcegroupstaggingapi_*' +service/robomaker: + - 'aws/internal/service/robomaker/**/*' + - '**/*_robomaker_*' + - '**/robomaker_*' +service/route53: + - 'aws/internal/service/route53/**/*' + - '**/*_route53_delegation_set*' + - '**/*_route53_health_check*' + - '**/*_route53_query_log*' + - '**/*_route53_record*' + - '**/*_route53_vpc_association_authorization*' + - '**/*_route53_zone*' + - '**/route53_delegation_set*' + - '**/route53_health_check*' + - '**/route53_query_log*' + - '**/route53_record*' + - '**/route53_vpc_association_authorization*' + - '**/route53_zone*' +service/route53domains: + - 'aws/internal/service/route53domains/**/*' + - '**/*_route53domains_*' + - '**/route53domains_*' +service/route53recoverycontrolconfig: + - 'aws/internal/service/route53recoverycontrolconfig/**/*' + - '**/*_route53recoverycontrolconfig_*' + - '**/route53recoverycontrolconfig_*' +service/route53recoveryreadiness: + - 'aws/internal/service/route53recoveryreadiness/**/*' + - '**/*_route53recoveryreadiness_*' + - '**/route53recoveryreadiness_*' +service/route53resolver: + - 'aws/internal/service/route53resolver/**/*' + - '**/*_route53_resolver_*' + - '**/route53_resolver_*' +service/s3: + - 'aws/internal/service/s3/**/*' + - '**/*_s3_bucket*' + - '**/s3_bucket*' + - '**/*_s3_object*' + - '**/s3_object*' + - 'aws/*_aws_canonical_user_id*' + - 'website/**/canonical_user_id*' +service/s3control: + - 'aws/internal/service/s3control/**/*' + - '**/*_s3_account_*' + - '**/s3_account_*' + - '**/*_s3control_*' + - '**/s3control_*' +service/s3outposts: + - 'aws/internal/service/s3outposts/**/*' + - '**/*_s3outposts_*' + - '**/s3outposts_*' +service/sagemaker: + - 'aws/internal/service/sagemaker/**/*' + - '**/*_sagemaker_*' + - '**/sagemaker_*' +service/schemas: + - 'aws/internal/service/schemas/**/*' + - '**/*_schemas_*' + - '**/schemas_*' +service/secretsmanager: + - 'aws/internal/service/secretsmanager/**/*' + - '**/*_secretsmanager_*' + - '**/secretsmanager_*' +service/securityhub: + - 'aws/internal/service/securityhub/**/*' + - '**/*_securityhub_*' + - '**/securityhub_*' +service/serverlessapplicationrepository: + - 'aws/internal/service/serverlessapplicationrepository/**/*' + - '**/*_serverlessapplicationrepository_*' + - '**/serverlessapplicationrepository_*' +service/servicecatalog: + - 'aws/internal/service/servicecatalog/**/*' + - '**/*_servicecatalog_*' + - '**/servicecatalog_*' +service/servicediscovery: + - 'aws/internal/service/servicediscovery/**/*' + - '**/*_service_discovery_*' + - '**/service_discovery_*' +service/servicequotas: + - 'aws/internal/service/servicequotas/**/*' + - '**/*_servicequotas_*' + - '**/servicequotas_*' +service/ses: + - 'aws/internal/service/ses/**/*' + - '**/*_ses_*' + - '**/ses_*' +service/sfn: + - 'aws/internal/service/sfn/**/*' + - '**/*_sfn_*' + - '**/sfn_*' +service/shield: + - 'aws/internal/service/shield/**/*' + - '**/*_shield_*' + - '**/shield_*' +service/signer: + - '**/*_signer_*' + - '**/signer_*' +service/simpledb: + - 'aws/internal/service/simpledb/**/*' + - '**/*_simpledb_*' + - '**/simpledb_*' +service/snowball: + - 'aws/internal/service/snowball/**/*' + - '**/*_snowball_*' + - '**/snowball_*' +service/sns: + - 'aws/internal/service/sns/**/*' + - '**/*_sns_*' + - '**/sns_*' +service/sqs: + - 'aws/internal/service/sqs/**/*' + - '**/*_sqs_*' + - '**/sqs_*' +service/ssm: + - 'aws/internal/service/ssm/**/*' + - '**/*_ssm_*' + - '**/ssm_*' +service/ssoadmin: + - 'aws/internal/service/ssoadmin/**/*' + - '**/*_ssoadmin_*' + - '**/ssoadmin_*' +service/storagegateway: + - 'aws/internal/service/storagegateway/**/*' + - '**/*_storagegateway_*' + - '**/storagegateway_*' +service/sts: + - 'aws/internal/service/sts/**/*' + - 'aws/*_aws_caller_identity*' + - 'website/**/caller_identity*' +service/swf: + - 'aws/internal/service/swf/**/*' + - '**/*_swf_*' + - '**/swf_*' +service/synthetics: + - 'aws/internal/service/synthetics/**/*' + - '**/*_synthetics_*' + - '**/synthetics_*' +service/timestreamwrite: + - 'aws/internal/service/timestreamwrite/**/*' + - '**/*_timestreamwrite_*' + - '**/timestreamwrite_*' +service/transfer: + - 'aws/internal/service/transfer/**/*' + - '**/*_transfer_*' + - '**/transfer_*' +service/waf: + - 'aws/internal/service/waf/**/*' + - 'aws/internal/service/wafregional/**/*' + - '**/*_waf_*' + - '**/waf_*' + - '**/*_wafregional_*' + - '**/wafregional_*' +service/wafv2: + - 'aws/internal/service/wafv2/**/*' + - '**/*_wafv2_*' + - '**/wafv2_*' +service/workdocs: + - 'aws/internal/service/workdocs/**/*' + - '**/*_workdocs_*' + - '**/workdocs_*' +service/worklink: + - 'aws/internal/service/worklink/**/*' + - '**/*_worklink_*' + - '**/worklink_*' +service/workmail: + - 'aws/internal/service/workmail/**/*' + - '**/*_workmail_*' + - '**/workmail_*' +service/workspaces: + - 'aws/internal/service/workspaces/**/*' + - '**/*_workspaces_*' + - '**/workspaces_*' +service/xray: + - 'aws/internal/service/xray/**/*' + - '**/*_xray_*' + - '**/xray_*' +tests: + - '**/*_test.go' + - '**/testdata/**/*' + - '.github/workflows/*' + - '.golangci.yml' + - '.markdownlinkcheck.json' + - '.markdownlint.yml' + - 'staticcheck.conf' diff --git a/.github/workflows/acctest-terraform-lint.yml b/.github/workflows/acctest-terraform-lint.yml index 0aaacd59adcf..878100db866d 100644 --- a/.github/workflows/acctest-terraform-lint.yml +++ b/.github/workflows/acctest-terraform-lint.yml @@ -8,13 +8,11 @@ on: paths: - .github/workflows/acctest-terraform-lint.yml - .go-version + - .tflint.hcl - aws/*_test.go - scripts/validate-terraform.sh - tools/go.mod -env: - GO111MODULE: on - jobs: terrafmt: runs-on: ubuntu-latest @@ -34,12 +32,15 @@ jobs: - run: cd tools && go install github.com/katbyte/terrafmt # - run: terrafmt diff ./aws --check --pattern '*_test.go' --fmtcompat - run: | + # resource_aws_ecs_capacity_provider_test.go: two format verbs on one line (%[2]q = %[3]q). https://github.com/katbyte/terrafmt/issues/46 + # resource_aws_efs_file_system_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 + # resource_aws_kms_grant_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 + # resource_aws_quicksight_user_test.go: format verb as resource name (%[1]q). https://github.com/katbyte/terrafmt/issues/48 + # resource_aws_sns_platform_application_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 find ./aws -type f -name '*_test.go' \ | sort -u \ - | grep -v resource_aws_apigatewayv2_domain_name_test.go \ | grep -v resource_aws_ecs_capacity_provider_test.go \ | grep -v resource_aws_efs_file_system_test.go \ - | grep -v resource_aws_kinesis_stream_test.go \ | grep -v resource_aws_kms_grant_test.go \ | grep -v resource_aws_quicksight_user_test.go \ | grep -v resource_aws_s3_bucket_object_test.go \ @@ -62,15 +63,28 @@ jobs: path: ~/go/pkg/mod key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('go.sum') }} - run: cd tools && go install github.com/katbyte/terrafmt + - run: cd tools && go install github.com/terraform-linters/tflint + + - uses: actions/cache@v2 + name: Cache plugin dir + with: + path: ~/.tflint.d/plugins + key: ${{ matrix.os }}-tflint-${{ hashFiles('.tflint.hcl') }} + + - run: tflint --init + - run: | + # resource_aws_ecs_capacity_provider_test.go: two format verbs on one line (%[2]q = %[3]q). https://github.com/katbyte/terrafmt/issues/46 + # resource_aws_efs_file_system_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 + # resource_aws_kms_grant_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 + # resource_aws_lambda_permission_test.go: format verb as resource name ("%s"). https://github.com/katbyte/terrafmt/issues/48 + # resource_aws_quicksight_user_test.go: format verb as resource name (%[1]q). https://github.com/katbyte/terrafmt/issues/48 + # resource_aws_sns_platform_application_test.go: argument name is format verb and replaced with quoted string. https://github.com/katbyte/terrafmt/issues/47 find ./aws -type f -name '*_test.go' \ | sort -u \ - | grep -v resource_aws_apigatewayv2_domain_name_test.go \ | grep -v resource_aws_ecs_capacity_provider_test.go \ | grep -v resource_aws_efs_file_system_test.go \ - | grep -v resource_aws_elasticache_cluster_test.go \ - | grep -v resource_aws_kinesis_stream_test.go \ | grep -v resource_aws_kms_grant_test.go \ | grep -v resource_aws_lambda_permission_test.go \ | grep -v resource_aws_quicksight_user_test.go \ diff --git a/.github/workflows/autoremove_labels.yml b/.github/workflows/autoremove_labels.yml new file mode 100644 index 000000000000..cf13d64738a9 --- /dev/null +++ b/.github/workflows/autoremove_labels.yml @@ -0,0 +1,17 @@ +on: + issues: + types: [closed] + pull_request_target: + types: [closed] +jobs: + remove-labels: + runs-on: ubuntu-latest + strategy: + matrix: + labels: ['needs-triage', 'waiting-response'] + steps: + - name: Remove ${{ matrix.labels }} label + uses: actions-ecosystem/action-remove-labels@v1 + if: contains(github.event.*.labels.*.name, matrix.labels) + with: + labels: ${{ matrix.labels }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 7f193f4fe07b..970e1704296d 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -11,12 +11,9 @@ on: - CHANGELOG.md pull_request_target: -env: - GO111MODULE: on - jobs: changes: - if: github.event_name == 'pull_request_target' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "gdavison", "maryelizbeth", "YakDriver"]'), github.actor) + if: github.event_name == 'pull_request_target' && !contains(fromJSON('["anGie44", "breathingdust", "ewbankkit", "gdavison", "justinretzolk", "maryelizbeth", "YakDriver", "zhelding"]'), github.actor) name: Filter Changes runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 99a472fd44c7..c14433282667 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -9,7 +9,7 @@ on: jobs: changes: - if: github.event_name == 'pull_request_target' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "dependabot[bot]", "DrFaust92", "ewbankkit", "gdavison", "maryelizbeth", "YakDriver"]'), github.actor) + if: github.event_name == 'pull_request_target' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "dependabot[bot]", "DrFaust92", "ewbankkit", "gdavison", "justinretzolk", "maryelizbeth", "YakDriver", "zhelding"]'), github.actor) name: Filter Changes runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 0eef549dfeec..646fb7991888 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -11,9 +11,6 @@ on: - .go-version - docs/** -env: - GO111MODULE: on - jobs: markdown-link-check: runs-on: ubuntu-latest diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 181d896fae66..282371afd626 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -7,6 +7,7 @@ on: paths: - .github/workflows/examples.yml - .go-version + - .tflint.hcl - examples/** - tools/go.mod @@ -19,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - terraform_version: ["0.12.29"] + terraform_version: ["0.12.31", "1.0.6"] steps: - uses: actions/checkout@v2 with: @@ -36,16 +37,31 @@ jobs: - name: go build run: go build -o terraform-plugin-dir/terraform-provider-aws_v99.99.99_x5 . - name: override plugin - run: mkdir -p ~/.terraform.d/plugins && cp terraform-plugin-dir/terraform-provider-aws_v99.99.99_x5 ~/.terraform.d/plugins + run: | + # For Terraform v0.12 + mkdir -p ~/.terraform.d/plugins + cp terraform-plugin-dir/terraform-provider-aws_v99.99.99_x5 ~/.terraform.d/plugins + # For newer versions + mkdir -p ~/.terraform.d/plugins/registry.terraform.io/hashicorp/aws/99.99.99/$(go env GOOS)_$(go env GOARCH)/ + cp terraform-plugin-dir/terraform-provider-aws_v99.99.99_x5 ~/.terraform.d/plugins/registry.terraform.io/hashicorp/aws/99.99.99/$(go env GOOS)_$(go env GOARCH)/ - uses: hashicorp/setup-terraform@v1 with: terraform_version: ${{ matrix.terraform_version }} # Needed to use the output of `terraform validate -json` terraform_wrapper: false + - name: install tflint run: cd tools && go install github.com/terraform-linters/tflint + + - uses: actions/cache@v2 + name: Cache plugin dir + with: + path: ~/.tflint.d/plugins + key: ${{ matrix.os }}-tflint-${{ hashFiles('.tflint.hcl') }} + - name: terraform run: | + TFLINT_CONFIG="$(pwd -P)/.tflint.hcl" for DIR in $(find ./examples -type f -name '*.tf' -exec dirname {} \; | sort -u); do pushd "$DIR" if [ -f terraform.template.tfvars ]; then @@ -59,11 +75,14 @@ jobs: # Catch errors terraform validate # Terraform syntax checks - tflint \ + # We don't want to exit on the first tflint error + set +e + tflint --config=$TFLINT_CONFIG \ --enable-rule=terraform_deprecated_interpolation \ --enable-rule=terraform_deprecated_index \ --enable-rule=terraform_unused_declarations \ --enable-rule=terraform_comment_syntax \ --enable-rule=terraform_required_version + set -e popd done diff --git a/.github/workflows/firewatch.yml b/.github/workflows/firewatch.yml new file mode 100644 index 000000000000..f4d264ed0cde --- /dev/null +++ b/.github/workflows/firewatch.yml @@ -0,0 +1,26 @@ + +on: + schedule: + - cron: '0 * * * *' + workflow_dispatch: +name: Firewatch +jobs: + FirewatchJob: + if: github.repository_owner == 'hashicorp' + runs-on: ubuntu-latest + steps: + - name: Firewatch + uses: breathingdust/firewatch@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + alert_threshold: 10 + issue_age_months: 3 + slack_token: ${{ secrets.SLACK_BOT_TOKEN }} + slack_channel: ${{ secrets.SLACK_CHANNEL }} + - name: UploadArtifact + uses: actions/upload-artifact@v2 + with: + name: firewatch + path: firewatch.data + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/generate_changelog.yml b/.github/workflows/generate_changelog.yml index b4c82f08c817..92fc503f1ceb 100644 --- a/.github/workflows/generate_changelog.yml +++ b/.github/workflows/generate_changelog.yml @@ -2,19 +2,27 @@ name: Generate CHANGELOG on: pull_request: types: [closed] + workflow_dispatch: jobs: GenerateChangelog: - if: github.event.pull_request.merged + if: github.event.pull_request.merged || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - run: cd tools && go install github.com/hashicorp/go-changelog/cmd/changelog-build + - run: ./scripts/generate-changelog.sh - run: | - ./scripts/generate-changelog.sh - git config --local user.email "changelogbot@hashicorp.com" - git config --local user.name "changelogbot" - git add CHANGELOG.md - git commit -m "Update CHANGELOG.md for #${{ github.event.pull_request.number }}" - git push + if [[ `git status --porcelain` ]]; then + if ${{github.event_name == 'workflow_dispatch'}}; then + MSG="Update CHANGELOG.md (Manual Trigger)" + else + MSG="Update CHANGELOG.md for #${{ github.event.pull_request.number }}" + fi + git config --local user.email changelogbot@hashicorp.com + git config --local user.name changelogbot + git add CHANGELOG.md + git commit -m "$MSG" + git push + fi diff --git a/.github/workflows/issue-comment-created.yml b/.github/workflows/issue-comment-created.yml new file mode 100644 index 000000000000..ff2256754927 --- /dev/null +++ b/.github/workflows/issue-comment-created.yml @@ -0,0 +1,15 @@ +name: Issue Comment Created Triage + +on: + issue_comment: + types: [created] + +jobs: + issue_comment_triage: + runs-on: ubuntu-latest + steps: + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: | + stale + waiting-response diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index e009163b52fe..85320ce8065c 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -6,16 +6,17 @@ jobs: markIssuesForTriage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1.0.0 - - name: Apply Issue Triage Label - uses: actions/github-script@v3 - if: github.event.action == 'opened' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "DrFaust92", "ewbankkit", "gdavison", "maryelizbeth", "YakDriver"]'), github.actor) + - uses: actions/checkout@v2 + - name: Apply Issue needs-triage Label + if: github.event.action == 'opened' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "DrFaust92", "ewbankkit", "gdavison", "justinretzolk", "maryelizbeth", "YakDriver", "zhelding"]'), github.actor) + uses: github/issue-labeler@v2.4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['needs-triage'] - }) + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler-issue-needs-triage.yml + enable-versioned-regex: 0 + - name: Apply Issue Triage Labels + uses: github/issue-labeler@v2.4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler-issue-triage.yml + enable-versioned-regex: 0 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 000000000000..3f7a969d889b --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,23 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '50 1 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + github-token: ${{ github.token }} + issue-lock-comment: > + I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. + + If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. + issue-lock-inactive-days: '30' + pr-lock-comment: > + I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. + + If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. + pr-lock-inactive-days: '30' diff --git a/.github/workflows/milestone-closed.yml b/.github/workflows/milestone-closed.yml new file mode 100644 index 000000000000..2ac8888bf9fd --- /dev/null +++ b/.github/workflows/milestone-closed.yml @@ -0,0 +1,20 @@ +name: Closed Milestones + +on: + milestone: + types: [closed] + +permissions: + issues: write + pull-requests: write + +jobs: + Comment: + runs-on: ubuntu-latest + steps: + - uses: bflad/action-milestone-comment@v1 + with: + body: | + This functionality has been released in [${{ github.event.milestone.title }} of the Terraform AWS Provider](https://github.com/${{ github.repository }}/blob/${{ github.event.milestone.title }}/CHANGELOG.md). Please see the [Terraform documentation on provider versioning](https://www.terraform.io/docs/configuration/providers.html#provider-versions) or reach out if you need any assistance upgrading. + + For further feature requests or bug reports with this functionality, please create a [new GitHub issue](https://github.com/${{ github.repository }}/issues/new/choose) following the template. Thank you! diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 32e5a977c9c5..c95068d99f09 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Move team PRs to Review column - uses: alex-page/github-project-automation-plus@v0.5.1 - if: contains(fromJSON('["anGie44", "bflad", "bill-rich", "breathingdust", "gdavison", "maryelizbeth", "YakDriver"]'), github.actor) && github.event.pull_request.draft == false + uses: alex-page/github-project-automation-plus@v0.8.1 + if: contains(fromJSON('["anGie44", "breathingdust", "ewbankkit", "gdavison", "maryelizbeth", "YakDriver", "zhelding"]'), github.actor) && github.event.pull_request.draft == false with: project: AWS Provider Working Board column: Open Maintainer PR - repo-token: ${{ secrets.GITHUB_ACTIONS_TOKEN}} + repo-token: ${{ secrets.ORGSCOPED_GITHUB_TOKEN}} diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 7b400f74c058..bcfb724c485a 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -3,16 +3,43 @@ on: name: Pull Request Target (All types) jobs: + Labeler: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Apply Labels + uses: actions/labeler@v3 + with: + configuration-path: .github/labeler-pr-triage.yml + repo-token: ${{ secrets.GITHUB_TOKEN }} NeedsTriageLabeler: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Apply needs-triage Label uses: actions/labeler@v3 - if: github.event.action == 'opened' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "dependabot[bot]", "DrFaust92", "ewbankkit", "gdavison", "maryelizbeth", "YakDriver"]'), github.actor) + if: github.event.action == 'opened' && !contains(fromJSON('["anGie44", "bflad", "breathingdust", "dependabot[bot]", "DrFaust92", "ewbankkit", "gdavison", "justinretzolk", "maryelizbeth", "YakDriver", "zhelding"]'), github.actor) with: - configuration-path: .github/labeler-needs-triage.yml + configuration-path: .github/labeler-pr-needs-triage.yml repo-token: ${{ secrets.GITHUB_TOKEN }} + SizeLabeler: + runs-on: ubuntu-latest + steps: + # See also: https://github.com/CodelyTV/pr-size-labeler/pull/26 + - name: Apply Size Label + uses: bflad/pr-size-labeler@7df62b12a176513631973abfe151d2b6213c3f12 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + xs_label: 'size/XS' + xs_max_size: '30' + s_label: 'size/S' + s_max_size: '60' + m_label: 'size/M' + m_max_size: '150' + l_label: 'size/L' + l_max_size: '300' + xl_label: 'size/XL' + message_if_xl: '' PullRequestComments: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..e86bad8f7b25 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Post Publish +on: + release: + types: [published] +jobs: + tidy: + name: Tidy Asana + runs-on: ubuntu-latest + steps: + - uses: breathingdust/github-asana-tidy@v1 + with: + asana_pat: ${{ secrets.asana_pat }} + asana_target_section_gid: '1141945723817371' + asana_workspace_gid: '90955849329269' + asana_project_gid: '632425409545160' + asana_github_url_field_gid: '1134594824474912' + github_release_name: ${{ github.event.release.tag_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + project-archive: + name: Archive Released Cards + runs-on: ubuntu-latest + steps: + - uses: breathingdust/github-project-archive@v1 + with: + github_done_column_id: 11513756 + github_release_name: ${{ github.event.release.tag_name }} + github_token: ${{ secrets.ORGSCOPED_GITHUB_TOKEN }} + changelog-newversion: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: main + - run: | + CHANGELOG_FILE_NAME="CHANGELOG.md" + PREVIOUS_RELEASE_TAG=$(git describe --abbrev=0 --match='v*.*.*' --tags) + echo Previous release is: $PREVIOUS_RELEASE_TAG + + NEW_RELEASE_LINE=$(echo $PREVIOUS_RELEASE_TAG | awk -F. '{ + $1 = substr($1,2) + $2 += 1 + printf("%s.%02d.%s\n\n", $1, $2, $3); + }') + + echo New minor version is: v$NEW_RELEASE_LINE + + echo -e "## $NEW_RELEASE_LINE (Unreleased)\n$(cat $CHANGELOG_FILE_NAME)" > $CHANGELOG_FILE_NAME + - run: | + git config --local user.email changelogbot@hashicorp.com + git config --local user.name changelogbot + git add CHANGELOG.md + git commit -m "Update CHANGELOG.md after ${{ github.event.release.tag_name }}" + git push diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index b4accb34088b..a78b97944ff7 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -5,9 +5,6 @@ on: - cron: '15 5 * * *' workflow_dispatch: -env: - GO111MODULE: on - jobs: goreleaser: runs-on: ubuntu-latest diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a7e83bc1a058..da93bf58c9dd 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 720 diff --git a/.github/workflows/team_slack_bot.yml b/.github/workflows/team_slack_bot.yml index b6af5280a6b6..4dbfc3e4f46a 100644 --- a/.github/workflows/team_slack_bot.yml +++ b/.github/workflows/team_slack_bot.yml @@ -8,11 +8,12 @@ jobs: open-pr-stats: runs-on: ubuntu-latest name: open-pr-stats + if: github.repository_owner == 'hashicorp' steps: - name: open-pr-stats uses: breathingdust/github-team-slackbot@v17 with: - github_token: ${{ secrets.GITHUB_ACTIONS_TOKEN}} + github_token: ${{ secrets.ORGSCOPED_GITHUB_TOKEN}} org: hashicorp repo: terraform-provider-aws team_slug: terraform-aws diff --git a/.github/workflows/terraform_provider.yml b/.github/workflows/terraform_provider.yml index b62824e2dd7c..cbd7fa79f276 100644 --- a/.github/workflows/terraform_provider.yml +++ b/.github/workflows/terraform_provider.yml @@ -10,7 +10,6 @@ on: - .github/workflows/terraform_provider.yml - .go-version - .golangci.yml - - .goreleaser.yml - .semgrep.yml - aws/** - awsproviderlint/** @@ -27,8 +26,7 @@ on: env: AWS_DEFAULT_REGION: us-west-2 - GO111MODULE: on - TERRAFORM_VERSION: "0.12.25" + TERRAFORM_VERSION: "0.14.8" jobs: go_mod_download: @@ -87,7 +85,7 @@ jobs: key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('go.sum') }} - if: steps.cache-terraform-plugin-dir.outputs.cache-hit != 'true' || steps.cache-terraform-plugin-dir.outcome == 'failure' name: go build - run: go build -o terraform-plugin-dir/terraform-provider-aws_v99.99.99_x4 . + run: go build -o terraform-plugin-dir/registry.terraform.io/hashicorp/aws/99.99.99/$(go env GOOS)_$(go env GOARCH)/terraform-provider-aws . terraform_providers_schema: name: terraform providers schema @@ -276,32 +274,6 @@ jobs: - run: cd tools && go install github.com/pavius/impi/cmd/impi - run: impi --local . --scheme stdThirdPartyLocal ./... - goreleaser: - needs: [go_mod_download] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - # See also: https://github.com/actions/setup-go/pull/62 - - run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV - - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - uses: actions/cache@v2 - continue-on-error: true - timeout-minutes: 2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('go.sum') }} - - name: goreleaser check - continue-on-error: true - uses: goreleaser/goreleaser-action@v2 - with: - args: check - - name: goreleaser build - uses: goreleaser/goreleaser-action@v2 - with: - args: build --snapshot --timeout 2h - semgrep: runs-on: ubuntu-latest steps: @@ -337,9 +309,9 @@ jobs: run: | tfproviderdocs check \ -allowed-resource-subcategories-file website/allowed-subcategories.txt \ + -enable-contents-check \ -ignore-file-missing-data-sources aws_alb,aws_alb_listener,aws_alb_target_group \ -ignore-file-missing-resources aws_alb,aws_alb_listener,aws_alb_listener_certificate,aws_alb_listener_rule,aws_alb_target_group,aws_alb_target_group_attachment \ - -ignore-side-navigation-data-sources aws_alb,aws_alb_listener,aws_alb_target_group,aws_kms_secret \ - -provider-name aws \ + -provider-source registry.terraform.io/hashicorp/aws \ -providers-schema-json terraform-providers-schema/schema.json \ -require-resource-subcategory diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 2de93dd19afa..9ac7edc19929 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -6,19 +6,17 @@ on: push: branches: - main - - 'release/**' + - "release/**" pull_request: paths: - .github/workflows/website.yml - .go-version - .markdownlinkcheck.json - .markdownlint.yml + - .tflint.hcl - website/docs/** - tools/go.mod -env: - GO111MODULE: on - jobs: markdown-link-check: runs-on: ubuntu-latest @@ -27,27 +25,27 @@ jobs: - uses: gaurav-nelson/github-action-markdown-link-check@v1 name: markdown-link-check website/docs/**/*.markdown with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' - config-file: '.markdownlinkcheck.json' - folder-path: 'website/docs' - file-extension: '.markdown' + use-quiet-mode: "yes" + use-verbose-mode: "yes" + config-file: ".markdownlinkcheck.json" + folder-path: "website/docs" + file-extension: ".markdown" - uses: gaurav-nelson/github-action-markdown-link-check@v1 name: markdown-link-check website/docs/**/*.md with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' - config-file: '.markdownlinkcheck.json' - folder-path: 'website/docs' - file-extension: '.md' + use-quiet-mode: "yes" + use-verbose-mode: "yes" + config-file: ".markdownlinkcheck.json" + folder-path: "website/docs" + file-extension: ".md" markdown-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: avto-dev/markdown-lint@v1 with: - config: '.markdownlint.yml' - args: 'website/docs' + config: ".markdownlint.yml" + args: "website/docs" misspell: runs-on: ubuntu-latest steps: @@ -98,7 +96,17 @@ jobs: path: ~/go/pkg/mod key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('go.sum') }} - run: cd tools && go install github.com/katbyte/terrafmt + - run: cd tools && go install github.com/terraform-linters/tflint + + - uses: actions/cache@v2 + name: Cache plugin dir + with: + path: ~/.tflint.d/plugins + key: ${{ matrix.os }}-tflint-${{ hashFiles('.tflint.hcl') }} + + - run: tflint --init + - run: | exit_code=0 @@ -109,9 +117,10 @@ jobs: shared_rules=( "--enable-rule=terraform_comment_syntax" "--disable-rule=aws_cloudwatch_event_target_invalid_arn" - "--disable-rule=aws_cognito_user_pool_domain_invalid_domain" "--disable-rule=aws_db_instance_default_parameter_group" "--disable-rule=aws_elasticache_cluster_default_parameter_group" + "--disable-rule=aws_elasticache_replication_group_default_parameter_group" + "--disable-rule=aws_iam_policy_sid_invalid_characters" "--disable-rule=aws_iam_saml_provider_invalid_saml_metadata_document" "--disable-rule=aws_iam_server_certificate_invalid_certificate_body" "--disable-rule=aws_iam_server_certificate_invalid_private_key" @@ -143,7 +152,7 @@ jobs: "--enable-rule=terraform_deprecated_index" ) fi - # echo "Let's go with $filename..." + # We need to capture the output and error code here. We don't want to exit on the first error set +e ./scripts/validate-terraform-file.sh "$filename" "${rules[@]}" diff --git a/.gitignore b/.gitignore index 1606a7811be0..851311b9b86a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ website/node_modules log.txt markdown-link-check*.txt changelog.tmp +terraform-provider-aws +aws/testdata/service/cloudformation/examplecompany-exampleservice-exampleresource/bin/handler # Test exclusions !command/test-fixtures/**/*.tfstate diff --git a/.go-version b/.go-version index d32434904bcb..15b989e398fc 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.15.5 +1.16.0 diff --git a/.golangci.yml b/.golangci.yml index 8a6b2d0e9fd7..3d5b7165184b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,11 +18,13 @@ linters: - deadcode - errcheck - gofmt + - gomnd - gosimple - ineffassign - makezero - misspell - nakedret + - nilerr - staticcheck - structcheck - unconvert @@ -34,6 +36,156 @@ linters: linters-settings: errcheck: ignore: github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema:ForceNew|Set,fmt:.*,io:Close + gomnd: + settings: + mnd: + checks: + - argument + ignored-files: + # Needing constants, comment ignores, switching to customizable timeouts, or retries moved to aws/config.go + - awserr.go + - data_source_aws_cognito_user_pools.go + - data_source_aws_lakeformation_permissions.go + - iam_policy_model.go + - resource_aws_acmpca_certificate_authority.go + - resource_aws_api_gateway_base_path_mapping.go + - resource_aws_appautoscaling_policy.go + - resource_aws_appautoscaling_scheduled_action.go + - resource_aws_appautoscaling_target.go + - resource_aws_autoscaling_lifecycle_hook.go + - resource_aws_backup_plan.go + - resource_aws_cloud9_environment_ec2.go + - resource_aws_cloudfront_distribution.go + - resource_aws_cloudhsm2_cluster.go + - resource_aws_cloudhsm2_hsm.go + - resource_aws_cloudwatch_event_rule.go + - resource_aws_cloudwatch_event_target.go + - resource_aws_cloudwatch_log_destination.go + - resource_aws_cloudwatch_log_stream.go + - resource_aws_cloudwatch_log_subscription_filter.go + - resource_aws_codebuild_project.go + - resource_aws_codedeploy_deployment_group.go + - resource_aws_codepipeline_webhook.go + - resource_aws_config_config_rule.go + - resource_aws_config_delivery_channel.go + - resource_aws_customer_gateway.go + - resource_aws_datapipeline_pipeline.go + - resource_aws_dax_cluster.go + - resource_aws_db_instance.go + - resource_aws_db_parameter_group.go + - resource_aws_dms_endpoint.go + - resource_aws_docdb_cluster.go + - resource_aws_docdb_cluster_parameter_group.go + - resource_aws_docdb_subnet_group.go + - resource_aws_dynamodb_table.go + - resource_aws_ebs_snapshot_copy.go + - resource_aws_ebs_volume.go + - resource_aws_ec2_transit_gateway.go + - resource_aws_ecs_cluster.go + - resource_aws_ecs_service.go + - resource_aws_efs_file_system.go + - resource_aws_efs_mount_target.go + - resource_aws_elastic_beanstalk_application.go + - resource_aws_elasticache_cluster.go + - resource_aws_elasticache_parameter_group.go + - resource_aws_elasticache_replication_group.go + - resource_aws_elasticache_security_group.go + - resource_aws_elasticache_subnet_group.go + - resource_aws_elasticsearch_domain.go + - resource_aws_elasticsearch_domain_policy.go + - resource_aws_elasticsearch_domain_saml_options.go + - resource_aws_elb.go + - resource_aws_elb_attachment.go + - resource_aws_emr_cluster.go + - resource_aws_gamelift_build.go + - resource_aws_gamelift_fleet.go + - resource_aws_glue_dev_endpoint.go + - resource_aws_iam_access_key.go + - resource_aws_iam_server_certificate.go + - resource_aws_inspector_assessment_target.go + - resource_aws_instance.go + - resource_aws_internet_gateway.go + - resource_aws_iot_thing_type.go + - resource_aws_kms_external_key.go + - resource_aws_kms_grant.go + - resource_aws_kms_key.go + - resource_aws_lakeformation_data_lake_settings.go + - resource_aws_lakeformation_permissions.go + - resource_aws_lambda_event_source_mapping.go + - resource_aws_lambda_function_event_invoke_config.go + - resource_aws_lambda_permission.go + - resource_aws_lb_listener.go + - resource_aws_lb_listener_rule.go + - resource_aws_lb_ssl_negotiation_policy.go + - resource_aws_lb_target_group_attachment.go + - resource_aws_macie2_account.go + - resource_aws_macie2_account.go + - resource_aws_macie2_classification_job.go + - resource_aws_macie2_custom_data_identifier.go + - resource_aws_macie2_findings_filter.go + - resource_aws_macie2_member.go + - resource_aws_macie2_organization_admin_account.go + - resource_aws_media_package_channel.go + - resource_aws_media_store_container.go + - resource_aws_msk_cluster.go + - resource_aws_neptune_cluster.go + - resource_aws_neptune_parameter_group.go + - resource_aws_network_acl.go + - resource_aws_network_acl_rule.go + - resource_aws_opsworks_stack.go + - resource_aws_organizations_account.go + - resource_aws_organizations_organizational_unit.go + - resource_aws_organizations_policy.go + - resource_aws_organizations_policy_attachment.go + - resource_aws_qldb_ledger.go + - resource_aws_ram_resource_share_accepter.go + - resource_aws_rds_cluster.go + - resource_aws_rds_cluster_parameter_group.go + - resource_aws_redshift_cluster.go + - resource_aws_redshift_snapshot_copy_grant.go + - resource_aws_redshift_snapshot_schedule.go + - resource_aws_redshift_snapshot_schedule_association.go + - resource_aws_route53_record.go + - resource_aws_route53_zone.go + - resource_aws_route_table.go + - resource_aws_route_table_association.go + - resource_aws_s3_bucket.go + - resource_aws_s3_bucket_notification.go + - resource_aws_sagemaker_model.go + - resource_aws_sagemaker_notebook_instance.go + - resource_aws_sagemaker_workteam.go + - resource_aws_security_group_rule.go + - resource_aws_sfn_state_machine.go + - resource_aws_sqs_queue.go + - resource_aws_storagegateway_cached_iscsi_volume.go + - resource_aws_storagegateway_stored_iscsi_volume.go + - resource_aws_transfer_server.go + - resource_aws_vpc.go + - resource_aws_vpc_dhcp_options.go + - resource_aws_vpc_peering_connection_options.go + - resource_aws_vpn_gateway.go + - resource_aws_wafregional_web_acl_association.go + - resource_aws_wafv2_ip_set.go + - resource_aws_wafv2_regex_pattern_set.go + - resource_aws_wafv2_rule_group.go + - resource_aws_wafv2_web_acl.go + - tls.go + - waf_token_handlers.go + - wafregional_token_handlers.go + ignored-functions: + # AWS Go SDK + - aws.Int64 + - nullable.* + - request.ConstantWaiterDelay + - request.WithWaiterMaxAttempts + # Terraform Plugin SDK + - schema.DefaultTimeout + - strconv.FormatFloat + - strconv.FormatInt + - strconv.ParseFloat + - strconv.ParseInt + - strings.SplitN + - validation.* run: timeout: 10m diff --git a/.hashibot.hcl b/.hashibot.hcl deleted file mode 100644 index 47e5cfe3a26b..000000000000 --- a/.hashibot.hcl +++ /dev/null @@ -1,1622 +0,0 @@ -poll "closed_issue_locker" "locker" { - schedule = "0 10 17 * * *" - closed_for = "720h" # 30 days - max_issues = 500 - sleep_between_issues = "5s" - - message = <<-EOF - I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. - - If you feel this issue should be reopened, we encourage creating a new issue linking back to this one for added context. Thanks! - EOF -} - -behavior "deprecated_import_commenter" "hashicorp_terraform" { - import_regexp = "github.com/hashicorp/terraform/" - marker_label = "terraform-plugin-sdk-migration" - - message = <<-EOF - Hello, and thank you for your contribution! - - This project recently migrated to the [standalone Terraform Plugin SDK](https://www.terraform.io/docs/extend/plugin-sdk.html). While the migration helps speed up future feature requests and bug fixes to the Terraform AWS Provider's interface with Terraform, it has the unfortunate consequence of requiring minor changes to pull requests created using the old SDK. - - This pull request appears to include the Go import path `${var.import_path}`, which was from the older SDK. The newer SDK uses import paths beginning with `github.com/hashicorp/terraform-plugin-sdk/`. - - To resolve this situation without losing any existing work, you may be able to Git rebase your branch against the current default (main) branch (example below); replacing any remaining old import paths with the newer ones. - - ```console - $ git fetch --all - $ git rebase origin/main - ``` - - Another option is to create a new branch from the current default (main) with the same code changes (replacing the import paths), submit a new pull request, and close this existing pull request. - - We apologize for this inconvenience and appreciate your effort. Thank you for contributing and helping make the Terraform AWS Provider better for everyone. - EOF -} - -behavior "deprecated_import_commenter" "sdkv1" { - import_regexp = "github.com/hashicorp/terraform-plugin-sdk/(helper/(acctest|customdiff|logging|resource|schema|structure|validation)|terraform)" - marker_label = "terraform-plugin-sdk-v1" - - message = <<-EOF - Hello, and thank you for your contribution! - - This project recently upgraded to [V2 of the Terraform Plugin SDK](https://www.terraform.io/docs/extend/guides/v2-upgrade-guide.html) - - This pull request appears to include at least one V1 import path of the SDK (`${var.import_path}`). Please import the V2 path `github.com/hashicorp/terraform-plugin-sdk/v2/helper/PACKAGE` - - To resolve this situation without losing any existing work, you may be able to Git rebase your branch against the current default (main) branch (example below); replacing any remaining old import paths with the newer ones. - - ```console - $ git fetch --all - $ git rebase origin/main - ``` - - Another option is to create a new branch from the current default (main) with the same code changes (replacing the import paths), submit a new pull request, and close this existing pull request. - - We apologize for this inconvenience and appreciate your effort. Thank you for contributing and helping make the Terraform AWS Provider better for everyone. - EOF -} - -behavior "deprecated_import_commenter" "sdkv1_deprecated" { - import_regexp = "github.com/hashicorp/terraform-plugin-sdk/helper/(hashcode|mutexkv|encryption)" - marker_label = "terraform-plugin-sdk-v1" - - message = <<-EOF - Hello, and thank you for your contribution! - This pull request appears to include the Go import path `${var.import_path}`, which was deprecated after upgrading to [V2 of the Terraform Plugin SDK](https://www.terraform.io/docs/extend/guides/v2-upgrade-guide.html). - You may use a now internalized version of the package found in `github.com/terraform-providers/terraform-provider-aws/aws/internal/PACKAGE`. - EOF -} - -queued_behavior "release_commenter" "releases" { - repo_prefix = "terraform-provider-" - - message = <<-EOF - This has been released in [version ${var.release_version} of the Terraform AWS provider](${var.changelog_link}). Please see the [Terraform documentation on provider versioning](https://www.terraform.io/docs/configuration/providers.html#provider-versions) or reach out if you need any assistance upgrading. - - For further feature requests or bug reports with this functionality, please create a [new GitHub issue](https://github.com/hashicorp/terraform-provider-aws/issues/new/choose) following the template for triage. Thanks! - EOF -} - -# Catch the following in issues: -# *aws_XXX -# * aws_XXX -# * `aws_XXX` -# -aws_XXX -# - aws_XXX -# - `aws_XXX` -# data "aws_XXX" -# resource "aws_XXX" -# NOTE: Go regexp does not support negative lookaheads -behavior "regexp_issue_labeler_v2" "service_labels" { - regexp = "(\\* ?`?|- ?`?|data \"|resource \")aws_(\\w+)" - - label_map = { - "service/accessanalyzer" = [ - "aws_accessanalyzer_", - ], - "service/acm" = [ - "aws_acm_", - ], - "service/acmpca" = [ - "aws_acmpca_", - ], - "service/alexaforbusiness" = [ - "aws_alexaforbusiness_", - ], - "service/amplify" = [ - "aws_amplify_", - ], - "service/apigateway" = [ - # Catch aws_api_gateway_XXX but not aws_api_gateway_v2_ - "aws_api_gateway_([^v]|v[^2]|v2[^_])", - ], - "service/apigatewayv2" = [ - "aws_api_gateway_v2_", - "aws_apigatewayv2_", - ], - "service/applicationautoscaling" = [ - "aws_appautoscaling_", - ], - "service/applicationdiscoveryservice" = [ - "aws_applicationdiscoveryservice_", - ], - "service/applicationinsights" = [ - "aws_applicationinsights_", - ], - "service/appmesh" = [ - "aws_appmesh_", - ], - "service/appstream" = [ - "aws_appstream_", - ], - "service/appsync" = [ - "aws_appsync_", - ], - "service/athena" = [ - "aws_athena_", - ], - "service/autoscaling" = [ - "aws_autoscaling_", - "aws_launch_configuration", - ], - "service/autoscalingplans" = [ - "aws_autoscalingplans_", - ], - "service/backup" = [ - "aws_backup_", - ], - "service/batch" = [ - "aws_batch_", - ], - "service/budgets" = [ - "aws_budgets_", - ], - "service/cloud9" = [ - "aws_cloud9_", - ], - "service/clouddirectory" = [ - "aws_clouddirectory_", - ], - "service/cloudformation" = [ - "aws_cloudformation_", - ], - "service/cloudfront" = [ - "aws_cloudfront_", - ], - "service/cloudhsmv2" = [ - "aws_cloudhsm_v2_", - ], - "service/cloudsearch" = [ - "aws_cloudsearch_", - ], - "service/cloudtrail" = [ - "aws_cloudtrail", - ], - "service/cloudwatch" = [ - "aws_cloudwatch_([^e]|e[^v]|ev[^e]|eve[^n]|even[^t]|event[^_]|[^l]|l[^o]|lo[^g]|log[^_])", - ], - "service/cloudwatchevents" = [ - "aws_cloudwatch_event_", - ], - "service/cloudwatchlogs" = [ - "aws_cloudwatch_log_", - ], - "service/codeartifact" = [ - "aws_codeartifact_", - ], - "service/codebuild" = [ - "aws_codebuild_", - ], - "service/codecommit" = [ - "aws_codecommit_", - ], - "service/codedeploy" = [ - "aws_codedeploy_", - ], - "service/codepipeline" = [ - "aws_codepipeline", - ], - "service/codestar" = [ - "aws_codestar_", - ], - "service/codestarconnections" = [ - "aws_codestarconnections_", - ], - "service/codestarnotifications" = [ - "aws_codestarnotifications_", - ], - "service/cognito" = [ - "aws_cognito_", - ], - "service/configservice" = [ - "aws_config_", - ], - "service/connect" = [ - "aws_connect_", - ], - "service/databasemigrationservice" = [ - "aws_dms_", - ], - "service/dataexchange" = [ - "aws_dataexchange_", - ], - "service/datapipeline" = [ - "aws_datapipeline_", - ], - "service/datasync" = [ - "aws_datasync_", - ], - "service/dax" = [ - "aws_dax_", - ], - "service/devicefarm" = [ - "aws_devicefarm_", - ], - "service/directconnect" = [ - "aws_dx_", - ], - "service/directoryservice" = [ - "aws_directory_service_", - ], - "service/dlm" = [ - "aws_dlm_", - ], - "service/docdb" = [ - "aws_docdb_", - ], - "service/dynamodb" = [ - "aws_dynamodb_", - ], - "service/ec2" = [ - "aws_ami", - "aws_availability_zone", - "aws_customer_gateway", - "aws_(default_)?(network_acl|route_table|security_group|subnet|vpc)", - "aws_ebs_", - "aws_ec2_", - "aws_egress_only_internet_gateway", - "aws_eip", - "aws_flow_log", - "aws_instance", - "aws_internet_gateway", - "aws_key_pair", - "aws_launch_template", - "aws_main_route_table_association", - "aws_network_interface", - "aws_placement_group", - "aws_prefix_list", - "aws_spot", - "aws_route(\"|`|$)", - "aws_vpn_", - "aws_volume_attachment", - ], - "service/ecr" = [ - "aws_ecr_", - ], - "service/ecrpublic" = [ - "aws_ecrpublic_", - ], - "service/ecs" = [ - "aws_ecs_", - ], - "service/efs" = [ - "aws_efs_", - ], - "service/eks" = [ - "aws_eks_", - ], - "service/elastic-transcoder" = [ - "aws_elastictranscoder_", - ], - "service/elasticache" = [ - "aws_elasticache_", - ], - "service/elasticbeanstalk" = [ - "aws_elastic_beanstalk_", - ], - "service/elasticsearch" = [ - "aws_elasticsearch_", - ], - "service/elb" = [ - "aws_app_cookie_stickiness_policy", - "aws_elb", - "aws_lb_cookie_stickiness_policy", - "aws_lb_ssl_negotiation_policy", - "aws_load_balancer_", - "aws_proxy_protocol_policy", - ], - "service/elbv2" = [ - "aws_(a)?lb(\"|`|$)", - # Catch aws_lb_XXX but not aws_lb_cookie_ or aws_lb_ssl_ (Classic ELB) - "aws_(a)?lb_([^c]|c[^o]|co[^o]|coo[^k]|cook[^i]|cooki[^e]|cookie[^_]|[^s]|s[^s]|ss[^l]|ssl[^_])", - ], - "service/emr" = [ - "aws_emr_", - ], - "service/emrcontainers" = [ - "aws_emrcontainers_", - ], - "service/eventbridge" = [ - # EventBridge is rebranded CloudWatch Events - "aws_cloudwatch_event_", - ], - "service/firehose" = [ - "aws_kinesis_firehose_", - ], - "service/fms" = [ - "aws_fms_", - ], - "service/forecast" = [ - "aws_forecast_", - ], - "service/fsx" = [ - "aws_fsx_", - ], - "service/gamelift" = [ - "aws_gamelift_", - ], - "service/glacier" = [ - "aws_glacier_", - ], - "service/globalaccelerator" = [ - "aws_globalaccelerator_", - ], - "service/glue" = [ - "aws_glue_", - ], - "service/greengrass" = [ - "aws_greengrass_", - ], - "service/guardduty" = [ - "aws_guardduty_", - ], - "service/iam" = [ - "aws_iam_", - ], - "service/identitystore" = [ - "aws_identitystore_", - ], - "service/imagebuilder" = [ - "aws_imagebuilder_", - ], - "service/inspector" = [ - "aws_inspector_", - ], - "service/iot" = [ - "aws_iot_", - ], - "service/iotanalytics" = [ - "aws_iotanalytics_", - ], - "service/iotevents" = [ - "aws_iotevents_", - ], - "service/kafka" = [ - "aws_msk_", - ], - "service/kinesis" = [ - # Catch aws_kinesis_XXX but not aws_kinesis_firehose_ - "aws_kinesis_([^f]|f[^i]|fi[^r]|fir[^e]|fire[^h]|fireh[^o]|fireho[^s]|firehos[^e]|firehose[^_])", - ], - "service/kinesisanalytics" = [ - "aws_kinesis_analytics_", - ], - "service/kinesisanalyticsv2" = [ - "aws_kinesisanalyticsv2_", - ], - "service/kms" = [ - "aws_kms_", - ], - "service/lakeformation" = [ - "aws_lakeformation_", - ], - "service/lambda" = [ - "aws_lambda_", - ], - "service/lexmodelbuildingservice" = [ - "aws_lex_", - ], - "service/licensemanager" = [ - "aws_licensemanager_", - ], - "service/lightsail" = [ - "aws_lightsail_", - ], - "service/machinelearning" = [ - "aws_machinelearning_", - ], - "service/macie" = [ - "aws_macie_", - ], - "service/macie2" = [ - "aws_macie2_", - ], - "service/marketplacecatalog" = [ - "aws_marketplace_catalog_", - ], - "service/mediaconnect" = [ - "aws_media_connect_", - ], - "service/mediaconvert" = [ - "aws_media_convert_", - ], - "service/medialive" = [ - "aws_media_live_", - ], - "service/mediapackage" = [ - "aws_media_package_", - ], - "service/mediastore" = [ - "aws_media_store_", - ], - "service/mediatailor" = [ - "aws_media_tailor_", - ], - "service/mobile" = [ - "aws_mobile_", - ], - "service/mq" = [ - "aws_mq_", - ], - "service/mwaa" = [ - "aws_mwaa_", - ], - "service/neptune" = [ - "aws_neptune_", - ], - "service/networkfirewall" = [ - "aws_networkfirewall_", - ], - "service/networkmanager" = [ - "aws_networkmanager_", - ], - "service/opsworks" = [ - "aws_opsworks_", - ], - "service/organizations" = [ - "aws_organizations_", - ], - "service/outposts" = [ - "aws_outposts_", - ], - "service/personalize" = [ - "aws_personalize_", - ], - "service/pinpoint" = [ - "aws_pinpoint_", - ], - "service/polly" = [ - "aws_polly_", - ], - "service/pricing" = [ - "aws_pricing_", - ], - "service/prometheusservice" = [ - "aws_prometheus_", - ], - "service/qldb" = [ - "aws_qldb_", - ], - "service/quicksight" = [ - "aws_quicksight_", - ], - "service/ram" = [ - "aws_ram_", - ], - "service/rds" = [ - "aws_db_", - "aws_rds_", - ], - "service/redshift" = [ - "aws_redshift_", - ], - "service/resourcegroups" = [ - "aws_resourcegroups_", - ], - "service/resourcegroupstaggingapi" = [ - "aws_resourcegroupstaggingapi_", - ], - "service/robomaker" = [ - "aws_robomaker_", - ], - "service/route53" = [ - # Catch aws_route53_XXX but not aws_route53_domains_ or aws_route53_resolver_ - "aws_route53_([^d]|d[^o]|do[^m]|dom[^a]|doma[^i]|domai[^n]|domain[^s]|domains[^_]|[^r]|r[^e]|re[^s]|res[^o]|reso[^l]|resol[^v]|resolv[^e]|resolve[^r]|resolver[^_])", - ], - "service/route53domains" = [ - "aws_route53domains_", - ], - "service/route53resolver" = [ - "aws_route53_resolver_", - ], - "service/s3" = [ - "aws_canonical_user_id", - "aws_s3_bucket", - ], - "service/s3control" = [ - "aws_s3_account_", - "aws_s3control_", - ], - "service/s3outposts" = [ - "aws_s3outposts_", - ], - "service/sagemaker" = [ - "aws_sagemaker_", - ], - "service/secretsmanager" = [ - "aws_secretsmanager_", - ], - "service/securityhub" = [ - "aws_securityhub_", - ], - "service/serverlessapplicationrepository" = [ - "aws_serverlessapplicationrepository_", - ], - "service/servicecatalog" = [ - "aws_servicecatalog_", - ], - "service/servicediscovery" = [ - "aws_service_discovery_", - ], - "service/servicequotas" = [ - "aws_servicequotas_", - ], - "service/ses" = [ - "aws_ses_", - ], - "service/sfn" = [ - "aws_sfn_", - ], - "service/shield" = [ - "aws_shield_", - ], - "service/signer" = [ - "aws_signer_", - ], - "service/simpledb" = [ - "aws_simpledb_", - ], - "service/snowball" = [ - "aws_snowball_", - ], - "service/sns" = [ - "aws_sns_", - ], - "service/sqs" = [ - "aws_sqs_", - ], - "service/ssm" = [ - "aws_ssm_", - ], - "service/ssoadmin" = [ - "aws_ssoadmin_", - ], - "service/storagegateway" = [ - "aws_storagegateway_", - ], - "service/sts" = [ - "aws_caller_identity", - ], - "service/swf" = [ - "aws_swf_", - ], - "service/synthetics" = [ - "aws_synthetics_", - ], - "service/timestreamwrite" = [ - "aws_timestreamwrite_", - ], - "service/transfer" = [ - "aws_transfer_", - ], - "service/waf" = [ - "aws_waf_", - "aws_wafregional_", - ], - "service/wafv2" = [ - "aws_wafv2_", - ], - "service/workdocs" = [ - "aws_workdocs_", - ], - "service/worklink" = [ - "aws_worklink_", - ], - "service/workmail" = [ - "aws_workmail_", - ], - "service/workspaces" = [ - "aws_workspaces_", - ], - "service/xray" = [ - "aws_xray_", - ], - } -} - -behavior "pull_request_path_labeler" "service_labels" { - label_map = { - # label provider related changes - "provider" = [ - "*.md", - ".github/**/*", - ".gitignore", - ".go-version", - ".hashibot.hcl", - "aws/auth_helpers.go", - "aws/awserr.go", - "aws/config.go", - "aws/*_aws_arn*", - "aws/*_aws_ip_ranges*", - "aws/*_aws_partition*", - "aws/*_aws_region*", - "aws/internal/flatmap/*", - "aws/internal/keyvaluetags/*", - "aws/internal/naming/*", - "aws/provider.go", - "aws/utils.go", - "docs/*.md", - "docs/contributing/**/*", - "GNUmakefile", - "infrastructure/**/*", - "main.go", - "website/docs/index.html.markdown", - "website/**/arn*", - "website/**/ip_ranges*", - "website/**/partition*", - "website/**/region*" - ] - "dependencies" = [ - ".github/dependabot.yml", - ] - "documentation" = [ - "docs/**/*", - "website/**/*", - "*.md", - ] - "examples" = [ - "examples/**/*", - ] - "tests" = [ - "**/*_test.go", - "**/testdata/**/*", - "**/test-fixtures/**/*", - ".github/workflows/*", - ".gometalinter.json", - ".markdownlinkcheck.json", - ".markdownlint.yml", - "staticcheck.conf" - ] - # label services - "service/accessanalyzer" = [ - "aws/internal/service/accessanalyzer/**/*", - "**/*_accessanalyzer_*", - "**/accessanalyzer_*" - ] - "service/acm" = [ - "aws/internal/service/acm/**/*", - "**/*_acm_*", - "**/acm_*" - ] - "service/acmpca" = [ - "aws/internal/service/acmpca/**/*", - "**/*_acmpca_*", - "**/acmpca_*" - ] - "service/alexaforbusiness" = [ - "aws/internal/service/alexaforbusiness/**/*", - "**/*_alexaforbusiness_*", - "**/alexaforbusiness_*" - ] - "service/amplify" = [ - "aws/internal/service/amplify/**/*", - "**/*_amplify_*", - "**/amplify_*" - ] - "service/apigateway" = [ - "aws/internal/service/apigateway/**/*", - "**/*_api_gateway_[^v][^2][^_]*", - "**/*_api_gateway_vpc_link*", - "**/api_gateway_[^v][^2][^_]*", - "**/api_gateway_vpc_link*" - ] - "service/apigatewayv2" = [ - "aws/internal/service/apigatewayv2/**/*", - "**/*_api_gateway_v2_*", - "**/*_apigatewayv2_*", - "**/api_gateway_v2_*", - "**/apigatewayv2_*" - ] - "service/applicationautoscaling" = [ - "aws/internal/service/applicationautoscaling/**/*", - "**/*_appautoscaling_*", - "**/appautoscaling_*" - ] - "service/applicationinsights" = [ - "aws/internal/service/applicationinsights/**/*", - "**/*_applicationinsights_*", - "**/applicationinsights_*" - ] - "service/appmesh" = [ - "aws/internal/service/appmesh/**/*", - "**/*_appmesh_*", - "**/appmesh_*" - ] - "service/appstream" = [ - "aws/internal/service/appstream/**/*", - "**/*_appstream_*", - "**/appstream_*" - ] - "service/appsync" = [ - "aws/internal/service/appsync/**/*", - "**/*_appsync_*", - "**/appsync_*" - ] - "service/athena" = [ - "aws/internal/service/athena/**/*", - "**/*_athena_*", - "**/athena_*" - ] - "service/autoscaling" = [ - "aws/internal/service/autoscaling/**/*", - "**/*_autoscaling_*", - "**/autoscaling_*", - "aws/*_aws_launch_configuration*", - "website/**/launch_configuration*" - ] - "service/autoscalingplans" = [ - "aws/internal/service/autoscalingplans/**/*", - "**/*_autoscalingplans_*", - "**/autoscalingplans_*" - ] - "service/backup" = [ - "aws/internal/service/backup/**/*", - "**/*backup_*", - "**/backup_*" - ] - "service/batch" = [ - "aws/internal/service/batch/**/*", - "**/*_batch_*", - "**/batch_*" - ] - "service/budgets" = [ - "aws/internal/service/budgets/**/*", - "**/*_budgets_*", - "**/budgets_*" - ] - "service/cloud9" = [ - "aws/internal/service/cloud9/**/*", - "**/*_cloud9_*", - "**/cloud9_*" - ] - "service/clouddirectory" = [ - "aws/internal/service/clouddirectory/**/*", - "**/*_clouddirectory_*", - "**/clouddirectory_*" - ] - "service/cloudformation" = [ - "aws/internal/service/cloudformation/**/*", - "**/*_cloudformation_*", - "**/cloudformation_*" - ] - "service/cloudfront" = [ - "aws/internal/service/cloudfront/**/*", - "**/*_cloudfront_*", - "**/cloudfront_*" - ] - "service/cloudhsmv2" = [ - "aws/internal/service/cloudhsmv2/**/*", - "**/*_cloudhsm_v2_*", - "**/cloudhsm_v2_*" - ] - "service/cloudsearch" = [ - "aws/internal/service/cloudsearch/**/*", - "**/*_cloudsearch_*", - "**/cloudsearch_*" - ] - "service/cloudtrail" = [ - "aws/internal/service/cloudtrail/**/*", - "**/*_cloudtrail*", - "**/cloudtrail*" - ] - "service/cloudwatch" = [ - "aws/internal/service/cloudwatch/**/*", - "**/*_cloudwatch_dashboard*", - "**/*_cloudwatch_metric_alarm*", - "**/cloudwatch_dashboard*", - "**/cloudwatch_metric_alarm*" - ] - "service/cloudwatchevents" = [ - "aws/internal/service/cloudwatchevents/**/*", - "**/*_cloudwatch_event_*", - "**/cloudwatch_event_*" - ] - "service/cloudwatchlogs" = [ - "aws/internal/service/cloudwatchlogs/**/*", - "**/*_cloudwatch_log_*", - "**/cloudwatch_log_*" - ] - "service/codeartifact" = [ - "aws/internal/service/codeartifact/**/*", - "**/*_codeartifact_*", - "**/codeartifact_*" - ] - "service/codebuild" = [ - "aws/internal/service/codebuild/**/*", - "**/*_codebuild_*", - "**/codebuild_*" - ] - "service/codecommit" = [ - "aws/internal/service/codecommit/**/*", - "**/*_codecommit_*", - "**/codecommit_*" - ] - "service/codedeploy" = [ - "aws/internal/service/codedeploy/**/*", - "**/*_codedeploy_*", - "**/codedeploy_*" - ] - "service/codepipeline" = [ - "aws/internal/service/codepipeline/**/*", - "**/*_codepipeline_*", - "**/codepipeline_*" - ] - "service/codestar" = [ - "aws/internal/service/codestar/**/*", - "**/*_codestar_*", - "**/codestar_*" - ] - "service/codestarconnections" = [ - "aws/internal/service/codestarconnections/**/*", - "**/*_codestarconnections_*", - "**/codestarconnections_*" - ] - "service/codestarnotifications" = [ - "aws/internal/service/codestarnotifications/**/*", - "**/*_codestarnotifications_*", - "**/codestarnotifications_*" - ] - "service/cognito" = [ - "aws/internal/service/cognitoidentity/**/*", - "aws/internal/service/cognitoidentityprovider/**/*", - "**/*_cognito_*", - "**/cognito_*" - ] - "service/comprehend" = [ - "aws/internal/service/comprehend/**/*", - "**/*_comprehend_*", - "**/comprehend_*" - ] - "service/configservice" = [ - "aws/internal/service/configservice/**/*", - "aws/*_aws_config_*", - "website/**/config_*" - ] - "service/connect" = [ - "aws/internal/service/connect/**/*", - "aws/*_aws_connect_*", - "website/**/connect_*" - ] - "service/costandusagereportservice" = [ - "aws/internal/service/costandusagereportservice/**/*", - "aws/*_aws_cur_*", - "website/**/cur_*" - ] - "service/databasemigrationservice" = [ - "aws/internal/service/databasemigrationservice/**/*", - "**/*_dms_*", - "**/dms_*" - ] - "service/dataexchange" = [ - "aws/internal/service/dataexchange/**/*", - "**/*_dataexchange_*", - "**/dataexchange_*", - ] - "service/datapipeline" = [ - "aws/internal/service/datapipeline/**/*", - "**/*_datapipeline_*", - "**/datapipeline_*", - ] - "service/datasync" = [ - "aws/internal/service/datasync/**/*", - "**/*_datasync_*", - "**/datasync_*", - ] - "service/dax" = [ - "aws/internal/service/dax/**/*", - "**/*_dax_*", - "**/dax_*" - ] - "service/devicefarm" = [ - "aws/internal/service/devicefarm/**/*", - "**/*_devicefarm_*", - "**/devicefarm_*" - ] - "service/directconnect" = [ - "aws/internal/service/directconnect/**/*", - "**/*_dx_*", - "**/dx_*" - ] - "service/directoryservice" = [ - "aws/internal/service/directoryservice/**/*", - "**/*_directory_service_*", - "**/directory_service_*" - ] - "service/dlm" = [ - "aws/internal/service/dlm/**/*", - "**/*_dlm_*", - "**/dlm_*" - ] - "service/docdb" = [ - "aws/internal/service/docdb/**/*", - "**/*_docdb_*", - "**/docdb_*" - ] - "service/dynamodb" = [ - "aws/internal/service/dynamodb/**/*", - "**/*_dynamodb_*", - "**/dynamodb_*" - ] - # Special casing this one because the files aren't _ec2_ - "service/ec2" = [ - "aws/internal/service/ec2/**/*", - "**/*_ec2_*", - "**/ec2_*", - "aws/*_aws_ami*", - "aws/*_aws_availability_zone*", - "aws/*_aws_customer_gateway*", - "aws/*_aws_default_network_acl*", - "aws/*_aws_default_route_table*", - "aws/*_aws_default_security_group*", - "aws/*_aws_default_subnet*", - "aws/*_aws_default_vpc*", - "aws/*_aws_ebs_*", - "aws/*_aws_egress_only_internet_gateway*", - "aws/*_aws_eip*", - "aws/*_aws_flow_log*", - "aws/*_aws_instance*", - "aws/*_aws_internet_gateway*", - "aws/*_aws_key_pair*", - "aws/*_aws_launch_template*", - "aws/*_aws_main_route_table_association*", - "aws/*_aws_nat_gateway*", - "aws/*_aws_network_acl*", - "aws/*_aws_network_interface*", - "aws/*_aws_placement_group*", - "aws/*_aws_prefix_list*", - "aws/*_aws_route_table*", - "aws/*_aws_route.*", - "aws/*_aws_security_group*", - "aws/*_aws_snapshot_create_volume_permission*", - "aws/*_aws_spot*", - "aws/*_aws_subnet*", - "aws/*_aws_vpc*", - "aws/*_aws_vpn*", - "aws/*_aws_volume_attachment*", - "website/**/availability_zone*", - "website/**/customer_gateway*", - "website/**/default_network_acl*", - "website/**/default_route_table*", - "website/**/default_security_group*", - "website/**/default_subnet*", - "website/**/default_vpc*", - "website/**/ebs_*", - "website/**/egress_only_internet_gateway*", - "website/**/eip*", - "website/**/flow_log*", - "website/**/instance*", - "website/**/internet_gateway*", - "website/**/key_pair*", - "website/**/launch_template*", - "website/**/main_route_table_association*", - "website/**/nat_gateway*", - "website/**/network_acl*", - "website/**/network_interface*", - "website/**/placement_group*", - "website/**/prefix_list*", - "website/**/route_table*", - "website/**/route.*", - "website/**/security_group*", - "website/**/snapshot_create_volume_permission*", - "website/**/spot_*", - "website/**/subnet*", - "website/**/vpc*", - "website/**/vpn*", - "website/**/volume_attachment*" - ] - "service/ecr" = [ - "aws/internal/service/ecr/**/*", - "**/*_ecr_*", - "**/ecr_*" - ] - "service/ecrpublic" = [ - "aws/internal/service/ecrpublic/**/*", - "**/*_ecrpublic_*", - "**/ecrpublic_*" - ] - "service/ecs" = [ - "aws/internal/service/ecs/**/*", - "**/*_ecs_*", - "**/ecs_*" - ] - "service/efs" = [ - "aws/internal/service/efs/**/*", - "**/*_efs_*", - "**/efs_*" - ] - "service/eks" = [ - "aws/internal/service/eks/**/*", - "**/*_eks_*", - "**/eks_*" - ] - "service/elastic-transcoder" = [ - "aws/internal/service/elastictranscoder/**/*", - "**/*_elastictranscoder_*", - "**/elastictranscoder_*", - "**/*_elastic_transcoder_*", - "**/elastic_transcoder_*" - ] - "service/elasticache" = [ - "aws/internal/service/elasticache/**/*", - "**/*_elasticache_*", - "**/elasticache_*" - ] - "service/elasticbeanstalk" = [ - "aws/internal/service/elasticbeanstalk/**/*", - "**/*_elastic_beanstalk_*", - "**/elastic_beanstalk_*" - ] - "service/elasticsearch" = [ - "aws/internal/service/elasticsearchservice/**/*", - "**/*_elasticsearch_*", - "**/elasticsearch_*", - "**/*_elasticsearchservice*" - ] - "service/elb" = [ - "aws/internal/service/elb/**/*", - "aws/*_aws_app_cookie_stickiness_policy*", - "aws/*_aws_elb*", - "aws/*_aws_lb_cookie_stickiness_policy*", - "aws/*_aws_lb_ssl_negotiation_policy*", - "aws/*_aws_load_balancer*", - "aws/*_aws_proxy_protocol_policy*", - "website/**/app_cookie_stickiness_policy*", - "website/**/elb*", - "website/**/lb_cookie_stickiness_policy*", - "website/**/lb_ssl_negotiation_policy*", - "website/**/load_balancer*", - "website/**/proxy_protocol_policy*" - ] - "service/elbv2" = [ - "aws/internal/service/elbv2/**/*", - "aws/*_lb.*", - "aws/*_lb_listener*", - "aws/*_lb_target_group*", - "website/**/lb.*", - "website/**/lb_listener*", - "website/**/lb_target_group*" - ] - "service/emr" = [ - "aws/internal/service/emr/**/*", - "**/*_emr_*", - "**/emr_*" - ] - "service/emrcontainers" = [ - "aws/internal/service/emrcontainers/**/*", - "**/*_emrcontainers_*", - "**/emrcontainers_*" - ] - "service/eventbridge" = [ - # EventBridge is rebranded CloudWatch Events - "aws/internal/service/cloudwatchevents/**/*", - "**/*_cloudwatch_event_*", - "**/cloudwatch_event_*" - ] - "service/firehose" = [ - "aws/internal/service/firehose/**/*", - "**/*_firehose_*", - "**/firehose_*" - ] - "service/fms" = [ - "aws/internal/service/fms/**/*", - "**/*_fms_*", - "**/fms_*" - ] - "service/fsx" = [ - "aws/internal/service/fsx/**/*", - "**/*_fsx_*", - "**/fsx_*" - ] - "service/gamelift" = [ - "aws/internal/service/gamelift/**/*", - "**/*_gamelift_*", - "**/gamelift_*" - ] - "service/glacier" = [ - "aws/internal/service/glacier/**/*", - "**/*_glacier_*", - "**/glacier_*" - ] - "service/globalaccelerator" = [ - "aws/internal/service/globalaccelerator/**/*", - "**/*_globalaccelerator_*", - "**/globalaccelerator_*" - ] - "service/glue" = [ - "aws/internal/service/glue/**/*", - "**/*_glue_*", - "**/glue_*" - ] - "service/greengrass" = [ - "aws/internal/service/greengrass/**/*", - "**/*_greengrass_*", - "**/greengrass_*" - ] - "service/guardduty" = [ - "aws/internal/service/guardduty/**/*", - "**/*_guardduty_*", - "**/guardduty_*" - ] - "service/iam" = [ - "aws/internal/service/iam/**/*", - "**/*_iam_*", - "**/iam_*" - ] - "service/identitystore" = [ - "aws/internal/service/identitystore/**/*", - "**/*_identitystore_*", - "**/identitystore_*" - ] - "service/imagebuilder" = [ - "aws/internal/service/imagebuilder/**/*", - "**/*_imagebuilder_*", - "**/imagebuilder_*" - ] - "service/inspector" = [ - "aws/internal/service/inspector/**/*", - "**/*_inspector_*", - "**/inspector_*" - ] - "service/iot" = [ - "aws/internal/service/iot/**/*", - "**/*_iot_*", - "**/iot_*" - ] - "service/iotanalytics" = [ - "aws/internal/service/iotanalytics/**/*", - "**/*_iotanalytics_*", - "**/iotanalytics_*" - ] - "service/iotevents" = [ - "aws/internal/service/iotevents/**/*", - "**/*_iotevents_*", - "**/iotevents_*" - ] - "service/kafka" = [ - "aws/internal/service/kafka/**/*", - "**/*_msk_*", - "**/msk_*", - ] - "service/kinesis" = [ - "aws/internal/service/kinesis/**/*", - "aws/*_aws_kinesis_stream*", - "website/kinesis_stream*" - ] - "service/kinesisanalytics" = [ - "aws/internal/service/kinesisanalytics/**/*", - "**/*_kinesis_analytics_*", - "**/kinesis_analytics_*" - ] - "service/kinesisanalyticsv2" = [ - "aws/internal/service/kinesisanalyticsv2/**/*", - "**/*_kinesisanalyticsv2_*", - "**/kinesisanalyticsv2_*" - ] - "service/kms" = [ - "aws/internal/service/kms/**/*", - "**/*_kms_*", - "**/kms_*" - ] - "service/lakeformation" = [ - "aws/internal/service/lakeformation/**/*", - "**/*_lakeformation_*", - "**/lakeformation_*" - ] - "service/lambda" = [ - "aws/internal/service/lambda/**/*", - "**/*_lambda_*", - "**/lambda_*" - ] - "service/lexmodelbuildingservice" = [ - "aws/internal/service/lexmodelbuildingservice/**/*", - "**/*_lex_*", - "**/lex_*" - ] - "service/licensemanager" = [ - "aws/internal/service/licensemanager/**/*", - "**/*_licensemanager_*", - "**/licensemanager_*" - ] - "service/lightsail" = [ - "aws/internal/service/lightsail/**/*", - "**/*_lightsail_*", - "**/lightsail_*" - ] - "service/machinelearning" = [ - "aws/internal/service/machinelearning/**/*", - "**/*_machinelearning_*", - "**/machinelearning_*" - ] - "service/macie" = [ - "aws/internal/service/macie/**/*", - "**/*_macie_*", - "**/macie_*" - ] - "service/macie2" = [ - "aws/internal/service/macie2/**/*", - "**/*_macie2_*", - "**/macie2_*" - ] - "service/marketplacecatalog" = [ - "aws/internal/service/marketplacecatalog/**/*", - "**/*_marketplace_catalog_*", - "**/marketplace_catalog_*" - ] - "service/mediaconnect" = [ - "aws/internal/service/mediaconnect/**/*", - "**/*_media_connect_*", - "**/media_connect_*" - ] - "service/mediaconvert" = [ - "aws/internal/service/mediaconvert/**/*", - "**/*_media_convert_*", - "**/media_convert_*" - ] - "service/medialive" = [ - "aws/internal/service/medialive/**/*", - "**/*_media_live_*", - "**/media_live_*" - ] - "service/mediapackage" = [ - "aws/internal/service/mediapackage/**/*", - "**/*_media_package_*", - "**/media_package_*" - ] - "service/mediastore" = [ - "aws/internal/service/mediastore/**/*", - "**/*_media_store_*", - "**/media_store_*" - ] - "service/mediatailor" = [ - "aws/internal/service/mediatailor/**/*", - "**/*_media_tailor_*", - "**/media_tailor_*", - ] - "service/mobile" = [ - "aws/internal/service/mobile/**/*", - "**/*_mobile_*", - "**/mobile_*" - ], - "service/mq" = [ - "aws/internal/service/mq/**/*", - "**/*_mq_*", - "**/mq_*" - ] - "service/mwaa" = [ - "aws/internal/service/mwaa/**/*", - "**/*_mwaa_*", - "**/mwaa_*" - ] - "service/neptune" = [ - "aws/internal/service/neptune/**/*", - "**/*_neptune_*", - "**/neptune_*" - ] - "service/networkfirewall" = [ - "aws/internal/service/networkfirewall/**/*", - "**/*_networkfirewall_*", - "**/networkfirewall_*", - ] - "service/networkmanager" = [ - "aws/internal/service/networkmanager/**/*", - "**/*_networkmanager_*", - "**/networkmanager_*" - ] - "service/opsworks" = [ - "aws/internal/service/opsworks/**/*", - "**/*_opsworks_*", - "**/opsworks_*" - ] - "service/organizations" = [ - "aws/internal/service/organizations/**/*", - "**/*_organizations_*", - "**/organizations_*" - ] - "service/outposts" = [ - "aws/internal/service/outposts/**/*", - "**/*_outposts_*", - "**/outposts_*" - ] - "service/pinpoint" = [ - "aws/internal/service/pinpoint/**/*", - "**/*_pinpoint_*", - "**/pinpoint_*" - ] - "service/polly" = [ - "aws/internal/service/polly/**/*", - "**/*_polly_*", - "**/polly_*" - ] - "service/pricing" = [ - "aws/internal/service/pricing/**/*", - "**/*_pricing_*", - "**/pricing_*" - ] - "service/prometheusservice" = [ - "aws/internal/service/prometheus/**/*", - "**/*_prometheus_*", - "**/prometheus_*", - ] - "service/qldb" = [ - "aws/internal/service/qldb/**/*", - "**/*_qldb_*", - "**/qldb_*" - ] - "service/quicksight" = [ - "aws/internal/service/quicksight/**/*", - "**/*_quicksight_*", - "**/quicksight_*" - ] - "service/ram" = [ - "aws/internal/service/ram/**/*", - "**/*_ram_*", - "**/ram_*" - ] - "service/rds" = [ - "aws/internal/service/rds/**/*", - "aws/*_aws_db_*", - "aws/*_aws_rds_*", - "website/**/db_*", - "website/**/rds_*" - ] - "service/redshift" = [ - "aws/internal/service/redshift/**/*", - "**/*_redshift_*", - "**/redshift_*" - ] - "service/resourcegroups" = [ - "aws/internal/service/resourcegroups/**/*", - "**/*_resourcegroups_*", - "**/resourcegroups_*" - ] - "service/resourcegroupstaggingapi" = [ - "aws/internal/service/resourcegroupstaggingapi/**/*", - "**/*_resourcegroupstaggingapi_*", - "**/resourcegroupstaggingapi_*" - ] - "service/robomaker" = [ - "aws/internal/service/robomaker/**/*", - "**/*_robomaker_*", - "**/robomaker_*", - ] - "service/route53" = [ - "aws/internal/service/route53/**/*", - "**/*_route53_delegation_set*", - "**/*_route53_health_check*", - "**/*_route53_query_log*", - "**/*_route53_record*", - "**/*_route53_vpc_association_authorization*", - "**/*_route53_zone*", - "**/route53_delegation_set*", - "**/route53_health_check*", - "**/route53_query_log*", - "**/route53_record*", - "**/route53_vpc_association_authorization*", - "**/route53_zone*" - ] - "service/route53domains" = [ - "aws/internal/service/route53domains/**/*", - "**/*_route53domains_*", - "**/route53domains_*" - ] - "service/route53resolver" = [ - "aws/internal/service/route53resolver/**/*", - "**/*_route53_resolver_*", - "**/route53_resolver_*" - ] - "service/s3" = [ - "aws/internal/service/s3/**/*", - "**/*_s3_bucket*", - "**/s3_bucket*", - "aws/*_aws_canonical_user_id*", - "website/**/canonical_user_id*" - ] - "service/s3control" = [ - "aws/internal/service/s3control/**/*", - "**/*_s3_account_*", - "**/s3_account_*", - "**/*_s3control_*", - "**/s3control_*" - ] - "service/s3outposts" = [ - "aws/internal/service/s3outposts/**/*", - "**/*_s3outposts_*", - "**/s3outposts_*" - ] - "service/sagemaker" = [ - "aws/internal/service/sagemaker/**/*", - "**/*_sagemaker_*", - "**/sagemaker_*" - ] - "service/secretsmanager" = [ - "aws/internal/service/secretsmanager/**/*", - "**/*_secretsmanager_*", - "**/secretsmanager_*" - ] - "service/securityhub" = [ - "aws/internal/service/securityhub/**/*", - "**/*_securityhub_*", - "**/securityhub_*" - ] - "service/serverlessapplicationrepository" = [ - "aws/internal/service/serverlessapplicationrepository/**/*", - "**/*_serverlessapplicationrepository_*", - "**/serverlessapplicationrepository_*" - ] - "service/servicecatalog" = [ - "aws/internal/service/servicecatalog/**/*", - "**/*_servicecatalog_*", - "**/servicecatalog_*" - ] - "service/servicediscovery" = [ - "aws/internal/service/servicediscovery/**/*", - "**/*_service_discovery_*", - "**/service_discovery_*" - ] - "service/servicequotas" = [ - "aws/internal/service/servicequotas/**/*", - "**/*_servicequotas_*", - "**/servicequotas_*" - ] - "service/ses" = [ - "aws/internal/service/ses/**/*", - "**/*_ses_*", - "**/ses_*" - ] - "service/sfn" = [ - "aws/internal/service/sfn/**/*", - "**/*_sfn_*", - "**/sfn_*" - ] - "service/shield" = [ - "aws/internal/service/shield/**/*", - "**/*_shield_*", - "**/shield_*", - ], - "service/signer" = [ - "**/*_signer_*", - "**/signer_*" - ] - "service/simpledb" = [ - "aws/internal/service/simpledb/**/*", - "**/*_simpledb_*", - "**/simpledb_*" - ] - "service/snowball" = [ - "aws/internal/service/snowball/**/*", - "**/*_snowball_*", - "**/snowball_*" - ] - "service/sns" = [ - "aws/internal/service/sns/**/*", - "**/*_sns_*", - "**/sns_*" - ] - "service/sqs" = [ - "aws/internal/service/sqs/**/*", - "**/*_sqs_*", - "**/sqs_*" - ] - "service/ssm" = [ - "aws/internal/service/ssm/**/*", - "**/*_ssm_*", - "**/ssm_*" - ] - "service/ssoadmin" = [ - "aws/internal/service/ssoadmin/**/*", - "**/*_ssoadmin_*", - "**/ssoadmin_*" - ] - "service/storagegateway" = [ - "aws/internal/service/storagegateway/**/*", - "**/*_storagegateway_*", - "**/storagegateway_*" - ] - "service/sts" = [ - "aws/internal/service/sts/**/*", - "aws/*_aws_caller_identity*", - "website/**/caller_identity*" - ] - "service/swf" = [ - "aws/internal/service/swf/**/*", - "**/*_swf_*", - "**/swf_*" - ] - "service/synthetics" = [ - "aws/internal/service/synthetics/**/*", - "**/*_synthetics_*", - "**/synthetics_*" - ] - "service/timestreamwrite" = [ - "aws/internal/service/timestreamwrite/**/*", - "**/*_timestreamwrite_*", - "**/timestreamwrite_*" - ] - "service/transfer" = [ - "aws/internal/service/transfer/**/*", - "**/*_transfer_*", - "**/transfer_*" - ] - "service/waf" = [ - "aws/internal/service/waf/**/*", - "aws/internal/service/wafregional/**/*", - "**/*_waf_*", - "**/waf_*", - "**/*_wafregional_*", - "**/wafregional_*" - ] - "service/wafv2" = [ - "aws/internal/service/wafv2/**/*", - "**/*_wafv2_*", - "**/wafv2_*", - ] - "service/workdocs" = [ - "aws/internal/service/workdocs/**/*", - "**/*_workdocs_*", - "**/workdocs_*" - ] - "service/worklink" = [ - "aws/internal/service/worklink/**/*", - "**/*_worklink_*", - "**/worklink_*" - ] - "service/workmail" = [ - "aws/internal/service/workmail/**/*", - "**/*_workmail_*", - "**/workmail_*" - ] - "service/workspaces" = [ - "aws/internal/service/workspaces/**/*", - "**/*_workspaces_*", - "**/workspaces_*" - ] - "service/xray" = [ - "aws/internal/service/xray/**/*", - "**/*_xray_*", - "**/xray_*" - ] - } -} - -behavior "regexp_issue_labeler" "panic_label" { - regexp = "panic:" - labels = ["crash", "bug"] -} - -behavior "remove_labels_on_reply" "remove_stale" { - labels = ["waiting-response", "stale"] - only_non_maintainers = true -} - -behavior "pull_request_size_labeler" "size" { - label_prefix = "size/" - label_map = { - "size/XS" = { - from = 0 - to = 30 - } - "size/S" = { - from = 31 - to = 60 - } - "size/M" = { - from = 61 - to = 150 - } - "size/L" = { - from = 151 - to = 300 - } - "size/XL" = { - from = 301 - to = 1000 - } - "size/XXL" = { - from = 1001 - to = 0 - } - } -} diff --git a/.semgrep.yml b/.semgrep.yml index 8637bcd5012e..6246ecc061db 100644 --- a/.semgrep.yml +++ b/.semgrep.yml @@ -22,8 +22,10 @@ rules: - aws/validators.go - aws/*wafregional*.go - aws/resource_aws_serverlessapplicationrepository_cloudformation_stack.go + - aws/resource_aws_transfer_server.go - aws/*_test.go - aws/internal/keyvaluetags/ + - aws/internal/namevaluesfilters/ - aws/internal/service/wafregional/ # Legacy resource handling - aws/resource_aws_autoscaling_group.go @@ -48,7 +50,83 @@ rules: metavariable: '$Y' regex: '^"github.com/aws/aws-sdk-go/service/[^/]+"$' severity: WARNING - + + - id: prefer-aws-go-sdk-pointer-conversion-assignment + languages: [go] + message: Prefer AWS Go SDK pointer conversion functions for dereferencing during assignment, e.g. aws.StringValue() + paths: + exclude: + - aws/cloudfront_distribution_configuration_structure.go + - aws/data_source_aws_route_table.go + - aws/opsworks_layers.go + - aws/resource_aws_d* + - aws/resource_aws_e* + - aws/resource_aws_g* + - aws/resource_aws_i* + - aws/resource_aws_k* + - aws/resource_aws_l* + - aws/resource_aws_mq_broker.go + - aws/resource_aws_o* + - aws/resource_aws_r* + - aws/resource_aws_s* + - aws/structure.go + - aws/waf_helpers.go + - aws/internal/generators/ + - aws/internal/keyvaluetags/ + - awsproviderlint/vendor/ + include: + - aws/ + patterns: + - pattern: '$LHS = *$RHS' + - pattern-not: '*$LHS2 = *$RHS' + severity: WARNING + + - id: prefer-aws-go-sdk-pointer-conversion-conditional + languages: [go] + message: Prefer AWS Go SDK pointer conversion functions for dereferencing during conditionals, e.g. aws.StringValue() + paths: + exclude: + - aws/cloudfront_distribution_configuration_structure.go + - aws/cloudfront_distribution_configuration_structure_test.go + - aws/config.go + - aws/data_source_aws_route* + - aws/ecs_task_definition_equivalency.go + - aws/opsworks_layers.go + - aws/resource_aws_d*.go + - aws/resource_aws_e*.go + - aws/resource_aws_g*.go + - aws/resource_aws_i*.go + - aws/resource_aws_k*.go + - aws/resource_aws_l*.go + - aws/resource_aws_main_route_table_association.go + - aws/resource_aws_n*.go + - aws/resource_aws_o*.go + - aws/resource_aws_r*.go + - aws/resource_aws_s*.go + - aws/resource*_test.go + - aws/structure.go + - aws/internal/generators/ + - aws/internal/keyvaluetags/ + - aws/internal/naming/ + - awsproviderlint/vendor/ + include: + - aws/ + patterns: + - pattern-either: + - pattern: '$LHS == *$RHS' + - pattern: '$LHS != *$RHS' + - pattern: '$LHS > *$RHS' + - pattern: '$LHS < *$RHS' + - pattern: '$LHS >= *$RHS' + - pattern: '$LHS <= *$RHS' + - pattern: '*$LHS == $RHS' + - pattern: '*$LHS != $RHS' + - pattern: '*$LHS > $RHS' + - pattern: '*$LHS < $RHS' + - pattern: '*$LHS >= $RHS' + - pattern: '*$LHS <= $RHS' + severity: WARNING + - id: aws-go-sdk-pointer-conversion-ResourceData-SetId fix: d.SetId(aws.StringValue($VALUE)) languages: [go] @@ -59,17 +137,85 @@ rules: pattern: 'd.SetId(*$VALUE)' severity: WARNING + - id: aws-go-sdk-pointer-conversion-immediate-dereference + fix: $VALUE + languages: [go] + message: Using AWS Go SDK pointer conversion, e.g. aws.String(), with immediate dereferencing is extraneous + paths: + include: + - aws/ + patterns: + - pattern-either: + - pattern: '*aws.Bool($VALUE)' + - pattern: '*aws.Float64($VALUE)' + - pattern: '*aws.Int64($VALUE)' + - pattern: '*aws.String($VALUE)' + - pattern: '*aws.Time($VALUE)' + severity: WARNING + + - id: data-source-with-resource-read + languages: [go] + message: Calling a resource's Read method from within a data-source is discouraged + paths: + include: + - aws/data_source_aws_*.go + patterns: + - pattern-regex: '(resource.+Read|flatten.+Resource)' + - pattern-inside: func $FUNCNAME(...) $RETURNTYPE { ... } + - pattern-not-inside: | + d.Set(..., []interface{}{ ... }) + - pattern-not-inside: | + d.Set($ATTRIBUTE, $FUNC($APIOBJECT)) + - metavariable-regex: + metavariable: "$FUNCNAME" + regex: "dataSource.+Read" + severity: WARNING + + - id: helper-acctest-RandInt-compiled + languages: [go] + message: Using `acctest.RandInt()` in constant or variable declaration will execute during compilation and not randomize, pass into string generating function instead + paths: + include: + - aws/ + patterns: + - pattern-either: + - pattern: const $CONST = fmt.Sprintf(..., <... acctest.RandInt() ...>, ...) + - pattern: var $VAR = fmt.Sprintf(..., <... acctest.RandInt() ...>, ...) + severity: WARNING + + - id: helper-acctest-RandString-compiled + languages: [go] + message: Using `acctest.RandString()` in constant or variable declaration will execute during compilation and not randomize, pass into string generating function instead + paths: + include: + - aws/ + patterns: + - pattern-either: + - pattern: const $CONST = fmt.Sprintf(..., <... acctest.RandString(...) ...>, ...) + - pattern: var $VAR = fmt.Sprintf(..., <... acctest.RandString(...) ...>, ...) + severity: WARNING + + - id: helper-acctest-RandomWithPrefix-compiled + languages: [go] + message: Using `acctest.RandomWithPrefix()` in constant or variable declaration will execute during compilation and not randomize, pass into string generating function instead + paths: + include: + - aws/ + patterns: + - pattern-either: + - pattern: const $CONST = fmt.Sprintf(..., <... acctest.RandomWithPrefix(...) ...>, ...) + - pattern: var $VAR = fmt.Sprintf(..., <... acctest.RandomWithPrefix(...) ...>, ...) + severity: WARNING + - id: helper-schema-Set-extraneous-NewSet-with-flattenStringList languages: [go] message: Prefer `flattenStringSet()` function for casting a list of string pointers to a set paths: include: - aws/ - patterns: - - pattern: schema.NewSet(schema.HashString, flattenStringList($APIOBJECT)) - - pattern-not-inside: func flattenStringSet(list []*string) *schema.Set { ... } + pattern: schema.NewSet(schema.HashString, flattenStringList($APIOBJECT)) severity: WARNING - + - id: helper-schema-Set-extraneous-expandStringList-with-List languages: [go] message: Prefer `expandStringSet()` function for casting a set to a list of string pointers @@ -83,10 +229,8 @@ rules: $LIST := $SET.List() ... expandStringList($LIST) - - pattern-not-inside: func expandStringSet(configured *schema.Set) []*string { ... } severity: WARNING - - id: helper-schema-ResourceData-GetOk-with-extraneous-conditional languages: [go] message: Zero value conditional check after `d.GetOk()` is extraneous @@ -102,48 +246,251 @@ rules: - pattern: if $VALUE, $OK := d.GetOk($KEY); $OK && len($VALUE.(string)) > 0 { $BODY } severity: WARNING - - id: helper-schema-resource-Retry-without-TimeoutError-check + - id: helper-schema-ResourceData-Set-extraneous-value-pointer-conversion + fix: d.Set($ATTRIBUTE, $APIOBJECT) languages: [go] - message: Check resource.Retry() errors with tfresource.TimedOut() + message: AWS Go SDK pointer conversion function for `d.Set()` value is extraneous paths: - exclude: - - "*_test.go" include: - aws/ patterns: - pattern-either: - - pattern: | - $ERR := resource.Retry(...) - ... - return ... - - pattern: | - $ERR = resource.Retry(...) - ... - return ... + - pattern: d.Set($ATTRIBUTE, aws.BoolValue($APIOBJECT)) + - pattern: d.Set($ATTRIBUTE, aws.Float64Value($APIOBJECT)) + - pattern: d.Set($ATTRIBUTE, aws.IntValue($APIOBJECT)) + - pattern: d.Set($ATTRIBUTE, aws.Int64Value($APIOBJECT)) + - pattern: d.Set($ATTRIBUTE, int(aws.Int64Value($APIOBJECT))) + - pattern: d.Set($ATTRIBUTE, aws.StringValue($APIOBJECT)) + severity: WARNING + + - id: helper-schema-ResourceData-DataSource-Set-tags + languages: [go] + message: (schema.ResourceData).Set() call with the tags key should include IgnoreConfig in the value + paths: + include: + - aws/data_source*.go + exclude: + - aws/resource*.go + patterns: + - pattern-inside: func $READMETHOD(...) $ERRORTYPE { ... } + - pattern: if err := d.Set("tags", $TAGSMAP); err != nil { ... } + - pattern-not: if err := d.Set("tags", $KEYVALUETAGS.IgnoreAws().IgnoreConfig($CONFIG).Map()); err != nil { ... } + severity: WARNING + + - id: helper-schema-ResourceData-Resource-Set-tags + languages: [go] + message: (schema.ResourceData).Set() call with the tags key should be preceded by a call to IgnoreConfig or include IgnoreConfig in the value in the case of ASG + paths: + include: + - aws/resource*.go + exclude: + - aws/data_source*.go + patterns: + - pattern-inside: func $READMETHOD(...) $ERRORTYPE { ... } + - pattern-either: + - pattern: | + tags := $TAGS + ... + if err := d.Set("tags", $TAGSMAP); err != nil { ... } + - pattern: | + tags = $TAGS + ... + if err := d.Set("tags", $TAGSMAP); err != nil { ... } + - pattern: | + $ASGTAGS := keyvaluetags.AutoscalingKeyValueTags(...) + ... + if err := d.Set("tags", $TAGSMAP); err != nil { ... } + - pattern-not-regex: 'keyvaluetags.AutoscalingKeyValueTags\(.+\).IgnoreAws\(\).IgnoreConfig' - pattern-not: | - $ERR := resource.Retry(...) - ... - if isResourceTimeoutError($ERR) { ... } + tags = $KEYVALUETAGS.IgnoreAws().IgnoreConfig($CONFIG) ... - return ... - pattern-not: | - $ERR = resource.Retry(...) + tags = $KEYVALUETAGS.$IGNORESERVICE().IgnoreConfig($CONFIG) ... - if isResourceTimeoutError($ERR) { ... } - ... - return ... - pattern-not: | - $ERR := resource.Retry(...) - ... - if tfresource.TimedOut($ERR) { ... } + tags := keyvaluetags.$VALUETAGS($RESOURCETAGS).IgnoreAws().IgnoreConfig($CONFIG) ... - return ... - pattern-not: | - $ERR = resource.Retry(...) + tags = keyvaluetags.$VALUETAGS($RESOURCETAGS).IgnoreAws().IgnoreConfig($CONFIG) ... - if tfresource.TimedOut($ERR) { ... } - ... - return ... + severity: WARNING + + - id: helper-schema-ResourceData-SetId-empty-without-IsNewResource-check + languages: [go] + message: Calling `d.SetId("")` should ensure `!d.IsNewResource()` is checked first + paths: + exclude: + - aws/resource_aws_api_gateway_*.go + - aws/resource_aws_apigatewayv2_*.go + - aws/resource_aws_app_cookie_stickiness_policy.go + - aws/resource_aws_appautoscaling_*.go + - aws/resource_aws_appsync_*.go + - aws/resource_aws_athena_*.go + - aws/resource_aws_autoscaling_*.go + - aws/resource_aws_autoscalingplans_scaling_plan.go + - aws/resource_aws_[b-ce-g]*.go + - aws/resource_aws_d[a-df-z]*.go + - aws/resource_aws_devicefarm*.go + - aws/resource_aws_i*.go + - aws/resource_aws_[k-r]*.go + - aws/resource_aws_s[a-df-z3]*.go + - aws/resource_aws_se[d-z]*.go + - aws/resource_aws_sec[a-t]*.go + - aws/resource_aws_securityhub*.go + - aws/resource_aws_[t-x]*.go + include: + - aws/resource*.go + patterns: + - pattern-either: + - pattern: | + d.SetId("") + ... + return nil + - pattern-not-inside: | + if ... { + if <... d.IsNewResource() ...> { ... } + ... + d.SetId("") + ... + return nil + } + - pattern-not-inside: | + if <... !d.IsNewResource() ...> { ... } + severity: WARNING + + - id: helper-schema-resource-Retry-without-TimeoutError-check + languages: [go] + message: Check resource.Retry() errors with tfresource.TimedOut() + paths: + exclude: + - "*_test.go" + - aws/internal/tfresource/*.go + include: + - aws/ + patterns: + - pattern-either: + - patterns: + - pattern-either: + - pattern: | + $ERR := resource.Retry(...) + ... + return ... + - pattern: | + $ERR = resource.Retry(...) + ... + return ... + - pattern-not: | + $ERR := resource.Retry(...) + ... + if isResourceTimeoutError($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR = resource.Retry(...) + ... + if isResourceTimeoutError($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR := resource.Retry(...) + ... + if tfresource.TimedOut($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR = resource.Retry(...) + ... + if tfresource.TimedOut($ERR) { ... } + ... + return ... + - patterns: + - pattern-either: + - pattern: | + $ERR := resource.RetryContext(...) + ... + return ... + - pattern: | + $ERR = resource.RetryContext(...) + ... + return ... + - pattern-not: | + $ERR := resource.RetryContext(...) + ... + if isResourceTimeoutError($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR = resource.RetryContext(...) + ... + if isResourceTimeoutError($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR := resource.RetryContext(...) + ... + if tfresource.TimedOut($ERR) { ... } + ... + return ... + - pattern-not: | + $ERR = resource.RetryContext(...) + ... + if tfresource.TimedOut($ERR) { ... } + ... + return ... + severity: WARNING + + - id: helper-schema-TimeoutError-check-doesnt-return-output + languages: [go] + message: If the resource.Retry() or resource.RetryContext() function returns a value, ensure the isResourceTimeoutError() check does as well + paths: + exclude: + - "*_test.go" + include: + - aws/ + patterns: + - pattern-either: + - patterns: + - pattern: | + if isResourceTimeoutError($ERR) { + _, $ERR = $CONN.$FUNC(...) + } + - pattern-not-inside: | + $ERR = resource.Retry(..., func() *resource.RetryError { + ... + _, $ERR2 = $CONN.$FUNC(...) + ... + }) + ... + if isResourceTimeoutError($ERR) { ... } + - pattern-not-inside: | + $ERR = resource.RetryContext(..., func() *resource.RetryError { + ... + _, $ERR2 = $CONN.$FUNC(...) + ... + }) + ... + if isResourceTimeoutError($ERR) { ... } + - patterns: + - pattern: | + if tfresource.TimedOut($ERR) { + _, $ERR = $CONN.$FUNC(...) + } + - pattern-not-inside: | + $ERR = resource.Retry(..., func() *resource.RetryError { + ... + _, $ERR2 = $CONN.$FUNC(...) + ... + }) + ... + if tfresource.TimedOut($ERR) { ... } + - pattern-not-inside: | + $ERR = resource.RetryContext(..., func() *resource.RetryError { + ... + _, $ERR2 = $CONN.$FUNC(...) + ... + }) + ... + if tfresource.TimedOut($ERR) { ... } severity: WARNING - id: is-not-found-error @@ -159,9 +506,135 @@ rules: var $CAST *resource.NotFoundError ... errors.As($ERR, &$CAST) - - pattern-not-inside: func NotFound(err error) bool { ... } + - pattern-not-inside: | + var $CAST *resource.NotFoundError + ... + errors.As($ERR, &$CAST) + ... + $CAST.$FIELD - patterns: - pattern: | $X, $Y := $ERR.(*resource.NotFoundError) - - pattern-not-inside: func isResourceNotFoundError(err error) bool { ... } + severity: WARNING + + - id: time-equality + languages: [go] + message: Use time.Equal() instead of == + paths: + include: + - aws/ + patterns: + - pattern-either: + - pattern: | + aws.TimeValue($X) == $Y + - pattern: | + aws.TimeValue($X) != $Y + - pattern: | + ($X : time.Time) == $Y + - pattern: | + ($X : time.Time) != $Y + - pattern: | + $X == aws.TimeValue($Y) + - pattern: | + $X != aws.TimeValue($Y) + - pattern: | + $X == ($Y : time.Time) + - pattern: | + $X != ($Y : time.Time) + severity: WARNING + + - id: prefer-pagination-bool-var-last-page + languages: [go] + message: Use lastPage for bool variable in pagination functions + paths: + include: + - aws/ + patterns: + - pattern: | + $X.$Z(..., func(..., $Y bool) { + ... + }) + - pattern-not: | + $X.$Z(..., func(..., lastPage bool) { + ... + }) + - pattern-not: | + $X.$Z(..., func(..., _ bool) { + ... + }) + - metavariable-regex: + metavariable: '$Z' + regex: '.*Pages$' + severity: WARNING + + - id: calling-fmt.Print-and-variants + languages: [go] + message: Do not call `fmt.Print` and variant + paths: + exclude: + - awsproviderlint/vendor/ + include: + - aws/ + patterns: + - pattern-either: + - pattern: | + fmt.Print(...) + - pattern: | + fmt.Printf(...) + - pattern: | + fmt.Println(...) + severity: WARNING + + - id: domain-names + languages: [go] + message: Domain names should be in the namespaces defined in RFC 6761 (https://datatracker.ietf.org/doc/html/rfc6761) as reserved for testing + paths: + include: + - aws/data_source*.go + - aws/resource_aws_a*.go + - aws/resource_aws_b*.go + - aws/resource_aws_workspaces_*.go + patterns: + - patterns: + - pattern-regex: '[\"`].*(? Running Semgrep static analysis..." + @docker run --rm --volume "${PWD}:/src" returntocorp/semgrep --config .semgrep.yml + +.PHONY: awsproviderlint build gen generate-changelog golangci-lint sweep test testacc fmt fmtcheck lint tools test-compile website-link-check website-lint website-lint-fix depscheck docscheck semgrep diff --git a/ROADMAP.md b/ROADMAP.md index 57402a07c29f..433c5fb55e31 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,102 +1,110 @@ -# Roadmap: February 2021 - April 2021 +# Roadmap: August 2021 - October 2021 Every few months, the team will highlight areas of focus for our work and upcoming research. -We select items for inclusion in the roadmap from the Top 10 Community Issues, [Core Services](docs/CORE_SERVICES.md), and internal priorities. Where community sourced contributions exist we will work with the authors to review and merge their work. Where this does not exist or the original contributors, are not available we will create the resources and implementation ourselves. +We select items for inclusion in the roadmap from the Top 10 Community Issues, [Core Services](docs/CORE_SERVICES.md), and internal priorities. Where community sourced contributions exist we will work with the authors to review and merge their work. Where this does not exist or the original contributors are not available we will create the resources and implementation ourselves. Each weekly release will include necessary tasks that lead to the completion of the stated goals as well as community pull requests, enhancements, and features that are not highlighted in the roadmap. To view all the items we've prioritized for this quarter, please see the [Roadmap milestone](https://github.com/hashicorp/terraform-provider-aws/milestone/138). This roadmap does not describe all the work that will be included within this timeframe, but it does describe our focus. We will include other work as events occur . -From [November through January](docs/roadmaps/2020_November_to_January.md), we added support for (among other things): +In the period spanning May to July 2021 539 Pull Requests were opened in the provider and 449 were merged, adding support for: -- SSO Permission Sets -- EC2 Managed Prefix Lists -- Firewall Manager Policies -- SASL/SCRAM Authentication for MSK -- ImageBuilder -- LakeFormation -- Serverless Application Repository -- Cloudwatch Composite Alarms +- Amazon Timestream +- AWS AppConfig +- AWS Amplify +- AWS Service Catalog +- AWS ElasticSearch Native SAML for Kibana +- Amazon Macie 2 +- Delegated Administrators for Organisations +- Predictive Autoscaling +- Amazon EKS OIDC +- AWS Transfer Family support for Amazon Elastic File System +- Amazon Kinesis Data Streams for Amazon DynamoDB -As well as partnering with AWS to provide launch day support for: +Among many other enhancements, bug fixes and resolutions to technical debt items. -- Network Firewall -- Code Signing for Lambda -- Container Images for Lambda -- Gateway Load Balancer -- Spot Launch for EKS Managed Node Groups +From August-October ‘21, we will be prioritizing the following areas of work: -From February-April ‘21, we will be prioritizing the following areas of work: +## Provider Version v4.0.0 -## Provider Functionality: Default Tags +Issue: [#20433](https://github.com/hashicorp/terraform-provider-aws/issues/20433) -Issue: [#7926](https://github.com/hashicorp/terraform-provider-aws/issues/7926) +The next major release of the provider will include the adoption of the AWS Go SDK v2.0 as well as a refactor of one of our oldest and most used resources: S3. -Default Tags builds on the workflows in Ignore Tags to provide additional control over the ways Terraform manages tagging capabilities. Users will be able to specify lists of tags to apply to all resources in a configuration at the provider level. Our goal in offering this use case is to assist in tidying up configuration files, decreasing development efforts, and streamlining cost allocation and resource attribution within organizations of all sizes. +There will also be the usual deprecations and sometimes breaking changes to existing resources which are necessary to maintain consistency of behavior across resources. Our goal is to focus on standardization to reduce technical debt and lay a strong foundation for future enhancement initiatives within the provider. + +For details of the changes in full please refer to #20433. We would love to hear your feedback. ## New Services -### CloudWatch Synthetics -Issue: [#11145](https://github.com/hashicorp/terraform-provider-aws/issues/11145) +### Amazon Quicksight +Issue: [#10990](https://github.com/hashicorp/terraform-provider-aws/issues/10990) -_[CloudWatch Synthetics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html) You can use Amazon CloudWatch Synthetics to create canaries, configurable scripts that run on a schedule, to monitor your endpoints and APIs. Canaries follow the same routes and perform the same actions as a customer, which makes it possible for you to continually verify your customer experience even when you don't have any customer traffic on your applications. By using canaries, you can discover issues before your customers do._ +_Amazon QuickSight is a scalable, serverless, embeddable, machine learning-powered business intelligence (BI) service built for the cloud. QuickSight lets you easily create and publish interactive BI dashboards that include Machine Learning-powered insights. QuickSight dashboards can be accessed from any device, and seamlessly embedded into your applications, portals, and websites._ -Support for CloudWatch Synthetics will include: +Support for Amazon Quicksight will include: New Resource(s): -- aws_synthetics_canary - -New Datasource(s): -- aws_synthetics_canary_runs +- aws_quicksight_data_source +- aws_quicksight_group_membership +- aws_quicksight_iam_policy_assignment +- aws_quicksight_data_set +- aws_quicksight_ingestion +- aws_quicksight_template +- aws_quicksight_dashboard +- aws_quicksight_template_alias -### Managed Workflows for Apache Airflow -Issue: [#16432](https://github.com/hashicorp/terraform-provider-aws/issues/16432) +### Amazon AppStream +Issue: [#6058](https://github.com/hashicorp/terraform-provider-aws/issues/6508) -_[Managed Workflows for Apache Airflow](https://aws.amazon.com/blogs/aws/introducing-amazon-managed-workflows-for-apache-airflow-mwaa/) Amazon Managed Workflows for Apache Airflow (MWAA) is a managed orchestration service for Apache Airflow1 that makes it easier to set up and operate end-to-end data pipelines in the cloud at scale. Apache Airflow is an open-source tool used to programmatically author, schedule, and monitor sequences of processes and tasks referred to as “workflows.” With Managed Workflows, you can use Airflow and Python to create workflows without having to manage the underlying infrastructure for scalability, availability, and security. Managed Workflows automatically scales its workflow execution capacity to meet your needs, and is integrated with AWS security services to help provide you with fast and secure access to data._ +_Amazon AppStream 2.0 is a fully managed non-persistent desktop and application virtualization service that allows your users to securely access the data, applications, and resources they need, anywhere, anytime, from any supported device. With AppStream 2.0, you can easily scale your applications and desktops to any number of users across the globe without acquiring, provisioning, and operating hardware or infrastructure. AppStream 2.0 is built on AWS, so you benefit from a data center and network architecture designed for the most security-sensitive organizations. Each end user has a fluid and responsive experience because your applications run on virtual machines optimized for specific use cases and each streaming sessions automatically adjusts to network conditions._ -Support for Amazon Managed Workflows for Apache Airflow will include: +Support for Amazon AppStream will include: New Resource(s): +- aws_appstream_stack +- aws_appstream_fleet +- aws_appstream_imagebuilder -- aws_mwaa_environment +### Amazon Connect +Issue: [#16392](https://github.com/hashicorp/terraform-provider-aws/issues/16392) -## Core Service Reliability -Core Services are areas of high usage or strategic importance for our users. We strive to offer rock solid reliability in these areas. This quarter we will have a focus on RDS and Elasticache (which we are also promoting to Core Service status) to address some common pain points in their usage and ensure they continue to meet our standards. +_Amazon Connect is an easy to use omnichannel cloud contact center that helps you provide superior customer service at a lower cost. Designed from the ground up to be omnichannel, Amazon Connect provides a seamless experience across voice and chat for your customers and agents. This includes one set of tools for skills-based routing, task management, powerful real-time and historical analytics, and intuitive management tools – all with pay-as-you-go pricing, which means Amazon Connect simplifies contact center operations, improves agent efficiency, and lowers costs. You can set up a contact center in minutes that can scale to support millions of customers from the office or as a virtual contact center._ -### RDS +Support for Amazon Connect will include: + +New Resource(s): +- aws_connect_instance +- aws_connect_contact_flow +- aws_connect_bot_association +- aws_connect_lex_bot_association +- aws_connect_lambda_function_association -- [#15177](https://github.com/hashicorp/terraform-provider-aws/issues/15177): Subsequent plan/apply forces global cluster recreation when source cluster's storage_encrypted=true -- [#15583](https://github.com/hashicorp/terraform-provider-aws/issues/15583): aws db parameter group ... converts keys and values to lowercase and fails 'apply' due to aws_db_parameter_group changes -- [#1198](https://github.com/hashicorp/terraform-provider-aws/issues/1198): Unable to ignore changes to RDS minor engine version -- [#9401](https://github.com/hashicorp/terraform-provider-aws/issues/9401): Destroy/recreate DB instance on minor version update rather than updating -- [#2635](https://github.com/hashicorp/terraform-provider-aws/issues/2635): RDS - storage_encrypted = true does not work -- [#467](https://github.com/hashicorp/terraform-provider-aws/issues/467): With aws_db_instance when you remove the snapshot_identifier it wants to force a new resource -- [#10197](https://github.com/hashicorp/terraform-provider-aws/issues/10197): AWS aurora unexpected state 'configuring-iam-database-auth' when modifying the `iam_database_authentication_enabled` flag -- [#13891](https://github.com/hashicorp/terraform-provider-aws/issues/13891): RDS Cluster is not reattached to Global Cluster after failing deletion +New Data Source(s): +- aws_connect_instance +- aws_connect_contact_flow +- aws_connect_bot_association +- aws_connect_lex_bot_association +- aws_connect_lambda_function_association -## Technical Debt Theme +## Enhancements to Existing Services +- [Support for KMS Multi-Region Keys](https://github.com/hashicorp/terraform-provider-aws/issues/19896) +- [S3 Replication Time Control](https://github.com/hashicorp/terraform-provider-aws/issues/10974) +- [New Data Source: aws_iam_roles](https://github.com/hashicorp/terraform-provider-aws/issues/14470) -Last quarter we continued to improve the stability of our Acceptance Test suite. Following on from that work we will begin to integrate our Pull Request workflow with our Acceptance testing suite with a goal of being able to determine which tests to run, trigger, and view results of Acceptance Test runs on GitHub. This will improve our time to merge incoming PR's and further protect against regressions. +## Project Restructure: Service Level Packages -We also spent time last quarter improving our documentation to give contributors more explicit guidance on best practice patterns for [data conversion](https://github.com/hashicorp/terraform-provider-aws/blob/main/docs/contributing/data-handling-and-conversion.md) and [error handling](https://github.com/hashicorp/terraform-provider-aws/blob/main/docs/contributing/error-handling.md). +The scale of the provider (now 1000 resources/datasources) has led to its existing package structure being difficult to work with and maintain. This quarter we are going to perform a large refactor of the codebase, to align on a single go package per AWS service. More details can be found in the encompassing issue [#20431](https://github.com/hashicorp/terraform-provider-aws/issues/20431) ## Research Topics Research topics include features, architectural changes, and ideas that we are pursuing in the longer term that may significantly impact the core user experience of the AWS provider. Research topics are discovery only and are not guaranteed to be included in a future release. -We are interested in your thoughts and feedback about the proposals below and encourage you to comment on the linked issues or schedule time with @maryelizbeth via the link on her GitHub profile to discuss. - -### API Calls/IAM Actions Per Terraform Resource (Minimum IAM) -Issue: [#9154](https://github.com/hashicorp/terraform-provider-aws/issues/9154) - -To address security concerns and best practices we are considering how Terraform could surface minimally viable IAM policies for taking actions on resources or executing a TF plan. This is in the early stages of research and we are particularly interested in whether or not this would be useful and the resources or services areas for which it is most valuable. - -### Lifecycle: Retain [Add 'retain' attribute to the Terraform lifecycle meta-parameter] -Issue: [#902](https://github.com/hashicorp/terraform-provider-aws/issues/902) +### Scaffolding for new resources, datasources and associated tests. -Some resources (e.g. log groups) are intended to be created but never destroyed. Terraform currently does not have a lifecycle attribute for retaining such resources. We are curious as to whether or not retaining resources is a workflow that meets the needs of our community and if so, how and where we might make use of that in the AWS Provider. +Adding resources, datasources and test files to the provider is a repetitive task which should be automated to ensure consistency and speed up contributor and maintainer workflow. A simple cli tool should be able to generate these files in place, and ensure that any code reference additions required (ie adding to `provider.go`) are performed as part of the process. ## Disclosures diff --git a/aws/api_gateway_domain_name_test.go b/aws/api_gateway_domain_name_test.go index b2385efe6375..a0d8d94860cd 100644 --- a/aws/api_gateway_domain_name_test.go +++ b/aws/api_gateway_domain_name_test.go @@ -37,17 +37,17 @@ var testAccProviderApigatewayEdgeDomainNameConfigure sync.Once func testAccPreCheckApigatewayEdgeDomainName(t *testing.T) { testAccPartitionHasServicePreCheck(apigateway.EndpointsID, t) + region := testAccGetApigatewayEdgeDomainNameRegion() + + if region == "" { + t.Skip("API Gateway Domain Name not available in this AWS Partition") + } + // Since we are outside the scope of the Terraform configuration we must // call Configure() to properly initialize the provider configuration. testAccProviderApigatewayEdgeDomainNameConfigure.Do(func() { testAccProviderApigatewayEdgeDomainName = Provider() - region := testAccGetApigatewayEdgeDomainNameRegion() - - if region == "" { - t.Skip("API Gateway Domain Name not available in this AWS Partition") - } - config := map[string]interface{}{ "region": region, } diff --git a/aws/aws_sweeper_test.go b/aws/aws_sweeper_test.go index fdece2fff542..6eda7d6b9e76 100644 --- a/aws/aws_sweeper_test.go +++ b/aws/aws_sweeper_test.go @@ -1,13 +1,29 @@ package aws import ( + "context" "fmt" + "log" "os" + "strconv" + "strings" "testing" + "time" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/envvar" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) +const ( + SweepThrottlingRetryTimeout = 10 * time.Minute +) + +const defaultSweeperAssumeRoleDurationSeconds = 3600 + // sweeperAwsClients is a shared cache of regional AWSClient // This prevents client re-initialization for every resource with no benefit. var sweeperAwsClients map[string]interface{} @@ -24,8 +40,16 @@ func sharedClientForRegion(region string) (interface{}, error) { return client, nil } - if os.Getenv("AWS_PROFILE") == "" && (os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "") { - return nil, fmt.Errorf("must provide environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY or environment variable AWS_PROFILE") + _, _, err := envvar.RequireOneOf([]string{envvar.AwsProfile, envvar.AwsAccessKeyId, envvar.AwsContainerCredentialsFullUri}, "credentials for running sweepers") + if err != nil { + return nil, err + } + + if os.Getenv(envvar.AwsAccessKeyId) != "" { + _, err := envvar.Require(envvar.AwsSecretAccessKey, "static credentials value when using "+envvar.AwsAccessKeyId) + if err != nil { + return nil, err + } } conf := &Config{ @@ -33,13 +57,143 @@ func sharedClientForRegion(region string) (interface{}, error) { Region: region, } + if role := os.Getenv(envvar.TfAwsAssumeRoleARN); role != "" { + conf.AssumeRoleARN = role + + conf.AssumeRoleDurationSeconds = defaultSweeperAssumeRoleDurationSeconds + if v := os.Getenv(envvar.TfAwsAssumeRoleDuration); v != "" { + d, err := strconv.Atoi(v) + if err != nil { + return nil, fmt.Errorf("environment variable %s: %w", envvar.TfAwsAssumeRoleDuration, err) + } + conf.AssumeRoleDurationSeconds = d + } + + if v := os.Getenv(envvar.TfAwsAssumeRoleExternalID); v != "" { + conf.AssumeRoleExternalID = v + } + + if v := os.Getenv(envvar.TfAwsAssumeRoleSessionName); v != "" { + conf.AssumeRoleSessionName = v + } + } + // configures a default client for the region, using the above env vars client, err := conf.Client() if err != nil { - return nil, fmt.Errorf("error getting AWS client") + return nil, fmt.Errorf("error getting AWS client: %w", err) } sweeperAwsClients[region] = client return client, nil } + +type testSweepResource struct { + d *schema.ResourceData + meta interface{} + resource *schema.Resource +} + +func NewTestSweepResource(resource *schema.Resource, d *schema.ResourceData, meta interface{}) *testSweepResource { + return &testSweepResource{ + d: d, + meta: meta, + resource: resource, + } +} + +func testSweepResourceOrchestrator(sweepResources []*testSweepResource) error { + return testSweepResourceOrchestratorContext(context.Background(), sweepResources, 0*time.Millisecond, 0*time.Millisecond, 0*time.Millisecond, 0*time.Millisecond, SweepThrottlingRetryTimeout) +} + +func testSweepResourceOrchestratorContext(ctx context.Context, sweepResources []*testSweepResource, delay time.Duration, delayRand time.Duration, minTimeout time.Duration, pollInterval time.Duration, timeout time.Duration) error { + var g multierror.Group + + for _, sweepResource := range sweepResources { + sweepResource := sweepResource + + g.Go(func() error { + err := tfresource.RetryConfigContext(ctx, delay, delayRand, minTimeout, pollInterval, timeout, func() *resource.RetryError { + err := testAccDeleteResource(sweepResource.resource, sweepResource.d, sweepResource.meta) + + if err != nil { + if strings.Contains(err.Error(), "Throttling") { + log.Printf("[INFO] While sweeping resource (%s), encountered throttling error (%s). Retrying...", sweepResource.d.Id(), err) + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + err = testAccDeleteResource(sweepResource.resource, sweepResource.d, sweepResource.meta) + } + + return err + }) + } + + return g.Wait().ErrorOrNil() +} + +// Check sweeper API call error for reasons to skip sweeping +// These include missing API endpoints and unsupported API calls +func testSweepSkipSweepError(err error) bool { + // Ignore missing API endpoints + if isAWSErr(err, "RequestError", "send request failed") { + return true + } + // Ignore unsupported API calls + if isAWSErr(err, "UnsupportedOperation", "") { + return true + } + // Ignore more unsupported API calls + // InvalidParameterValue: Use of cache security groups is not permitted in this API version for your account. + if isAWSErr(err, "InvalidParameterValue", "not permitted in this API version for your account") { + return true + } + // InvalidParameterValue: Access Denied to API Version: APIGlobalDatabases + if isAWSErr(err, "InvalidParameterValue", "Access Denied to API Version") { + return true + } + // GovCloud has endpoints that respond with (no message provided): + // AccessDeniedException: + // Since acceptance test sweepers are best effort and this response is very common, + // we allow bypassing this error globally instead of individual test sweeper fixes. + if isAWSErr(err, "AccessDeniedException", "") { + return true + } + // Example: BadRequestException: vpc link not supported for region us-gov-west-1 + if isAWSErr(err, "BadRequestException", "not supported") { + return true + } + // Example: InvalidAction: The action DescribeTransitGatewayAttachments is not valid for this web service + if isAWSErr(err, "InvalidAction", "is not valid") { + return true + } + // For example from GovCloud SES.SetActiveReceiptRuleSet. + if isAWSErr(err, "InvalidAction", "Unavailable Operation") { + return true + } + // For example from us-west-2 Route53 key signing key + if isAWSErr(err, "InvalidKeySigningKeyStatus", "cannot be deleted because") { + return true + } + // For example from us-west-2 Route53 zone + if isAWSErr(err, "KeySigningKeyInParentDSRecord", "Due to DNS lookup failure") { + return true + } + return false +} + +// Check sweeper API call error for reasons to skip a specific resource +// These include AccessDenied or AccessDeniedException for individual resources, e.g. managed by central IT +func testSweepSkipResourceError(err error) bool { + // Since acceptance test sweepers are best effort, we allow bypassing this error globally + // instead of individual test sweeper fixes. + return tfawserr.ErrCodeContains(err, "AccessDenied") +} diff --git a/aws/awserr.go b/aws/awserr.go index cc1c6ba1d0a4..6df07f7bec17 100644 --- a/aws/awserr.go +++ b/aws/awserr.go @@ -1,10 +1,8 @@ package aws import ( - "errors" "time" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" @@ -18,15 +16,6 @@ func isAWSErr(err error, code string, message string) bool { return tfawserr.ErrMessageContains(err, code, message) } -// Returns true if the error matches all these conditions: -// * err is of type awserr.RequestFailure -// * RequestFailure.StatusCode() matches status code -// It is always preferable to use isAWSErr() except in older APIs (e.g. S3) -// that sometimes only respond with status codes. -func isAWSErrRequestFailureStatusCode(err error, statusCode int) bool { - return tfawserr.ErrStatusCodeEquals(err, statusCode) -} - func retryOnAwsCode(code string, f func() (interface{}, error)) (interface{}, error) { var resp interface{} err := resource.Retry(2*time.Minute, func() *resource.RetryError { @@ -47,31 +36,3 @@ func retryOnAwsCode(code string, f func() (interface{}, error)) (interface{}, er return resp, err } - -// RetryOnAwsCodes retries AWS error codes for one minute -// Note: This function will be moved out of the aws package in the future. -func RetryOnAwsCodes(codes []string, f func() (interface{}, error)) (interface{}, error) { - var resp interface{} - err := resource.Retry(1*time.Minute, func() *resource.RetryError { - var err error - resp, err = f() - if err != nil { - var awsErr awserr.Error - if errors.As(err, &awsErr) { - for _, code := range codes { - if awsErr.Code() == code { - return resource.RetryableError(err) - } - } - } - return resource.NonRetryableError(err) - } - return nil - }) - - if tfresource.TimedOut(err) { - resp, err = f() - } - - return resp, err -} diff --git a/aws/cloudfront_cache_policy_structure.go b/aws/cloudfront_cache_policy_structure.go index 7412bb16cd9a..1a8e0b0ffaad 100644 --- a/aws/cloudfront_cache_policy_structure.go +++ b/aws/cloudfront_cache_policy_structure.go @@ -224,10 +224,10 @@ func setParametersConfig(parametersConfig *cloudfront.ParametersInCacheKeyAndFor } func setCloudFrontCachePolicy(d *schema.ResourceData, cachePolicy *cloudfront.CachePolicyConfig) { - d.Set("comment", aws.StringValue(cachePolicy.Comment)) - d.Set("default_ttl", aws.Int64Value(cachePolicy.DefaultTTL)) - d.Set("max_ttl", aws.Int64Value(cachePolicy.MaxTTL)) - d.Set("min_ttl", aws.Int64Value(cachePolicy.MinTTL)) - d.Set("name", aws.StringValue(cachePolicy.Name)) + d.Set("comment", cachePolicy.Comment) + d.Set("default_ttl", cachePolicy.DefaultTTL) + d.Set("max_ttl", cachePolicy.MaxTTL) + d.Set("min_ttl", cachePolicy.MinTTL) + d.Set("name", cachePolicy.Name) d.Set("parameters_in_cache_key_and_forwarded_to_origin", setParametersConfig(cachePolicy.ParametersInCacheKeyAndForwardedToOrigin)) } diff --git a/aws/cloudfront_distribution_configuration_structure.go b/aws/cloudfront_distribution_configuration_structure.go index e1b61a36a244..bace2eca835c 100644 --- a/aws/cloudfront_distribution_configuration_structure.go +++ b/aws/cloudfront_distribution_configuration_structure.go @@ -207,6 +207,12 @@ func expandCloudFrontDefaultCacheBehavior(m map[string]interface{}) *cloudfront. dcb.DefaultTTL = aws.Int64(int64(m["default_ttl"].(int))) } + if v, ok := m["trusted_key_groups"]; ok { + dcb.TrustedKeyGroups = expandTrustedKeyGroups(v.([]interface{})) + } else { + dcb.TrustedKeyGroups = expandTrustedKeyGroups([]interface{}{}) + } + if v, ok := m["trusted_signers"]; ok { dcb.TrustedSigners = expandTrustedSigners(v.([]interface{})) } else { @@ -217,6 +223,10 @@ func expandCloudFrontDefaultCacheBehavior(m map[string]interface{}) *cloudfront. dcb.LambdaFunctionAssociations = expandLambdaFunctionAssociations(v.(*schema.Set).List()) } + if v, ok := m["function_association"]; ok { + dcb.FunctionAssociations = expandFunctionAssociations(v.(*schema.Set).List()) + } + if v, ok := m["smooth_streaming"]; ok { dcb.SmoothStreaming = aws.Bool(v.(bool)) } @@ -255,6 +265,12 @@ func expandCacheBehavior(m map[string]interface{}) *cloudfront.CacheBehavior { cb.DefaultTTL = aws.Int64(int64(m["default_ttl"].(int))) } + if v, ok := m["trusted_key_groups"]; ok { + cb.TrustedKeyGroups = expandTrustedKeyGroups(v.([]interface{})) + } else { + cb.TrustedKeyGroups = expandTrustedKeyGroups([]interface{}{}) + } + if v, ok := m["trusted_signers"]; ok { cb.TrustedSigners = expandTrustedSigners(v.([]interface{})) } else { @@ -265,6 +281,10 @@ func expandCacheBehavior(m map[string]interface{}) *cloudfront.CacheBehavior { cb.LambdaFunctionAssociations = expandLambdaFunctionAssociations(v.(*schema.Set).List()) } + if v, ok := m["function_association"]; ok { + cb.FunctionAssociations = expandFunctionAssociations(v.(*schema.Set).List()) + } + if v, ok := m["smooth_streaming"]; ok { cb.SmoothStreaming = aws.Bool(v.(bool)) } @@ -299,12 +319,18 @@ func flattenCloudFrontDefaultCacheBehavior(dcb *cloudfront.DefaultCacheBehavior) if dcb.ForwardedValues != nil { m["forwarded_values"] = []interface{}{flattenForwardedValues(dcb.ForwardedValues)} } + if len(dcb.TrustedKeyGroups.Items) > 0 { + m["trusted_key_groups"] = flattenTrustedKeyGroups(dcb.TrustedKeyGroups) + } if len(dcb.TrustedSigners.Items) > 0 { m["trusted_signers"] = flattenTrustedSigners(dcb.TrustedSigners) } if len(dcb.LambdaFunctionAssociations.Items) > 0 { m["lambda_function_association"] = flattenLambdaFunctionAssociations(dcb.LambdaFunctionAssociations) } + if len(dcb.FunctionAssociations.Items) > 0 { + m["function_association"] = flattenFunctionAssociations(dcb.FunctionAssociations) + } if dcb.MaxTTL != nil { m["max_ttl"] = aws.Int64Value(dcb.MaxTTL) } @@ -339,12 +365,18 @@ func flattenCacheBehavior(cb *cloudfront.CacheBehavior) map[string]interface{} { if cb.ForwardedValues != nil { m["forwarded_values"] = []interface{}{flattenForwardedValues(cb.ForwardedValues)} } + if len(cb.TrustedKeyGroups.Items) > 0 { + m["trusted_key_groups"] = flattenTrustedKeyGroups(cb.TrustedKeyGroups) + } if len(cb.TrustedSigners.Items) > 0 { m["trusted_signers"] = flattenTrustedSigners(cb.TrustedSigners) } if len(cb.LambdaFunctionAssociations.Items) > 0 { m["lambda_function_association"] = flattenLambdaFunctionAssociations(cb.LambdaFunctionAssociations) } + if len(cb.FunctionAssociations.Items) > 0 { + m["function_association"] = flattenFunctionAssociations(cb.FunctionAssociations) + } if cb.MaxTTL != nil { m["max_ttl"] = int(*cb.MaxTTL) } @@ -366,6 +398,26 @@ func flattenCacheBehavior(cb *cloudfront.CacheBehavior) map[string]interface{} { return m } +func expandTrustedKeyGroups(s []interface{}) *cloudfront.TrustedKeyGroups { + var tkg cloudfront.TrustedKeyGroups + if len(s) > 0 { + tkg.Quantity = aws.Int64(int64(len(s))) + tkg.Items = expandStringList(s) + tkg.Enabled = aws.Bool(true) + } else { + tkg.Quantity = aws.Int64(0) + tkg.Enabled = aws.Bool(false) + } + return &tkg +} + +func flattenTrustedKeyGroups(tkg *cloudfront.TrustedKeyGroups) []interface{} { + if tkg.Items != nil { + return flattenStringList(tkg.Items) + } + return []interface{}{} +} + func expandTrustedSigners(s []interface{}) *cloudfront.TrustedSigners { var ts cloudfront.TrustedSigners if len(s) > 0 { @@ -395,6 +447,14 @@ func lambdaFunctionAssociationHash(v interface{}) int { return hashcode.String(buf.String()) } +func functionAssociationHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["event_type"].(string))) + buf.WriteString(m["function_arn"].(string)) + return hashcode.String(buf.String()) +} + func expandLambdaFunctionAssociations(v interface{}) *cloudfront.LambdaFunctionAssociations { if v == nil { return &cloudfront.LambdaFunctionAssociations{ @@ -426,6 +486,34 @@ func expandLambdaFunctionAssociation(lf map[string]interface{}) *cloudfront.Lamb return &lfa } +func expandFunctionAssociations(v interface{}) *cloudfront.FunctionAssociations { + if v == nil { + return &cloudfront.FunctionAssociations{ + Quantity: aws.Int64(0), + } + } + + s := v.([]interface{}) + var fa cloudfront.FunctionAssociations + fa.Quantity = aws.Int64(int64(len(s))) + fa.Items = make([]*cloudfront.FunctionAssociation, len(s)) + for i, f := range s { + fa.Items[i] = expandFunctionAssociation(f.(map[string]interface{})) + } + return &fa +} + +func expandFunctionAssociation(f map[string]interface{}) *cloudfront.FunctionAssociation { + var fa cloudfront.FunctionAssociation + if v, ok := f["event_type"]; ok { + fa.EventType = aws.String(v.(string)) + } + if v, ok := f["function_arn"]; ok { + fa.FunctionARN = aws.String(v.(string)) + } + return &fa +} + func flattenLambdaFunctionAssociations(lfa *cloudfront.LambdaFunctionAssociations) *schema.Set { s := schema.NewSet(lambdaFunctionAssociationHash, []interface{}{}) for _, v := range lfa.Items { @@ -444,6 +532,23 @@ func flattenLambdaFunctionAssociation(lfa *cloudfront.LambdaFunctionAssociation) return m } +func flattenFunctionAssociations(fa *cloudfront.FunctionAssociations) *schema.Set { + s := schema.NewSet(functionAssociationHash, []interface{}{}) + for _, v := range fa.Items { + s.Add(flattenFunctionAssociation(v)) + } + return s +} + +func flattenFunctionAssociation(fa *cloudfront.FunctionAssociation) map[string]interface{} { + m := map[string]interface{}{} + if fa != nil { + m["event_type"] = aws.StringValue(fa.EventType) + m["function_arn"] = aws.StringValue(fa.FunctionARN) + } + return m +} + func expandForwardedValues(m map[string]interface{}) *cloudfront.ForwardedValues { if len(m) < 1 { return nil @@ -594,6 +699,13 @@ func expandOrigin(m map[string]interface{}) *cloudfront.Origin { Id: aws.String(m["origin_id"].(string)), DomainName: aws.String(m["domain_name"].(string)), } + + if v, ok := m["connection_attempts"]; ok { + origin.ConnectionAttempts = aws.Int64(int64(v.(int))) + } + if v, ok := m["connection_timeout"]; ok { + origin.ConnectionTimeout = aws.Int64(int64(v.(int))) + } if v, ok := m["custom_header"]; ok { origin.CustomHeaders = expandCustomHeaders(v.(*schema.Set)) } @@ -605,6 +717,13 @@ func expandOrigin(m map[string]interface{}) *cloudfront.Origin { if v, ok := m["origin_path"]; ok { origin.OriginPath = aws.String(v.(string)) } + + if v, ok := m["origin_shield"]; ok { + if s := v.([]interface{}); len(s) > 0 { + origin.OriginShield = expandOriginShield(s[0].(map[string]interface{})) + } + } + if v, ok := m["s3_origin_config"]; ok { if s := v.([]interface{}); len(s) > 0 { origin.S3OriginConfig = expandS3OriginConfig(s[0].(map[string]interface{})) @@ -626,6 +745,12 @@ func flattenOrigin(or *cloudfront.Origin) map[string]interface{} { m := make(map[string]interface{}) m["origin_id"] = aws.StringValue(or.Id) m["domain_name"] = aws.StringValue(or.DomainName) + if or.ConnectionAttempts != nil { + m["connection_attempts"] = int(aws.Int64Value(or.ConnectionAttempts)) + } + if or.ConnectionTimeout != nil { + m["connection_timeout"] = int(aws.Int64Value(or.ConnectionTimeout)) + } if or.CustomHeaders != nil { m["custom_header"] = flattenCustomHeaders(or.CustomHeaders) } @@ -635,6 +760,9 @@ func flattenOrigin(or *cloudfront.Origin) map[string]interface{} { if or.OriginPath != nil { m["origin_path"] = aws.StringValue(or.OriginPath) } + if or.OriginShield != nil && aws.BoolValue(or.OriginShield.Enabled) { + m["origin_shield"] = []interface{}{flattenOriginShield(or.OriginShield)} + } if or.S3OriginConfig != nil && aws.StringValue(or.S3OriginConfig.OriginAccessIdentity) != "" { m["s3_origin_config"] = []interface{}{flattenS3OriginConfig(or.S3OriginConfig)} } @@ -746,6 +874,12 @@ func originHash(v interface{}) int { m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["origin_id"].(string))) buf.WriteString(fmt.Sprintf("%s-", m["domain_name"].(string))) + if v, ok := m["connection_attempts"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + if v, ok := m["connection_timeout"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } if v, ok := m["custom_header"]; ok { buf.WriteString(fmt.Sprintf("%d-", customHeadersHash(v.(*schema.Set)))) } @@ -757,6 +891,13 @@ func originHash(v interface{}) int { if v, ok := m["origin_path"]; ok { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } + + if v, ok := m["origin_shield"]; ok { + if s := v.([]interface{}); len(s) > 0 && s[0] != nil { + buf.WriteString(fmt.Sprintf("%d-", originShieldHash((s[0].(map[string]interface{}))))) + } + } + if v, ok := m["s3_origin_config"]; ok { if s := v.([]interface{}); len(s) > 0 && s[0] != nil { buf.WriteString(fmt.Sprintf("%d-", s3OriginConfigHash((s[0].(map[string]interface{}))))) @@ -921,12 +1062,26 @@ func expandS3OriginConfig(m map[string]interface{}) *cloudfront.S3OriginConfig { } } +func expandOriginShield(m map[string]interface{}) *cloudfront.OriginShield { + return &cloudfront.OriginShield{ + Enabled: aws.Bool(m["enabled"].(bool)), + OriginShieldRegion: aws.String(m["origin_shield_region"].(string)), + } +} + func flattenS3OriginConfig(s3o *cloudfront.S3OriginConfig) map[string]interface{} { return map[string]interface{}{ "origin_access_identity": aws.StringValue(s3o.OriginAccessIdentity), } } +func flattenOriginShield(o *cloudfront.OriginShield) map[string]interface{} { + return map[string]interface{}{ + "origin_shield_region": aws.StringValue(o.OriginShieldRegion), + "enabled": aws.BoolValue(o.Enabled), + } +} + // Assemble the hash for the aws_cloudfront_distribution s3_origin_config // TypeSet attribute. func s3OriginConfigHash(v interface{}) int { @@ -936,6 +1091,14 @@ func s3OriginConfigHash(v interface{}) int { return hashcode.String(buf.String()) } +func originShieldHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["enabled"].(bool))) + buf.WriteString(fmt.Sprintf("%s-", m["origin_shield_region"].(string))) + return hashcode.String(buf.String()) +} + func expandCustomErrorResponses(s *schema.Set) *cloudfront.CustomErrorResponses { qty := 0 items := []*cloudfront.CustomErrorResponse{} @@ -1133,6 +1296,34 @@ func flattenViewerCertificate(vc *cloudfront.ViewerCertificate) []interface{} { return []interface{}{m} } +func flattenCloudfrontActiveTrustedKeyGroups(atkg *cloudfront.ActiveTrustedKeyGroups) []interface{} { + if atkg == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "enabled": aws.BoolValue(atkg.Enabled), + "items": flattenCloudfrontKGKeyPairIds(atkg.Items), + } + + return []interface{}{m} +} + +func flattenCloudfrontKGKeyPairIds(keyPairIds []*cloudfront.KGKeyPairIds) []interface{} { + result := make([]interface{}, 0, len(keyPairIds)) + + for _, keyPairId := range keyPairIds { + m := map[string]interface{}{ + "key_group_id": aws.StringValue(keyPairId.KeyGroupId), + "key_pair_ids": aws.StringValueSlice(keyPairId.KeyPairIds.Items), + } + + result = append(result, m) + } + + return result +} + func flattenCloudfrontActiveTrustedSigners(ats *cloudfront.ActiveTrustedSigners) []interface{} { if ats == nil { return []interface{}{} diff --git a/aws/cloudfront_distribution_configuration_structure_test.go b/aws/cloudfront_distribution_configuration_structure_test.go index aafcf63adb3e..b1ffdc2e8593 100644 --- a/aws/cloudfront_distribution_configuration_structure_test.go +++ b/aws/cloudfront_distribution_configuration_structure_test.go @@ -18,6 +18,7 @@ func defaultCacheBehaviorConf() map[string]interface{} { "min_ttl": 0, "trusted_signers": trustedSignersConf(), "lambda_function_association": lambdaFunctionAssociationsConf(), + "function_association": functionAssociationsConf(), "max_ttl": 31536000, "smooth_streaming": false, "default_ttl": 86400, @@ -51,6 +52,21 @@ func lambdaFunctionAssociationsConf() *schema.Set { return schema.NewSet(lambdaFunctionAssociationHash, x) } +func functionAssociationsConf() *schema.Set { + x := []interface{}{ + map[string]interface{}{ + "event_type": "viewer-request", + "function_arn": "arn:aws:cloudfront::999999999:function/function1", //lintignore:AWSAT003,AWSAT005 + }, + map[string]interface{}{ + "event_type": "viewer-response", + "function_arn": "arn:aws:cloudfront::999999999:function/function2", //lintignore:AWSAT003,AWSAT005 + }, + } + + return schema.NewSet(functionAssociationHash, x) +} + func forwardedValuesConf() map[string]interface{} { return map[string]interface{}{ "query_string": true, @@ -120,6 +136,13 @@ func customOriginSslProtocolsConf() *schema.Set { return schema.NewSet(schema.HashString, []interface{}{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}) } +func originShield() map[string]interface{} { + return map[string]interface{}{ + "enabled": true, + "origin_shield_region": "testRegion", + } +} + func s3OriginConf() map[string]interface{} { return map[string]interface{}{ "origin_access_identity": "origin-access-identity/cloudfront/E127EXAMPLE51Z", @@ -135,6 +158,7 @@ func originWithCustomConf() map[string]interface{} { "custom_header": originCustomHeadersConf(), } } + func originWithS3Conf() map[string]interface{} { return map[string]interface{}{ "origin_id": "S3Origin", @@ -304,6 +328,9 @@ func TestCloudFrontStructure_expandCloudFrontDefaultCacheBehavior(t *testing.T) if *dcb.LambdaFunctionAssociations.Quantity != 2 { t.Fatalf("Expected LambdaFunctionAssociations to be 2, got %v", *dcb.LambdaFunctionAssociations.Quantity) } + if *dcb.FunctionAssociations.Quantity != 2 { + t.Fatalf("Expected FunctionAssociations to be 2, got %v", *dcb.FunctionAssociations.Quantity) + } if !reflect.DeepEqual(dcb.AllowedMethods.Items, expandStringSet(allowedMethodsConf())) { t.Fatalf("Expected AllowedMethods.Items to be %v, got %v", allowedMethodsConf().List(), dcb.AllowedMethods.Items) } @@ -391,6 +418,47 @@ func TestCloudFrontStructure_expandlambdaFunctionAssociations_empty(t *testing.T } } +func TestCloudFrontStructure_expandFunctionAssociations(t *testing.T) { + data := functionAssociationsConf() + lfa := expandFunctionAssociations(data.List()) + if *lfa.Quantity != 2 { + t.Fatalf("Expected Quantity to be 2, got %v", *lfa.Quantity) + } + if len(lfa.Items) != 2 { + t.Fatalf("Expected Items to be len 2, got %v", len(lfa.Items)) + } + if et := "viewer-response"; *lfa.Items[0].EventType != et { + t.Fatalf("Expected first Item's EventType to be %q, got %q", et, *lfa.Items[0].EventType) + } + if et := "viewer-request"; *lfa.Items[1].EventType != et { + t.Fatalf("Expected second Item's EventType to be %q, got %q", et, *lfa.Items[1].EventType) + } +} + +func TestCloudFrontStructure_flattenFunctionAssociations(t *testing.T) { + in := functionAssociationsConf() + lfa := expandFunctionAssociations(in.List()) + out := flattenFunctionAssociations(lfa) + + if !reflect.DeepEqual(in.List(), out.List()) { + t.Fatalf("Expected out to be %v, got %v", in, out) + } +} + +func TestCloudFrontStructure_expandFunctionAssociations_empty(t *testing.T) { + data := new(schema.Set) + lfa := expandFunctionAssociations(data.List()) + if *lfa.Quantity != 0 { + t.Fatalf("Expected Quantity to be 0, got %v", *lfa.Quantity) + } + if len(lfa.Items) != 0 { + t.Fatalf("Expected Items to be len 0, got %v", len(lfa.Items)) + } + if !reflect.DeepEqual(lfa.Items, []*cloudfront.FunctionAssociation{}) { + t.Fatalf("Expected Items to be empty, got %v", lfa.Items) + } +} + func TestCloudFrontStructure_expandForwardedValues(t *testing.T) { data := forwardedValuesConf() fv := expandForwardedValues(data) @@ -756,6 +824,27 @@ func TestCloudFrontStructure_flattenCustomOriginConfigSSL(t *testing.T) { } } +func TestCloudFrontStructure_expandOriginShield(t *testing.T) { + data := originShield() + o := expandOriginShield(data) + if *o.Enabled != true { + t.Fatalf("Expected Enabled to be true, got %v", *o.Enabled) + } + if *o.OriginShieldRegion != "testRegion" { + t.Fatalf("Expected OriginShieldRegion to be testRegion, got %v", *o.OriginShieldRegion) + } +} + +func TestCloudFrontStructure_flattenOriginShield(t *testing.T) { + in := originShield() + o := expandOriginShield(in) + out := flattenOriginShield(o) + + if !reflect.DeepEqual(in, out) { + t.Fatalf("Expected out to be %v, got %v", in, out) + } +} + func TestCloudFrontStructure_expandS3OriginConfig(t *testing.T) { data := s3OriginConf() s3o := expandS3OriginConfig(data) diff --git a/aws/cognito_user_pool_domain_test.go b/aws/cognito_user_pool_domain_test.go index 9264b46735d0..cb82bce2bb8c 100644 --- a/aws/cognito_user_pool_domain_test.go +++ b/aws/cognito_user_pool_domain_test.go @@ -34,17 +34,17 @@ var testAccProviderCognitoUserPoolCustomDomainConfigure sync.Once func testAccPreCheckCognitoUserPoolCustomDomain(t *testing.T) { testAccPartitionHasServicePreCheck(cognitoidentityprovider.EndpointsID, t) + region := testAccGetCognitoUserPoolCustomDomainRegion() + + if region == "" { + t.Skip("Cognito User Pool Custom Domains not available in this AWS Partition") + } + // Since we are outside the scope of the Terraform configuration we must // call Configure() to properly initialize the provider configuration. testAccProviderCognitoUserPoolCustomDomainConfigure.Do(func() { testAccProviderCognitoUserPoolCustomDomain = Provider() - region := testAccGetCognitoUserPoolCustomDomainRegion() - - if region == "" { - t.Skip("Cognito User Pool Custom Domains not available in this AWS Partition") - } - config := map[string]interface{}{ "region": region, } diff --git a/aws/config.go b/aws/config.go index 0493b8d71a08..e1de3978be5c 100644 --- a/aws/config.go +++ b/aws/config.go @@ -14,18 +14,23 @@ import ( "github.com/aws/aws-sdk-go/service/amplify" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/aws/aws-sdk-go/service/appconfig" "github.com/aws/aws-sdk-go/service/applicationautoscaling" "github.com/aws/aws-sdk-go/service/applicationinsights" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/aws/aws-sdk-go/service/apprunner" "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/auditmanager" "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/autoscalingplans" "github.com/aws/aws-sdk-go/service/backup" "github.com/aws/aws-sdk-go/service/batch" "github.com/aws/aws-sdk-go/service/budgets" + "github.com/aws/aws-sdk-go/service/chime" "github.com/aws/aws-sdk-go/service/cloud9" + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudfront" "github.com/aws/aws-sdk-go/service/cloudhsmv2" @@ -51,6 +56,7 @@ import ( "github.com/aws/aws-sdk-go/service/datapipeline" "github.com/aws/aws-sdk-go/service/datasync" "github.com/aws/aws-sdk-go/service/dax" + "github.com/aws/aws-sdk-go/service/detective" "github.com/aws/aws-sdk-go/service/devicefarm" "github.com/aws/aws-sdk-go/service/directconnect" "github.com/aws/aws-sdk-go/service/directoryservice" @@ -99,6 +105,7 @@ import ( "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" "github.com/aws/aws-sdk-go/service/licensemanager" "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go/service/locationservice" "github.com/aws/aws-sdk-go/service/macie" "github.com/aws/aws-sdk-go/service/macie2" "github.com/aws/aws-sdk-go/service/managedblockchain" @@ -109,6 +116,7 @@ import ( "github.com/aws/aws-sdk-go/service/mediapackage" "github.com/aws/aws-sdk-go/service/mediastore" "github.com/aws/aws-sdk-go/service/mediastoredata" + "github.com/aws/aws-sdk-go/service/memorydb" "github.com/aws/aws-sdk-go/service/mq" "github.com/aws/aws-sdk-go/service/mwaa" "github.com/aws/aws-sdk-go/service/neptune" @@ -130,11 +138,14 @@ import ( "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/route53domains" + "github.com/aws/aws-sdk-go/service/route53recoverycontrolconfig" + "github.com/aws/aws-sdk-go/service/route53recoveryreadiness" "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3control" "github.com/aws/aws-sdk-go/service/s3outposts" "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/aws/aws-sdk-go/service/schemas" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/securityhub" "github.com/aws/aws-sdk-go/service/serverlessapplicationrepository" @@ -191,9 +202,11 @@ type Config struct { AllowedAccountIds []string ForbiddenAccountIds []string - Endpoints map[string]string - IgnoreTagsConfig *keyvaluetags.IgnoreConfig - Insecure bool + DefaultTagsConfig *keyvaluetags.DefaultConfig + Endpoints map[string]string + IgnoreTagsConfig *keyvaluetags.IgnoreConfig + Insecure bool + HTTPProxy string SkipCredsValidation bool SkipGetEC2Platforms bool @@ -214,18 +227,23 @@ type AWSClient struct { apigatewayconn *apigateway.APIGateway apigatewayv2conn *apigatewayv2.ApiGatewayV2 appautoscalingconn *applicationautoscaling.ApplicationAutoScaling + appconfigconn *appconfig.AppConfig applicationinsightsconn *applicationinsights.ApplicationInsights appmeshconn *appmesh.AppMesh + apprunnerconn *apprunner.AppRunner appstreamconn *appstream.AppStream appsyncconn *appsync.AppSync athenaconn *athena.Athena + auditmanagerconn *auditmanager.AuditManager autoscalingconn *autoscaling.AutoScaling autoscalingplansconn *autoscalingplans.AutoScalingPlans backupconn *backup.Backup batchconn *batch.Batch budgetconn *budgets.Budgets cfconn *cloudformation.CloudFormation + chimeconn *chime.Chime cloud9conn *cloud9.Cloud9 + cloudcontrolapiconn *cloudcontrolapi.CloudControlApi cloudfrontconn *cloudfront.CloudFront cloudhsmv2conn *cloudhsmv2.CloudHSMV2 cloudsearchconn *cloudsearch.CloudSearch @@ -249,6 +267,8 @@ type AWSClient struct { datapipelineconn *datapipeline.DataPipeline datasyncconn *datasync.DataSync daxconn *dax.DAX + DefaultTagsConfig *keyvaluetags.DefaultConfig + detectiveconn *detective.Detective devicefarmconn *devicefarm.DeviceFarm dlmconn *dlm.DLM dmsconn *databasemigrationservice.DatabaseMigrationService @@ -300,6 +320,7 @@ type AWSClient struct { lexmodelconn *lexmodelbuildingservice.LexModelBuildingService licensemanagerconn *licensemanager.LicenseManager lightsailconn *lightsail.Lightsail + locationconn *locationservice.LocationService macieconn *macie.Macie macie2conn *macie2.Macie2 managedblockchainconn *managedblockchain.ManagedBlockchain @@ -311,6 +332,7 @@ type AWSClient struct { mediapackageconn *mediapackage.MediaPackage mediastoreconn *mediastore.MediaStore mediastoredataconn *mediastoredata.MediaStoreData + memorydbconn *memorydb.MemoryDB mqconn *mq.MQ mwaaconn *mwaa.MWAA neptuneconn *neptune.Neptune @@ -335,6 +357,8 @@ type AWSClient struct { resourcegroupstaggingapiconn *resourcegroupstaggingapi.ResourceGroupsTaggingAPI reverseDnsPrefix string route53domainsconn *route53domains.Route53Domains + route53recoverycontrolconfigconn *route53recoverycontrolconfig.Route53RecoveryControlConfig + route53recoveryreadinessconn *route53recoveryreadiness.Route53RecoveryReadiness route53resolverconn *route53resolver.Route53Resolver s3conn *s3.S3 s3connUriCleaningDisabled *s3.S3 @@ -342,6 +366,7 @@ type AWSClient struct { s3outpostsconn *s3outposts.S3Outposts sagemakerconn *sagemaker.SageMaker scconn *servicecatalog.ServiceCatalog + schemasconn *schemas.Schemas sdconn *servicediscovery.ServiceDiscovery secretsmanagerconn *secretsmanager.SecretsManager securityhubconn *securityhub.SecurityHub @@ -413,6 +438,7 @@ func (c *Config) Client() (interface{}, error) { DebugLogging: logging.IsDebugOrHigher(), IamEndpoint: c.Endpoints["iam"], Insecure: c.Insecure, + HTTPProxy: c.HTTPProxy, MaxRetries: c.MaxRetries, Profile: c.Profile, Region: c.Region, @@ -457,18 +483,23 @@ func (c *Config) Client() (interface{}, error) { apigatewayconn: apigateway.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["apigateway"])})), apigatewayv2conn: apigatewayv2.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["apigateway"])})), appautoscalingconn: applicationautoscaling.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["applicationautoscaling"])})), + appconfigconn: appconfig.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["appconfig"])})), applicationinsightsconn: applicationinsights.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["applicationinsights"])})), appmeshconn: appmesh.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["appmesh"])})), + apprunnerconn: apprunner.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["apprunner"])})), appstreamconn: appstream.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["appstream"])})), appsyncconn: appsync.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["appsync"])})), athenaconn: athena.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["athena"])})), + auditmanagerconn: auditmanager.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["auditmanager"])})), autoscalingconn: autoscaling.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["autoscaling"])})), autoscalingplansconn: autoscalingplans.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["autoscalingplans"])})), backupconn: backup.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["backup"])})), batchconn: batch.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["batch"])})), budgetconn: budgets.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["budgets"])})), cfconn: cloudformation.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloudformation"])})), + chimeconn: chime.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["chime"])})), cloud9conn: cloud9.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloud9"])})), + cloudcontrolapiconn: cloudcontrolapi.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloudcontrolapi"])})), cloudfrontconn: cloudfront.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloudfront"])})), cloudhsmv2conn: cloudhsmv2.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloudhsm"])})), cloudsearchconn: cloudsearch.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["cloudsearch"])})), @@ -492,6 +523,8 @@ func (c *Config) Client() (interface{}, error) { datapipelineconn: datapipeline.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["datapipeline"])})), datasyncconn: datasync.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["datasync"])})), daxconn: dax.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dax"])})), + DefaultTagsConfig: c.DefaultTagsConfig, + detectiveconn: detective.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["detective"])})), devicefarmconn: devicefarm.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["devicefarm"])})), dlmconn: dlm.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dlm"])})), dmsconn: databasemigrationservice.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dms"])})), @@ -542,6 +575,7 @@ func (c *Config) Client() (interface{}, error) { lexmodelconn: lexmodelbuildingservice.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["lexmodels"])})), licensemanagerconn: licensemanager.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["licensemanager"])})), lightsailconn: lightsail.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["lightsail"])})), + locationconn: locationservice.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["location"])})), macieconn: macie.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["macie"])})), macie2conn: macie2.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["macie2"])})), managedblockchainconn: managedblockchain.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["managedblockchain"])})), @@ -552,6 +586,7 @@ func (c *Config) Client() (interface{}, error) { mediapackageconn: mediapackage.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["mediapackage"])})), mediastoreconn: mediastore.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["mediastore"])})), mediastoredataconn: mediastoredata.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["mediastoredata"])})), + memorydbconn: memorydb.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["memorydb"])})), mqconn: mq.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["mq"])})), mwaaconn: mwaa.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["mwaa"])})), neptuneconn: neptune.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["neptune"])})), @@ -575,11 +610,14 @@ func (c *Config) Client() (interface{}, error) { resourcegroupstaggingapiconn: resourcegroupstaggingapi.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["resourcegroupstaggingapi"])})), reverseDnsPrefix: ReverseDns(dnsSuffix), route53domainsconn: route53domains.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["route53domains"])})), + route53recoverycontrolconfigconn: route53recoverycontrolconfig.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["route53recoverycontrolconfig"])})), + route53recoveryreadinessconn: route53recoveryreadiness.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["route53recoveryreadiness"])})), route53resolverconn: route53resolver.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["route53resolver"])})), s3controlconn: s3control.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["s3control"])})), s3outpostsconn: s3outposts.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["s3outposts"])})), sagemakerconn: sagemaker.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["sagemaker"])})), scconn: servicecatalog.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["servicecatalog"])})), + schemasconn: schemas.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["schemas"])})), sdconn: servicediscovery.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["servicediscovery"])})), secretsmanagerconn: secretsmanager.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["secretsmanager"])})), securityhubconn: securityhub.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["securityhub"])})), @@ -616,6 +654,12 @@ func (c *Config) Client() (interface{}, error) { route53Config := &aws.Config{ Endpoint: aws.String(c.Endpoints["route53"]), } + route53RecoveryControlConfigConfig := &aws.Config{ + Endpoint: aws.String(c.Endpoints["route53recoverycontrolconfig"]), + } + route53RecoveryReadinessConfig := &aws.Config{ + Endpoint: aws.String(c.Endpoints["route53recoveryreadiness"]), + } shieldConfig := &aws.Config{ Endpoint: aws.String(c.Endpoints["shield"]), } @@ -636,6 +680,8 @@ func (c *Config) Client() (interface{}, error) { case endpoints.AwsPartitionID: globalAcceleratorConfig.Region = aws.String(endpoints.UsWest2RegionID) route53Config.Region = aws.String(endpoints.UsEast1RegionID) + route53RecoveryControlConfigConfig.Region = aws.String(endpoints.UsWest2RegionID) + route53RecoveryReadinessConfig.Region = aws.String(endpoints.UsWest2RegionID) shieldConfig.Region = aws.String(endpoints.UsEast1RegionID) case endpoints.AwsCnPartitionID: // The AWS Go SDK is missing endpoint information for Route 53 in the AWS China partition. @@ -650,6 +696,8 @@ func (c *Config) Client() (interface{}, error) { client.globalacceleratorconn = globalaccelerator.New(sess.Copy(globalAcceleratorConfig)) client.r53conn = route53.New(sess.Copy(route53Config)) + client.route53recoverycontrolconfigconn = route53recoverycontrolconfig.New(sess.Copy(route53RecoveryControlConfigConfig)) + client.route53recoveryreadinessconn = route53recoveryreadiness.New(sess.Copy(route53RecoveryReadinessConfig)) client.shieldconn = shield.New(sess.Copy(shieldConfig)) client.apigatewayconn.Handlers.Retry.PushBack(func(r *request.Request) { @@ -671,6 +719,17 @@ func (c *Config) Client() (interface{}, error) { } }) + // StartDeployment operations can return a ConflictException + // if ongoing deployments are in-progress, thus we handle them + // here for the service client. + client.appconfigconn.Handlers.Retry.PushBack(func(r *request.Request) { + if r.Operation.Name == "StartDeployment" { + if tfawserr.ErrCodeEquals(r.Error, appconfig.ErrCodeConflictException) { + r.Retryable = aws.Bool(true) + } + } + }) + client.appsyncconn.Handlers.Retry.PushBack(func(r *request.Request) { if r.Operation.Name == "CreateGraphqlApi" { if isAWSErr(r.Error, appsync.ErrCodeConcurrentModificationException, "a GraphQL API creation is already in progress") { @@ -679,6 +738,22 @@ func (c *Config) Client() (interface{}, error) { } }) + client.chimeconn.Handlers.Retry.PushBack(func(r *request.Request) { + // When calling CreateVoiceConnector across multiple resources, + // the API can randomly return a BadRequestException without explanation + if r.Operation.Name == "CreateVoiceConnector" { + if tfawserr.ErrMessageContains(r.Error, chime.ErrCodeBadRequestException, "Service received a bad request") { + r.Retryable = aws.Bool(true) + } + } + }) + + client.cloudhsmv2conn.Handlers.Retry.PushBack(func(r *request.Request) { + if tfawserr.ErrMessageContains(r.Error, cloudhsmv2.ErrCodeCloudHsmInternalFailureException, "request was rejected because of an AWS CloudHSM internal failure") { + r.Retryable = aws.Bool(true) + } + }) + client.configconn.Handlers.Retry.PushBack(func(r *request.Request) { // When calling Config Organization Rules API actions immediately // after Organization creation, the API can randomly return the @@ -690,6 +765,23 @@ func (c *Config) Client() (interface{}, error) { return } + // We only want to retry briefly as the default max retry count would + // excessively retry when the error could be legitimate. + // We currently depend on the DefaultRetryer exponential backoff here. + // ~10 retries gives a fair backoff of a few seconds. + if r.RetryCount < 9 { + r.Retryable = aws.Bool(true) + } else { + r.Retryable = aws.Bool(false) + } + case "DeleteOrganizationConformancePack", "DescribeOrganizationConformancePacks", "DescribeOrganizationConformancePackStatuses", "PutOrganizationConformancePack": + if !tfawserr.ErrCodeEquals(r.Error, configservice.ErrCodeOrganizationAccessDeniedException) { + if r.Operation.Name == "DeleteOrganizationConformancePack" && tfawserr.ErrCodeEquals(err, configservice.ErrCodeResourceInUseException) { + r.Retryable = aws.Bool(true) + } + return + } + // We only want to retry briefly as the default max retry count would // excessively retry when the error could be legitimate. // We currently depend on the DefaultRetryer exponential backoff here. @@ -702,6 +794,12 @@ func (c *Config) Client() (interface{}, error) { } }) + client.cfconn.Handlers.Retry.PushBack(func(r *request.Request) { + if isAWSErr(r.Error, cloudformation.ErrCodeOperationInProgressException, "Another Operation on StackSet") { + r.Retryable = aws.Bool(true) + } + }) + // See https://github.com/aws/aws-sdk-go/pull/1276 client.dynamodbconn.Handlers.Retry.PushBack(func(r *request.Request) { if r.Operation.Name != "PutItem" && r.Operation.Name != "UpdateItem" && r.Operation.Name != "DeleteItem" { @@ -738,6 +836,23 @@ func (c *Config) Client() (interface{}, error) { } }) + client.fmsconn.Handlers.Retry.PushBack(func(r *request.Request) { + // Acceptance testing creates and deletes resources in quick succession. + // The FMS onboarding process into Organizations is opaque to consumers. + // Since we cannot reasonably check this status before receiving the error, + // set the operation as retryable. + switch r.Operation.Name { + case "AssociateAdminAccount": + if tfawserr.ErrMessageContains(r.Error, fms.ErrCodeInvalidOperationException, "Your AWS Organization is currently offboarding with AWS Firewall Manager. Please submit onboard request after offboarded.") { + r.Retryable = aws.Bool(true) + } + case "DisassociateAdminAccount": + if tfawserr.ErrMessageContains(r.Error, fms.ErrCodeInvalidOperationException, "Your AWS Organization is currently onboarding with AWS Firewall Manager and cannot be offboarded.") { + r.Retryable = aws.Bool(true) + } + } + }) + client.kafkaconn.Handlers.Retry.PushBack(func(r *request.Request) { if isAWSErr(r.Error, kafka.ErrCodeTooManyRequestsException, "Too Many Requests") { r.Retryable = aws.Bool(true) @@ -765,6 +880,25 @@ func (c *Config) Client() (interface{}, error) { } }) + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/17996 + client.securityhubconn.Handlers.Retry.PushBack(func(r *request.Request) { + switch r.Operation.Name { + case "EnableOrganizationAdminAccount": + if tfawserr.ErrCodeEquals(r.Error, securityhub.ErrCodeResourceConflictException) { + r.Retryable = aws.Bool(true) + } + } + }) + + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/19215 + client.ssoadminconn.Handlers.Retry.PushBack(func(r *request.Request) { + if r.Operation.Name == "AttachManagedPolicyToPermissionSet" || r.Operation.Name == "DetachManagedPolicyFromPermissionSet" { + if tfawserr.ErrCodeEquals(r.Error, ssoadmin.ErrCodeConflictException) { + r.Retryable = aws.Bool(true) + } + } + }) + client.storagegatewayconn.Handlers.Retry.PushBack(func(r *request.Request) { // InvalidGatewayRequestException: The specified gateway proxy network connection is busy. if isAWSErr(r.Error, storagegateway.ErrCodeInvalidGatewayRequestException, "The specified gateway proxy network connection is busy") { diff --git a/aws/configservice.go b/aws/configservice.go index 712f43cf189a..e9eef1aeabff 100644 --- a/aws/configservice.go +++ b/aws/configservice.go @@ -135,6 +135,62 @@ func configDescribeOrganizationConfigRuleStatus(conn *configservice.ConfigServic return nil, nil } +func configDescribeOrganizationConformancePack(conn *configservice.ConfigService, name string) (*configservice.OrganizationConformancePack, error) { + input := &configservice.DescribeOrganizationConformancePacksInput{ + OrganizationConformancePackNames: []*string{aws.String(name)}, + } + + for { + output, err := conn.DescribeOrganizationConformancePacks(input) + + if err != nil { + return nil, err + } + + for _, pack := range output.OrganizationConformancePacks { + if aws.StringValue(pack.OrganizationConformancePackName) == name { + return pack, nil + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + return nil, nil +} + +func configDescribeOrganizationConformancePackStatus(conn *configservice.ConfigService, name string) (*configservice.OrganizationConformancePackStatus, error) { + input := &configservice.DescribeOrganizationConformancePackStatusesInput{ + OrganizationConformancePackNames: []*string{aws.String(name)}, + } + + for { + output, err := conn.DescribeOrganizationConformancePackStatuses(input) + + if err != nil { + return nil, err + } + + for _, status := range output.OrganizationConformancePackStatuses { + if aws.StringValue(status.OrganizationConformancePackName) == name { + return status, nil + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + return nil, nil +} + func configGetOrganizationConfigRuleDetailedStatus(conn *configservice.ConfigService, ruleName, ruleStatus string) ([]*configservice.MemberAccountStatus, error) { input := &configservice.GetOrganizationConfigRuleDetailedStatusInput{ Filters: &configservice.StatusDetailFilters{ @@ -163,6 +219,35 @@ func configGetOrganizationConfigRuleDetailedStatus(conn *configservice.ConfigSer return statuses, nil } +func configGetOrganizationConformancePackDetailedStatus(conn *configservice.ConfigService, name, status string) ([]*configservice.OrganizationConformancePackDetailedStatus, error) { + input := &configservice.GetOrganizationConformancePackDetailedStatusInput{ + Filters: &configservice.OrganizationResourceDetailedStatusFilters{ + Status: aws.String(status), + }, + OrganizationConformancePackName: aws.String(name), + } + + var statuses []*configservice.OrganizationConformancePackDetailedStatus + + for { + output, err := conn.GetOrganizationConformancePackDetailedStatus(input) + + if err != nil { + return nil, err + } + + statuses = append(statuses, output.OrganizationConformancePackDetailedStatuses...) + + if aws.StringValue(output.NextToken) == "" { + break + } + + input.NextToken = output.NextToken + } + + return statuses, nil +} + func configRefreshConformancePackStatus(conn *configservice.ConfigService, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { status, err := configDescribeConformancePackStatus(conn, name) @@ -221,6 +306,78 @@ func configRefreshOrganizationConfigRuleStatus(conn *configservice.ConfigService } } +func configRefreshOrganizationConformancePackCreationStatus(conn *configservice.ConfigService, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + status, err := configDescribeOrganizationConformancePackStatus(conn, name) + + // Transient ResourceDoesNotExist error after creation caught here + // in cases where the StateChangeConf's delay time is not sufficient + if tfawserr.ErrCodeEquals(err, configservice.ErrCodeNoSuchOrganizationConformancePackException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if status == nil { + return nil, "", nil + } + + if status.ErrorCode != nil { + return status, aws.StringValue(status.Status), fmt.Errorf("%s: %s", aws.StringValue(status.ErrorCode), aws.StringValue(status.ErrorMessage)) + } + + switch s := aws.StringValue(status.Status); s { + case configservice.OrganizationResourceStatusCreateFailed, configservice.OrganizationResourceStatusDeleteFailed, configservice.OrganizationResourceStatusUpdateFailed: + return status, s, configOrganizationConformancePackDetailedStatusError(conn, name, s) + } + + return status, aws.StringValue(status.Status), nil + } +} + +func configRefreshOrganizationConformancePackStatus(conn *configservice.ConfigService, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + status, err := configDescribeOrganizationConformancePackStatus(conn, name) + + if err != nil { + return nil, "", err + } + + if status == nil { + return nil, "", nil + } + + if status.ErrorCode != nil { + return status, aws.StringValue(status.Status), fmt.Errorf("%s: %s", aws.StringValue(status.ErrorCode), aws.StringValue(status.ErrorMessage)) + } + + switch s := aws.StringValue(status.Status); s { + case configservice.OrganizationResourceStatusCreateFailed, configservice.OrganizationResourceStatusDeleteFailed, configservice.OrganizationResourceStatusUpdateFailed: + return status, s, configOrganizationConformancePackDetailedStatusError(conn, name, s) + } + + return status, aws.StringValue(status.Status), nil + } +} + +func configOrganizationConformancePackDetailedStatusError(conn *configservice.ConfigService, name, status string) error { + memberAccountStatuses, err := configGetOrganizationConformancePackDetailedStatus(conn, name, status) + + if err != nil { + return fmt.Errorf("unable to get Config Organization Conformance Pack detailed status for showing member account errors: %w", err) + } + + var errBuilder strings.Builder + + for _, mas := range memberAccountStatuses { + errBuilder.WriteString(fmt.Sprintf("Account ID (%s): %s: %s\n", aws.StringValue(mas.AccountId), aws.StringValue(mas.ErrorCode), aws.StringValue(mas.ErrorMessage))) + } + + return fmt.Errorf("Failed in %d account(s):\n\n%s", len(memberAccountStatuses), errBuilder.String()) +} + func configWaitForConformancePackStateCreateComplete(conn *configservice.ConfigService, name string) error { stateChangeConf := resource.StateChangeConf{ Pending: []string{configservice.ConformancePackStateCreateInProgress}, @@ -256,6 +413,48 @@ func configWaitForConformancePackStateDeleteComplete(conn *configservice.ConfigS return err } +func configWaitForOrganizationConformancePackStatusCreateSuccessful(conn *configservice.ConfigService, name string, timeout time.Duration) error { + stateChangeConf := resource.StateChangeConf{ + Pending: []string{configservice.OrganizationResourceStatusCreateInProgress}, + Target: []string{configservice.OrganizationResourceStatusCreateSuccessful}, + Timeout: timeout, + Refresh: configRefreshOrganizationConformancePackCreationStatus(conn, name), + // Include a delay to help avoid ResourceDoesNotExist errors + Delay: 30 * time.Second, + } + + _, err := stateChangeConf.WaitForState() + + return err + +} + +func configWaitForOrganizationConformancePackStatusUpdateSuccessful(conn *configservice.ConfigService, name string, timeout time.Duration) error { + stateChangeConf := resource.StateChangeConf{ + Pending: []string{configservice.OrganizationResourceStatusUpdateInProgress}, + Target: []string{configservice.OrganizationResourceStatusUpdateSuccessful}, + Timeout: timeout, + Refresh: configRefreshOrganizationConformancePackStatus(conn, name), + } + + _, err := stateChangeConf.WaitForState() + + return err +} + +func configWaitForOrganizationConformancePackStatusDeleteSuccessful(conn *configservice.ConfigService, name string, timeout time.Duration) error { + stateChangeConf := resource.StateChangeConf{ + Pending: []string{configservice.OrganizationResourceStatusDeleteInProgress}, + Target: []string{configservice.OrganizationResourceStatusDeleteSuccessful}, + Timeout: timeout, + Refresh: configRefreshOrganizationConformancePackStatus(conn, name), + } + + _, err := stateChangeConf.WaitForState() + + return err +} + func configWaitForOrganizationRuleStatusCreateSuccessful(conn *configservice.ConfigService, name string, timeout time.Duration) error { stateChangeConf := &resource.StateChangeConf{ Pending: []string{configservice.OrganizationRuleStatusCreateInProgress}, diff --git a/aws/core_acceptance_test.go b/aws/core_acceptance_test.go index cdf212a86acc..3fdfa0f6a1e1 100644 --- a/aws/core_acceptance_test.go +++ b/aws/core_acceptance_test.go @@ -12,6 +12,7 @@ func TestAccAWSVpc_coreMismatchedDiffs(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ diff --git a/aws/cur_test.go b/aws/cur_test.go index 715285efd308..327329017854 100644 --- a/aws/cur_test.go +++ b/aws/cur_test.go @@ -97,3 +97,11 @@ func testAccGetCurRegion() string { return testAccCurRegion } + +func testAccRegionSupportsCur(region, partition string) bool { + if rs, ok := endpoints.RegionsForService(endpoints.DefaultPartitions(), partition, costandusagereportservice.ServiceName); ok { + _, ok := rs[region] + return ok + } + return false +} diff --git a/aws/data_source_aws_acm_certificate.go b/aws/data_source_aws_acm_certificate.go index 47de5653366c..09b35b1b7489 100644 --- a/aws/data_source_aws_acm_certificate.go +++ b/aws/data_source_aws_acm_certificate.go @@ -28,6 +28,10 @@ func dataSourceAwsAcmCertificate() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "status": { + Type: schema.TypeString, + Computed: true, + }, "key_types": { Type: schema.TypeSet, Optional: true, @@ -83,7 +87,7 @@ func dataSourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) e log.Printf("[DEBUG] Reading ACM Certificate: %s", params) err := conn.ListCertificatesPages(params, func(page *acm.ListCertificatesOutput, lastPage bool) bool { for _, cert := range page.CertificateSummaryList { - if *cert.DomainName == target { + if aws.StringValue(cert.DomainName) == target { arns = append(arns, cert.CertificateArn) } } @@ -125,7 +129,7 @@ func dataSourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) e if filterTypesOk { for _, certType := range typesStrings { - if *certificate.Type == *certType { + if aws.StringValue(certificate.Type) == aws.StringValue(certType) { // We do not have a candidate certificate if matchedCertificate == nil { matchedCertificate = certificate @@ -170,6 +174,7 @@ func dataSourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) e d.SetId(aws.StringValue(matchedCertificate.CertificateArn)) d.Set("arn", matchedCertificate.CertificateArn) + d.Set("status", matchedCertificate.Status) tags, err := keyvaluetags.AcmListTags(conn, aws.StringValue(matchedCertificate.CertificateArn)) @@ -185,18 +190,18 @@ func dataSourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) e } func mostRecentAcmCertificate(i, j *acm.CertificateDetail) (*acm.CertificateDetail, error) { - if *i.Status != *j.Status { + if aws.StringValue(i.Status) != aws.StringValue(j.Status) { return nil, fmt.Errorf("most_recent filtering on different ACM certificate statues is not supported") } // Cover IMPORTED and ISSUED AMAZON_ISSUED certificates - if *i.Status == acm.CertificateStatusIssued { - if (*i.NotBefore).After(*j.NotBefore) { + if aws.StringValue(i.Status) == acm.CertificateStatusIssued { + if aws.TimeValue(i.NotBefore).After(aws.TimeValue(j.NotBefore)) { return i, nil } return j, nil } // Cover non-ISSUED AMAZON_ISSUED certificates - if (*i.CreatedAt).After(*j.CreatedAt) { + if aws.TimeValue(i.CreatedAt).After(aws.TimeValue(j.CreatedAt)) { return i, nil } return j, nil diff --git a/aws/data_source_aws_acm_certificate_test.go b/aws/data_source_aws_acm_certificate_test.go index c9ebf50697c8..a63601e20153 100644 --- a/aws/data_source_aws_acm_certificate_test.go +++ b/aws/data_source_aws_acm_certificate_test.go @@ -36,14 +36,16 @@ func TestAccAWSAcmCertificateDataSource_singleIssued(t *testing.T) { resourceName := "data.aws_acm_certificate.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain), Check: resource.ComposeTestCheckFunc( //lintignore:AWSAT001 resource.TestMatchResourceAttr(resourceName, "arn", arnRe), + resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), ), }, { @@ -51,6 +53,7 @@ func TestAccAWSAcmCertificateDataSource_singleIssued(t *testing.T) { Check: resource.ComposeTestCheckFunc( //lintignore:AWSAT001 resource.TestMatchResourceAttr(resourceName, "arn", arnRe), + resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), ), }, { @@ -108,8 +111,9 @@ func TestAccAWSAcmCertificateDataSource_multipleIssued(t *testing.T) { resourceName := "data.aws_acm_certificate.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain), @@ -156,8 +160,9 @@ func TestAccAWSAcmCertificateDataSource_noMatchReturnsError(t *testing.T) { domain := fmt.Sprintf("tf-acc-nonexistent.%s", os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN")) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain), @@ -195,8 +200,9 @@ func TestAccAWSAcmCertificateDataSource_KeyTypes(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsAcmCertificateDataSourceConfigKeyTypes(tlsPemEscapeNewlines(certificate), tlsPemEscapeNewlines(key), rName), diff --git a/aws/data_source_aws_acmpca_certificate.go b/aws/data_source_aws_acmpca_certificate.go new file mode 100644 index 000000000000..18484b5fca22 --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate.go @@ -0,0 +1,60 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsAcmpcaCertificate() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAcmpcaCertificateRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsAcmpcaCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + certificateArn := d.Get("arn").(string) + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(certificateArn), + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + log.Printf("[DEBUG] Reading ACM PCA Certificate: %s", getCertificateInput) + + certificateOutput, err := conn.GetCertificate(getCertificateInput) + if err != nil { + return fmt.Errorf("error reading ACM PCA Certificate (%s): %w", certificateArn, err) + } + + d.SetId(certificateArn) + d.Set("certificate", certificateOutput.Certificate) + d.Set("certificate_chain", certificateOutput.CertificateChain) + + return nil +} diff --git a/aws/data_source_aws_acmpca_certificate_authority.go b/aws/data_source_aws_acmpca_certificate_authority.go index 1047cd971ef3..7caf8ca0a6e8 100644 --- a/aws/data_source_aws_acmpca_certificate_authority.go +++ b/aws/data_source_aws_acmpca_certificate_authority.go @@ -71,6 +71,10 @@ func dataSourceAwsAcmpcaCertificateAuthority() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "s3_object_acl": { + Type: schema.TypeString, + Computed: true, + }, }, }, }, @@ -103,15 +107,15 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority: %s", describeCertificateAuthorityInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority: %s", describeCertificateAuthorityInput) describeCertificateAuthorityOutput, err := conn.DescribeCertificateAuthority(describeCertificateAuthorityInput) if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority: %w", err) } if describeCertificateAuthorityOutput.CertificateAuthority == nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: not found") + return fmt.Errorf("error reading ACM PCA Certificate Authority: not found") } certificateAuthority := describeCertificateAuthorityOutput.CertificateAuthority @@ -131,14 +135,14 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) if err != nil { // Returned when in PENDING_CERTIFICATE status // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. if !tfawserr.ErrCodeEquals(err, acmpca.ErrCodeInvalidStateException) { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate: %w", err) } } @@ -153,11 +157,11 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate Signing Request: %w", err) } d.Set("certificate_signing_request", "") @@ -168,7 +172,7 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in tags, err := keyvaluetags.AcmpcaListTags(conn, certificateAuthorityArn) if err != nil { - return fmt.Errorf("error listing tags for ACMPCA Certificate Authority (%s): %w", certificateAuthorityArn, err) + return fmt.Errorf("error listing tags for ACM PCA Certificate Authority (%s): %w", certificateAuthorityArn, err) } if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { diff --git a/aws/data_source_aws_acmpca_certificate_authority_test.go b/aws/data_source_aws_acmpca_certificate_authority_test.go index 617146b4d9d7..a74bff22ca66 100644 --- a/aws/data_source_aws_acmpca_certificate_authority_test.go +++ b/aws/data_source_aws_acmpca_certificate_authority_test.go @@ -1,9 +1,11 @@ package aws import ( + "fmt" "regexp" "testing" + "github.com/aws/aws-sdk-go/service/acmpca" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,16 +13,56 @@ func TestAccDataSourceAwsAcmpcaCertificateAuthority_basic(t *testing.T) { resourceName := "aws_acmpca_certificate_authority.test" datasourceName := "data.aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_NonExistent, + ExpectError: regexp.MustCompile(`(AccessDeniedException|ResourceNotFoundException)`), + }, + { + Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN(commonName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "certificate", resourceName, "certificate"), + resource.TestCheckResourceAttrPair(datasourceName, "certificate_chain", resourceName, "certificate_chain"), + resource.TestCheckResourceAttrPair(datasourceName, "certificate_signing_request", resourceName, "certificate_signing_request"), + resource.TestCheckResourceAttrPair(datasourceName, "not_after", resourceName, "not_after"), + resource.TestCheckResourceAttrPair(datasourceName, "not_before", resourceName, "not_before"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.#", resourceName, "revocation_configuration.#"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.#", resourceName, "revocation_configuration.0.crl_configuration.#"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.enabled", resourceName, "revocation_configuration.0.crl_configuration.0.enabled"), + resource.TestCheckResourceAttrPair(datasourceName, "serial", resourceName, "serial"), + resource.TestCheckResourceAttrPair(datasourceName, "status", resourceName, "status"), + resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(datasourceName, "type", resourceName, "type"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsAcmpcaCertificateAuthority_S3ObjectAcl(t *testing.T) { + resourceName := "aws_acmpca_certificate_authority.test" + datasourceName := "data.aws_acmpca_certificate_authority.test" + + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_NonExistent, ExpectError: regexp.MustCompile(`(AccessDeniedException|ResourceNotFoundException)`), }, { - Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN, + Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfigS3ObjectAcl_ARN(commonName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(datasourceName, "certificate", resourceName, "certificate"), @@ -31,6 +73,10 @@ func TestAccDataSourceAwsAcmpcaCertificateAuthority_basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.#", resourceName, "revocation_configuration.#"), resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.#", resourceName, "revocation_configuration.0.crl_configuration.#"), resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.enabled", resourceName, "revocation_configuration.0.crl_configuration.0.enabled"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name"), + resource.TestCheckResourceAttrPair(datasourceName, "revocation_configuration.0.crl_configuration.0.s3_object_acl", resourceName, "revocation_configuration.0.crl_configuration.0.s3_object_acl"), resource.TestCheckResourceAttrPair(datasourceName, "serial", resourceName, "serial"), resource.TestCheckResourceAttrPair(datasourceName, "status", resourceName, "status"), resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), @@ -41,7 +87,8 @@ func TestAccDataSourceAwsAcmpcaCertificateAuthority_basic(t *testing.T) { }) } -const testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN = ` +func testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN(commonName string) string { + return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "wrong" { permanent_deletion_time_in_days = 7 @@ -50,7 +97,7 @@ resource "aws_acmpca_certificate_authority" "wrong" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } } @@ -63,7 +110,7 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } } @@ -71,7 +118,42 @@ resource "aws_acmpca_certificate_authority" "test" { data "aws_acmpca_certificate_authority" "test" { arn = aws_acmpca_certificate_authority.test.arn } -` +`, commonName) +} + +func testAccDataSourceAwsAcmpcaCertificateAuthorityConfigS3ObjectAcl_ARN(commonName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "wrong" { + permanent_deletion_time_in_days = 7 + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +data "aws_acmpca_certificate_authority" "test" { + arn = aws_acmpca_certificate_authority.test.arn +} +`, commonName) +} //lintignore:AWSAT003,AWSAT005 const testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_NonExistent = ` diff --git a/aws/data_source_aws_acmpca_certificate_test.go b/aws/data_source_aws_acmpca_certificate_test.go new file mode 100644 index 000000000000..281b0f1364c6 --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate_test.go @@ -0,0 +1,89 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsAcmpcaCertificate_Basic(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + dataSourceName := "data.aws_acmpca_certificate.test" + + domain := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsAcmpcaCertificateConfig_NonExistent, + ExpectError: regexp.MustCompile(`ResourceNotFoundException`), + }, + { + Config: testAccDataSourceAwsAcmpcaCertificateConfig_ARN(domain), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate", resourceName, "certificate"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate_chain", resourceName, "certificate_chain"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate_authority_arn", resourceName, "certificate_authority_arn"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsAcmpcaCertificateConfig_ARN(domain string) string { + return fmt.Sprintf(` +data "aws_acmpca_certificate" "test" { + arn = aws_acmpca_certificate.test.arn + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +data "aws_partition" "current" {} +`, domain) +} + +const testAccDataSourceAwsAcmpcaCertificateConfig_NonExistent = ` +data "aws_acmpca_certificate" "test" { + arn = "arn:${data.aws_partition.current.partition}:acm-pca:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:certificate-authority/does-not-exist/certificate/does-not-exist" + certificate_authority_arn = "arn:${data.aws_partition.current.partition}:acm-pca:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:certificate-authority/does-not-exist" +} + +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "current" {} +` diff --git a/aws/data_source_aws_ami.go b/aws/data_source_aws_ami.go index c0a5035d645b..64c7b114fc0f 100644 --- a/aws/data_source_aws_ami.go +++ b/aws/data_source_aws_ami.go @@ -132,6 +132,18 @@ func dataSourceAwsAmi() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "usage_operation": { + Type: schema.TypeString, + Computed: true, + }, + "platform_details": { + Type: schema.TypeString, + Computed: true, + }, + "ena_support": { + Type: schema.TypeBool, + Computed: true, + }, // Complex computed values "block_device_mappings": { Type: schema.TypeSet, @@ -287,6 +299,10 @@ func amiDescriptionAttributes(d *schema.ResourceData, image *ec2.Image, meta int } d.Set("state", image.State) d.Set("virtualization_type", image.VirtualizationType) + d.Set("usage_operation", image.UsageOperation) + d.Set("platform_details", image.PlatformDetails) + d.Set("ena_support", image.EnaSupport) + // Complex types get their own functions if err := d.Set("block_device_mappings", amiBlockDeviceMappings(image.BlockDeviceMappings)); err != nil { return err @@ -305,7 +321,7 @@ func amiDescriptionAttributes(d *schema.ResourceData, image *ec2.Image, meta int Partition: meta.(*AWSClient).partition, Region: meta.(*AWSClient).region, Resource: fmt.Sprintf("image/%s", d.Id()), - Service: "ec2", + Service: ec2.ServiceName, }.String() d.Set("arn", imageArn) diff --git a/aws/data_source_aws_ami_ids.go b/aws/data_source_aws_ami_ids.go index fba0271a1cf7..e9cb7117c5aa 100644 --- a/aws/data_source_aws_ami_ids.go +++ b/aws/data_source_aws_ami_ids.go @@ -82,13 +82,14 @@ func dataSourceAwsAmiIdsRead(d *schema.ResourceData, meta interface{}) error { // Check for a very rare case where the response would include no // image name. No name means nothing to attempt a match against, // therefore we are skipping such image. - if image.Name == nil || *image.Name == "" { + name := aws.StringValue(image.Name) + if name == "" { log.Printf("[WARN] Unable to find AMI name to match against "+ "for image ID %q owned by %q, nothing to do.", - *image.ImageId, *image.OwnerId) + aws.StringValue(image.ImageId), aws.StringValue(image.OwnerId)) continue } - if r.MatchString(*image.Name) { + if r.MatchString(name) { filteredImages = append(filteredImages, image) } } diff --git a/aws/data_source_aws_ami_ids_test.go b/aws/data_source_aws_ami_ids_test.go index e2c6ba4689af..ae29537a228e 100644 --- a/aws/data_source_aws_ami_ids_test.go +++ b/aws/data_source_aws_ami_ids_test.go @@ -4,13 +4,15 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsAmiIds_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAmiIdsConfig_basic, @@ -24,8 +26,9 @@ func TestAccDataSourceAwsAmiIds_basic(t *testing.T) { func TestAccDataSourceAwsAmiIds_sorted(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAmiIdsConfig_sorted(false), diff --git a/aws/data_source_aws_ami_test.go b/aws/data_source_aws_ami_test.go index 9def968d79c2..8bb59cb297de 100644 --- a/aws/data_source_aws_ami_test.go +++ b/aws/data_source_aws_ami_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "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" @@ -13,8 +14,9 @@ import ( func TestAccAWSAmiDataSource_natInstance(t *testing.T) { resourceName := "data.aws_ami.nat_ami" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAmiDataSourceConfig, @@ -51,6 +53,9 @@ func TestAccAWSAmiDataSource_natInstance(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "state_reason.message", "UNSET"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "platform_details", "Linux/UNIX"), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckResourceAttr(resourceName, "usage_operation", "RunInstances"), ), }, }, @@ -60,8 +65,9 @@ func TestAccAWSAmiDataSource_natInstance(t *testing.T) { func TestAccAWSAmiDataSource_windowsInstance(t *testing.T) { resourceName := "data.aws_ami.windows_ami" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAmiDataSourceWindowsConfig, @@ -91,6 +97,9 @@ func TestAccAWSAmiDataSource_windowsInstance(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "state_reason.message", "UNSET"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestMatchResourceAttr(resourceName, "platform_details", regexp.MustCompile(`Windows`)), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestMatchResourceAttr(resourceName, "usage_operation", regexp.MustCompile(`^RunInstances`)), ), }, }, @@ -100,8 +109,9 @@ func TestAccAWSAmiDataSource_windowsInstance(t *testing.T) { func TestAccAWSAmiDataSource_instanceStore(t *testing.T) { resourceName := "data.aws_ami.amzn-ami-minimal-hvm-instance-store" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccLatestAmazonLinuxHvmInstanceStoreAmiConfig(), @@ -127,6 +137,9 @@ func TestAccAWSAmiDataSource_instanceStore(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "state_reason.message", "UNSET"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "platform_details", "Linux/UNIX"), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckResourceAttr(resourceName, "usage_operation", "RunInstances"), ), }, }, @@ -135,8 +148,9 @@ func TestAccAWSAmiDataSource_instanceStore(t *testing.T) { func TestAccAWSAmiDataSource_localNameFilter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAmiDataSourceNameRegexConfig, @@ -155,8 +169,9 @@ func TestAccAWSAmiDataSource_Gp3BlockDevice(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAmiDataSourceConfigGp3BlockDevice(rName), diff --git a/aws/data_source_aws_api_gateway_api_key_test.go b/aws/data_source_aws_api_gateway_api_key_test.go index fc7b2374311d..0777ea585874 100644 --- a/aws/data_source_aws_api_gateway_api_key_test.go +++ b/aws/data_source_aws_api_gateway_api_key_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsApiGatewayApiKey_basic(t *testing.T) { dataSourceName1 := "data.aws_api_gateway_api_key.test_key" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsApiGatewayApiKeyConfig(rName), diff --git a/aws/data_source_aws_api_gateway_domain_name_test.go b/aws/data_source_aws_api_gateway_domain_name_test.go index 830b69f9b6e0..a2bab5406a71 100644 --- a/aws/data_source_aws_api_gateway_domain_name_test.go +++ b/aws/data_source_aws_api_gateway_domain_name_test.go @@ -4,20 +4,21 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsApiGatewayDomainName_basic(t *testing.T) { resourceName := "aws_api_gateway_domain_name.test" dataSourceName := "data.aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + rName := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, rName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_api_gateway_resource_test.go b/aws/data_source_aws_api_gateway_resource_test.go index 147bb02e319f..f7c4aaf3f404 100644 --- a/aws/data_source_aws_api_gateway_resource_test.go +++ b/aws/data_source_aws_api_gateway_resource_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -16,8 +17,9 @@ func TestAccDataSourceAwsApiGatewayResource_basic(t *testing.T) { dataSourceName2 := "data.aws_api_gateway_resource.example_v1_endpoint" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsApiGatewayResourceConfig(rName), diff --git a/aws/data_source_aws_api_gateway_rest_api.go b/aws/data_source_aws_api_gateway_rest_api.go index ef32a8f0b25f..ef152a42259a 100644 --- a/aws/data_source_aws_api_gateway_rest_api.go +++ b/aws/data_source_aws_api_gateway_rest_api.go @@ -148,7 +148,7 @@ func dataSourceAwsApiGatewayRestApiRead(d *schema.ResourceData, meta interface{} err = conn.GetResourcesPages(resourceParams, func(page *apigateway.GetResourcesOutput, lastPage bool) bool { for _, item := range page.Items { - if *item.Path == "/" { + if aws.StringValue(item.Path) == "/" { d.Set("root_resource_id", item.Id) return false } diff --git a/aws/data_source_aws_api_gateway_rest_api_test.go b/aws/data_source_aws_api_gateway_rest_api_test.go index 3489e700e5da..a7e70231020a 100644 --- a/aws/data_source_aws_api_gateway_rest_api_test.go +++ b/aws/data_source_aws_api_gateway_rest_api_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccDataSourceAwsApiGatewayRestApi_basic(t *testing.T) { dataSourceName := "data.aws_api_gateway_rest_api.test" resourceName := "aws_api_gateway_rest_api.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( @@ -43,8 +45,9 @@ func TestAccDataSourceAwsApiGatewayRestApi_EndpointConfiguration_VpcEndpointIds( dataSourceName := "data.aws_api_gateway_rest_api.test" resourceName := "aws_api_gateway_rest_api.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( diff --git a/aws/data_source_aws_api_gateway_vpc_link_test.go b/aws/data_source_aws_api_gateway_vpc_link_test.go index 7c66203e32f7..dc726221e620 100644 --- a/aws/data_source_aws_api_gateway_vpc_link_test.go +++ b/aws/data_source_aws_api_gateway_vpc_link_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsApiGatewayVpcLink_basic(t *testing.T) { resourceName := "aws_api_gateway_vpc_link.vpc_link" dataSourceName := "data.aws_api_gateway_vpc_link.vpc_link" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsApiGatewayVpcLinkConfig(rName), diff --git a/aws/data_source_aws_apigatewayv2_api.go b/aws/data_source_aws_apigatewayv2_api.go new file mode 100644 index 000000000000..958893ce5c6c --- /dev/null +++ b/aws/data_source_aws_apigatewayv2_api.go @@ -0,0 +1,155 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apigatewayv2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func dataSourceAwsApiGatewayV2Api() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAwsApiGatewayV2ApiRead, + + Schema: map[string]*schema.Schema{ + "api_endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "api_id": { + Type: schema.TypeString, + Required: true, + }, + "api_key_selection_expression": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "cors_configuration": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allow_credentials": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_headers": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: hashStringCaseInsensitive, + }, + "allow_methods": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: hashStringCaseInsensitive, + }, + "allow_origins": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: hashStringCaseInsensitive, + }, + "expose_headers": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: hashStringCaseInsensitive, + }, + "max_age": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "disable_execute_api_endpoint": { + Type: schema.TypeBool, + Computed: true, + }, + "execution_arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "protocol_type": { + Type: schema.TypeString, + Computed: true, + }, + "route_selection_expression": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + "version": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsAwsApiGatewayV2ApiRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigatewayv2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + apiID := d.Get("api_id").(string) + + api, err := finder.ApiByID(conn, apiID) + + if tfresource.NotFound(err) { + return fmt.Errorf("no API Gateway v2 API matched; change the search criteria and try again") + } + + if err != nil { + return fmt.Errorf("error reading API Gateway v2 API (%s): %w", apiID, err) + } + + d.SetId(apiID) + + d.Set("api_endpoint", api.ApiEndpoint) + d.Set("api_key_selection_expression", api.ApiKeySelectionExpression) + apiArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "apigateway", + Region: meta.(*AWSClient).region, + Resource: fmt.Sprintf("/apis/%s", d.Id()), + }.String() + d.Set("arn", apiArn) + if err := d.Set("cors_configuration", flattenApiGateway2CorsConfiguration(api.CorsConfiguration)); err != nil { + return fmt.Errorf("error setting cors_configuration: %w", err) + } + d.Set("description", api.Description) + d.Set("disable_execute_api_endpoint", api.DisableExecuteApiEndpoint) + executionArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "execute-api", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: d.Id(), + }.String() + d.Set("execution_arn", executionArn) + d.Set("name", api.Name) + d.Set("protocol_type", api.ProtocolType) + d.Set("route_selection_expression", api.RouteSelectionExpression) + if err := d.Set("tags", keyvaluetags.Apigatewayv2KeyValueTags(api.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + d.Set("version", api.Version) + + return nil +} diff --git a/aws/data_source_aws_apigatewayv2_api_test.go b/aws/data_source_aws_apigatewayv2_api_test.go new file mode 100644 index 000000000000..fddaab546275 --- /dev/null +++ b/aws/data_source_aws_apigatewayv2_api_test.go @@ -0,0 +1,132 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSAPIGatewayV2ApiDataSource_Http(t *testing.T) { + dataSourceName := "data.aws_apigatewayv2_api.test" + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiDataSourceConfigHttp(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "api_endpoint", resourceName, "api_endpoint"), + resource.TestCheckResourceAttrPair(dataSourceName, "api_key_selection_expression", resourceName, "api_key_selection_expression"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.#", resourceName, "cors_configuration.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.allow_credentials", resourceName, "cors_configuration.0.allow_credentials"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.allow_headers.#", resourceName, "cors_configuration.0.allow_headers.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.allow_methods.#", resourceName, "cors_configuration.0.allow_methods.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.allow_origins.#", resourceName, "cors_configuration.0.allow_origins.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.expose_headers.#", resourceName, "cors_configuration.0.expose_headers.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.0.max_age", resourceName, "cors_configuration.0.max_age"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "disable_execute_api_endpoint", resourceName, "disable_execute_api_endpoint"), + resource.TestCheckResourceAttrPair(dataSourceName, "execution_arn", resourceName, "execution_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "protocol_type", resourceName, "protocol_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "route_selection_expression", resourceName, "route_selection_expression"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Key1", resourceName, "tags.Key1"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Key2", resourceName, "tags.Key2"), + resource.TestCheckResourceAttrPair(dataSourceName, "version", resourceName, "version"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2ApiDataSource_WebSocket(t *testing.T) { + dataSourceName := "data.aws_apigatewayv2_api.test" + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiDataSourceConfigWebSocket(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "api_endpoint", resourceName, "api_endpoint"), + resource.TestCheckResourceAttrPair(dataSourceName, "api_key_selection_expression", resourceName, "api_key_selection_expression"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "cors_configuration.#", resourceName, "cors_configuration.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "disable_execute_api_endpoint", resourceName, "disable_execute_api_endpoint"), + resource.TestCheckResourceAttrPair(dataSourceName, "execution_arn", resourceName, "execution_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "protocol_type", resourceName, "protocol_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "route_selection_expression", resourceName, "route_selection_expression"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Key1", resourceName, "tags.Key1"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Key2", resourceName, "tags.Key2"), + resource.TestCheckResourceAttrPair(dataSourceName, "version", resourceName, "version"), + ), + }, + }, + }) +} + +func testAccAWSAPIGatewayV2ApiDataSourceConfigHttp(rName string) string { + return fmt.Sprintf(` +resource "aws_apigatewayv2_api" "test" { + description = "test description" + name = %[1]q + protocol_type = "HTTP" + version = "v1" + + cors_configuration { + allow_headers = ["Authorization"] + allow_methods = ["GET", "put"] + allow_origins = ["https://www.example.com"] + } + + tags = { + Key1 = "Value1h" + Key2 = "Value2h" + } +} + +data "aws_apigatewayv2_api" "test" { + api_id = aws_apigatewayv2_api.test.id +} +`, rName) +} + +func testAccAWSAPIGatewayV2ApiDataSourceConfigWebSocket(rName string) string { + return fmt.Sprintf(` +resource "aws_apigatewayv2_api" "test" { + api_key_selection_expression = "$context.authorizer.usageIdentifierKey" + description = "test description" + name = %[1]q + protocol_type = "WEBSOCKET" + route_selection_expression = "$request.body.service" + version = "v1" + + tags = { + Key1 = "Value1ws" + Key2 = "Value2ws" + } +} + +data "aws_apigatewayv2_api" "test" { + api_id = aws_apigatewayv2_api.test.id +} +`, rName) +} diff --git a/aws/data_source_aws_apigatewayv2_apis.go b/aws/data_source_aws_apigatewayv2_apis.go new file mode 100644 index 000000000000..981de6081f7e --- /dev/null +++ b/aws/data_source_aws_apigatewayv2_apis.go @@ -0,0 +1,74 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apigatewayv2/finder" +) + +func dataSourceAwsApiGatewayV2Apis() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAwsApiGatewayV2ApisRead, + + Schema: map[string]*schema.Schema{ + "ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "name": { + Type: schema.TypeString, + Optional: true, + }, + "protocol_type": { + Type: schema.TypeString, + Optional: true, + }, + "tags": tagsSchema(), + }, + } +} + +func dataSourceAwsAwsApiGatewayV2ApisRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigatewayv2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + tagsToMatch := keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + apis, err := finder.Apis(conn, &apigatewayv2.GetApisInput{}) + + if err != nil { + return fmt.Errorf("error reading API Gateway v2 APIs: %w", err) + } + + var ids []*string + + for _, api := range apis { + if v, ok := d.GetOk("name"); ok && v.(string) != aws.StringValue(api.Name) { + continue + } + + if v, ok := d.GetOk("protocol_type"); ok && v.(string) != aws.StringValue(api.ProtocolType) { + continue + } + + if len(tagsToMatch) > 0 && !keyvaluetags.Apigatewayv2KeyValueTags(api.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).ContainsAll(tagsToMatch) { + continue + } + + ids = append(ids, api.ApiId) + } + + d.SetId(meta.(*AWSClient).region) + + if err := d.Set("ids", flattenStringSet(ids)); err != nil { + return fmt.Errorf("error setting ids: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_apigatewayv2_apis_test.go b/aws/data_source_aws_apigatewayv2_apis_test.go new file mode 100644 index 000000000000..9e8a1f554b42 --- /dev/null +++ b/aws/data_source_aws_apigatewayv2_apis_test.go @@ -0,0 +1,175 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSAPIGatewayV2ApisDataSource_Name(t *testing.T) { + dataSource1Name := "data.aws_apigatewayv2_apis.test1" + dataSource2Name := "data.aws_apigatewayv2_apis.test2" + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApisDataSourceConfigName(rName1, rName2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSource1Name, "ids.#", "1"), + resource.TestCheckResourceAttr(dataSource2Name, "ids.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2ApisDataSource_ProtocolType(t *testing.T) { + dataSource1Name := "data.aws_apigatewayv2_apis.test1" + dataSource2Name := "data.aws_apigatewayv2_apis.test2" + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApisDataSourceConfigProtocolType(rName1, rName2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSource1Name, "ids.#", "1"), + resource.TestCheckResourceAttr(dataSource2Name, "ids.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2ApisDataSource_Tags(t *testing.T) { + dataSource1Name := "data.aws_apigatewayv2_apis.test1" + dataSource2Name := "data.aws_apigatewayv2_apis.test2" + dataSource3Name := "data.aws_apigatewayv2_apis.test3" + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApisDataSourceConfigTags(rName1, rName2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSource1Name, "ids.#", "1"), + resource.TestCheckResourceAttr(dataSource2Name, "ids.#", "2"), + resource.TestCheckResourceAttr(dataSource3Name, "ids.#", "0"), + ), + }, + }, + }) +} + +func testAccAWSAPIGatewayV2ApisDataSourceConfigBase(rName1, rName2 string) string { + return fmt.Sprintf(` +resource "aws_apigatewayv2_api" "test1" { + name = %[1]q + protocol_type = "HTTP" + + tags = { + Name = %[1]q + } +} + +resource "aws_apigatewayv2_api" "test2" { + name = %[2]q + protocol_type = "HTTP" + + tags = { + Name = %[2]q + } +} + +resource "aws_apigatewayv2_api" "test3" { + name = %[2]q + protocol_type = "WEBSOCKET" + route_selection_expression = "$request.body.action" + + tags = { + Name = %[2]q + } +} +`, rName1, rName2) +} + +func testAccAWSAPIGatewayV2ApisDataSourceConfigName(rName1, rName2 string) string { + return composeConfig( + testAccAWSAPIGatewayV2ApisDataSourceConfigBase(rName1, rName2), + ` +data "aws_apigatewayv2_apis" "test1" { + # Force dependency on resources. + name = element([aws_apigatewayv2_api.test1.name, aws_apigatewayv2_api.test2.name, aws_apigatewayv2_api.test3.name], 0) +} + +data "aws_apigatewayv2_apis" "test2" { + # Force dependency on resources. + name = element([aws_apigatewayv2_api.test1.name, aws_apigatewayv2_api.test2.name, aws_apigatewayv2_api.test3.name], 1) +} +`) +} + +func testAccAWSAPIGatewayV2ApisDataSourceConfigProtocolType(rName1, rName2 string) string { + return composeConfig( + testAccAWSAPIGatewayV2ApisDataSourceConfigBase(rName1, rName2), + fmt.Sprintf(` +data "aws_apigatewayv2_apis" "test1" { + name = %[1]q + + protocol_type = element([aws_apigatewayv2_api.test1.protocol_type, aws_apigatewayv2_api.test2.protocol_type, aws_apigatewayv2_api.test3.protocol_type], 0) +} + +data "aws_apigatewayv2_apis" "test2" { + name = %[2]q + + protocol_type = element([aws_apigatewayv2_api.test1.protocol_type, aws_apigatewayv2_api.test2.protocol_type, aws_apigatewayv2_api.test3.protocol_type], 3) +} +`, rName1, rName2)) +} + +func testAccAWSAPIGatewayV2ApisDataSourceConfigTags(rName1, rName2 string) string { + return composeConfig( + testAccAWSAPIGatewayV2ApisDataSourceConfigBase(rName1, rName2), + ` +data "aws_apigatewayv2_apis" "test1" { + # Force dependency on resources. + tags = { + Name = element([aws_apigatewayv2_api.test1.name, aws_apigatewayv2_api.test2.name, aws_apigatewayv2_api.test3.name], 0) + } +} + +data "aws_apigatewayv2_apis" "test2" { + # Force dependency on resources. + tags = { + Name = element([aws_apigatewayv2_api.test1.name, aws_apigatewayv2_api.test2.name, aws_apigatewayv2_api.test3.name], 1) + } +} + +data "aws_apigatewayv2_apis" "test3" { + # Force dependency on resources. + tags = { + Name = element([aws_apigatewayv2_api.test1.name, aws_apigatewayv2_api.test2.name, aws_apigatewayv2_api.test3.name], 2) + Key2 = "Value2" + } +} +`) +} diff --git a/aws/data_source_aws_appmesh_mesh.go b/aws/data_source_aws_appmesh_mesh.go new file mode 100644 index 000000000000..038ece65d68b --- /dev/null +++ b/aws/data_source_aws_appmesh_mesh.go @@ -0,0 +1,120 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsAppmeshMesh() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAppmeshMeshRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "created_date": { + Type: schema.TypeString, + Computed: true, + }, + + "last_updated_date": { + Type: schema.TypeString, + Computed: true, + }, + + "mesh_owner": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "resource_owner": { + Type: schema.TypeString, + Computed: true, + }, + + "spec": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "egress_filter": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsAppmeshMeshRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appmeshconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + meshName := d.Get("name").(string) + + req := &appmesh.DescribeMeshInput{ + MeshName: aws.String(meshName), + } + if v, ok := d.GetOk("mesh_owner"); ok { + req.MeshOwner = aws.String(v.(string)) + } + + resp, err := conn.DescribeMesh(req) + if err != nil { + return fmt.Errorf("error reading App Mesh service mesh: %s", err) + } + if aws.StringValue(resp.Mesh.Status.Status) == appmesh.MeshStatusCodeDeleted { + return fmt.Errorf("App Mesh Mesh (%s) has status: 'DELETED'", meshName) + } + + arn := aws.StringValue(resp.Mesh.Metadata.Arn) + + d.SetId(aws.StringValue(resp.Mesh.MeshName)) + d.Set("arn", arn) + d.Set("created_date", resp.Mesh.Metadata.CreatedAt.Format(time.RFC3339)) + d.Set("last_updated_date", resp.Mesh.Metadata.LastUpdatedAt.Format(time.RFC3339)) + d.Set("mesh_owner", resp.Mesh.Metadata.MeshOwner) + d.Set("resource_owner", resp.Mesh.Metadata.ResourceOwner) + err = d.Set("spec", flattenAppmeshMeshSpec(resp.Mesh.Spec)) + if err != nil { + return fmt.Errorf("error setting spec: %s", err) + } + + tags, err := keyvaluetags.AppmeshListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for App Mesh service mesh (%s): %s", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + return nil +} diff --git a/aws/data_source_aws_appmesh_mesh_test.go b/aws/data_source_aws_appmesh_mesh_test.go new file mode 100644 index 000000000000..f636fa8324bd --- /dev/null +++ b/aws/data_source_aws_appmesh_mesh_test.go @@ -0,0 +1,146 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAWSAppmeshMesh_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appmesh_mesh.test" + dataSourceName := "data.aws_appmesh_mesh.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshMeshDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsAppmeshMeshDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_date", dataSourceName, "created_date"), + resource.TestCheckResourceAttrPair(resourceName, "last_updated_date", dataSourceName, "last_updated_date"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_owner", dataSourceName, "mesh_owner"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_owner", dataSourceName, "resource_owner"), + resource.TestCheckResourceAttrPair(resourceName, "spec.0.egress_filter.0.type", dataSourceName, "spec.0.egress_filter.0.type"), + resource.TestCheckResourceAttrPair(resourceName, "tags", dataSourceName, "tags"), + ), + }, + }, + }) +} + +func TestAccDataSourceAWSAppmeshMesh_meshOwner(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appmesh_mesh.test" + dataSourceName := "data.aws_appmesh_mesh.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshMeshDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsAppmeshMeshDataSourceConfig_meshOwner(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_date", dataSourceName, "created_date"), + resource.TestCheckResourceAttrPair(resourceName, "last_updated_date", dataSourceName, "last_updated_date"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_owner", dataSourceName, "mesh_owner"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_owner", dataSourceName, "resource_owner"), + resource.TestCheckResourceAttrPair(resourceName, "spec.0.egress_filter.0.type", dataSourceName, "spec.0.egress_filter.0.type"), + resource.TestCheckResourceAttrPair(resourceName, "tags", dataSourceName, "tags"), + ), + }, + }, + }) +} + +func TestAccDataSourceAWSAppmeshMesh_specAndTagsSet(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appmesh_mesh.test" + dataSourceName := "data.aws_appmesh_mesh.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshMeshDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsAppmeshMeshDataSourceConfig_specAndTagsSet(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_date", dataSourceName, "created_date"), + resource.TestCheckResourceAttrPair(resourceName, "last_updated_date", dataSourceName, "last_updated_date"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_owner", dataSourceName, "mesh_owner"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_owner", dataSourceName, "resource_owner"), + resource.TestCheckResourceAttrPair(resourceName, "spec.0.egress_filter.0.type", dataSourceName, "spec.0.egress_filter.0.type"), + resource.TestCheckResourceAttrPair(resourceName, "tags", dataSourceName, "tags"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppmeshMeshDataSourceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +data "aws_appmesh_mesh" "test" { + name = aws_appmesh_mesh.test.name +} +`, rName) +} + +func testAccCheckAwsAppmeshMeshDataSourceConfig_meshOwner(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +data "aws_appmesh_mesh" "test" { + name = aws_appmesh_mesh.test.name + mesh_owner = data.aws_caller_identity.current.account_id +} +`, rName) +} + +func testAccCheckAwsAppmeshMeshDataSourceConfig_specAndTagsSet(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_appmesh_mesh" "test" { + name = %[1]q + + spec { + egress_filter { + type = "DROP_ALL" + } + } + + tags = { + foo = "bar" + good = "bad" + } +} + +data "aws_appmesh_mesh" "test" { + name = aws_appmesh_mesh.test.name +} +`, rName) +} diff --git a/aws/data_source_aws_appmesh_virtual_service.go b/aws/data_source_aws_appmesh_virtual_service.go new file mode 100644 index 000000000000..8954223d0fbf --- /dev/null +++ b/aws/data_source_aws_appmesh_virtual_service.go @@ -0,0 +1,147 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsAppmeshVirtualService() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAppmeshVirtualServiceRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "created_date": { + Type: schema.TypeString, + Computed: true, + }, + + "last_updated_date": { + Type: schema.TypeString, + Computed: true, + }, + + "mesh_name": { + Type: schema.TypeString, + Required: true, + }, + + "mesh_owner": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "resource_owner": { + Type: schema.TypeString, + Computed: true, + }, + + "spec": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "provider": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "virtual_node": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "virtual_node_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "virtual_router": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "virtual_router_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + + "tags": tagsSchema(), + }, + } +} + +func dataSourceAwsAppmeshVirtualServiceRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appmeshconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + req := &appmesh.DescribeVirtualServiceInput{ + MeshName: aws.String(d.Get("mesh_name").(string)), + VirtualServiceName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("mesh_owner"); ok { + req.MeshOwner = aws.String(v.(string)) + } + + resp, err := conn.DescribeVirtualService(req) + if err != nil { + return fmt.Errorf("error reading App Mesh Virtual Service: %s", err) + } + + arn := aws.StringValue(resp.VirtualService.Metadata.Arn) + + d.SetId(aws.StringValue(resp.VirtualService.VirtualServiceName)) + + d.Set("name", resp.VirtualService.VirtualServiceName) + d.Set("mesh_name", resp.VirtualService.MeshName) + d.Set("mesh_owner", resp.VirtualService.Metadata.MeshOwner) + d.Set("arn", arn) + d.Set("created_date", resp.VirtualService.Metadata.CreatedAt.Format(time.RFC3339)) + d.Set("last_updated_date", resp.VirtualService.Metadata.LastUpdatedAt.Format(time.RFC3339)) + d.Set("resource_owner", resp.VirtualService.Metadata.ResourceOwner) + + err = d.Set("spec", flattenAppmeshVirtualServiceSpec(resp.VirtualService.Spec)) + if err != nil { + return fmt.Errorf("error setting spec: %s", err) + } + + tags, err := keyvaluetags.AppmeshListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for App Mesh Virtual Service (%s): %s", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + return nil +} diff --git a/aws/data_source_aws_appmesh_virtual_service_test.go b/aws/data_source_aws_appmesh_virtual_service_test.go new file mode 100644 index 000000000000..92c808acc7ff --- /dev/null +++ b/aws/data_source_aws_appmesh_virtual_service_test.go @@ -0,0 +1,151 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAWSAppmeshVirtualService_virtualNode(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appmesh_virtual_service.test" + dataSourceName := "data.aws_appmesh_virtual_service.test" + vsName := fmt.Sprintf("tf-acc-test-%d.mesh.local", acctest.RandInt()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsAppmeshVirtualServiceDataSourceConfig_virtualNode(rName, vsName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_date", dataSourceName, "created_date"), + resource.TestCheckResourceAttrPair(resourceName, "last_updated_date", dataSourceName, "last_updated_date"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_name", dataSourceName, "mesh_name"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_owner", dataSourceName, "mesh_owner"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_owner", dataSourceName, "resource_owner"), + resource.TestCheckResourceAttrPair(resourceName, "spec.0.provider.0.virtual_node.0.virtual_node_name", dataSourceName, "spec.0.provider.0.virtual_node.0.virtual_node_name"), + resource.TestCheckResourceAttrPair(resourceName, "tags", dataSourceName, "tags"), + ), + }, + }, + }) +} + +func TestAccDataSourceAWSAppmeshVirtualService_virtualRouter(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appmesh_virtual_service.test" + dataSourceName := "data.aws_appmesh_virtual_service.test" + vsName := fmt.Sprintf("tf-acc-test-%d.mesh.local", acctest.RandInt()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsAppmeshVirtualServiceDataSourceConfig_virtualRouter(rName, vsName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_date", dataSourceName, "created_date"), + resource.TestCheckResourceAttrPair(resourceName, "last_updated_date", dataSourceName, "last_updated_date"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_name", dataSourceName, "mesh_name"), + resource.TestCheckResourceAttrPair(resourceName, "mesh_owner", dataSourceName, "mesh_owner"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_owner", dataSourceName, "resource_owner"), + resource.TestCheckResourceAttrPair(resourceName, "spec.0.provider.0.virtual_router.0.virtual_router_name", dataSourceName, "spec.0.provider.0.virtual_router.0.virtual_router_name"), + resource.TestCheckResourceAttrPair(resourceName, "tags", dataSourceName, "tags"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppmeshVirtualServiceDataSourceConfig_virtualNode(rName, vsName string) string { + return fmt.Sprintf(` +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +resource "aws_appmesh_virtual_node" "test" { + name = %[1]q + mesh_name = aws_appmesh_mesh.test.id + + spec {} +} + +resource "aws_appmesh_virtual_service" "test" { + name = %[2]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + provider { + virtual_node { + virtual_node_name = aws_appmesh_virtual_node.test.name + } + } + } + + tags = { + foo = "bar" + good = "bad" + } +} + +data "aws_appmesh_virtual_service" "test" { + name = aws_appmesh_virtual_service.test.name + mesh_name = aws_appmesh_mesh.test.name +} +`, rName, vsName) +} + +func testAccCheckAwsAppmeshVirtualServiceDataSourceConfig_virtualRouter(rName, vsName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +resource "aws_appmesh_virtual_router" "test" { + name = %[1]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + listener { + port_mapping { + port = 8080 + protocol = "http" + } + } + } +} + +resource "aws_appmesh_virtual_service" "test" { + name = %[2]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + provider { + virtual_router { + virtual_router_name = aws_appmesh_virtual_router.test.name + } + } + } +} + +data "aws_appmesh_virtual_service" "test" { + name = aws_appmesh_virtual_service.test.name + mesh_name = aws_appmesh_mesh.test.name + mesh_owner = data.aws_caller_identity.current.account_id +} +`, rName, vsName) +} diff --git a/aws/data_source_aws_arn_test.go b/aws/data_source_aws_arn_test.go index 01b366802c8c..21870dc4efa6 100644 --- a/aws/data_source_aws_arn_test.go +++ b/aws/data_source_aws_arn_test.go @@ -22,8 +22,9 @@ func TestAccDataSourceAwsArn_basic(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsArnConfig(testARN.String()), diff --git a/aws/data_source_aws_autoscaling_group_test.go b/aws/data_source_aws_autoscaling_group_test.go index 8378e7754b28..77c8fb57a0f3 100644 --- a/aws/data_source_aws_autoscaling_group_test.go +++ b/aws/data_source_aws_autoscaling_group_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAwsAutoScalingGroupDataSource_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAutoScalingGroupDataResourceConfig(rName), @@ -46,8 +48,9 @@ func TestAccAwsAutoScalingGroupDataSource_launchTemplate(t *testing.T) { resourceName := "aws_autoscaling_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAutoScalingGroupDataResourceConfig_launchTemplate(), diff --git a/aws/data_source_aws_autoscaling_groups_test.go b/aws/data_source_aws_autoscaling_groups_test.go index 7059048cb154..b022e8435a26 100644 --- a/aws/data_source_aws_autoscaling_groups_test.go +++ b/aws/data_source_aws_autoscaling_groups_test.go @@ -7,6 +7,7 @@ import ( "strconv" "testing" + "github.com/aws/aws-sdk-go/service/autoscaling" "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" @@ -14,8 +15,9 @@ import ( func TestAccAWSAutoscalingGroups_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAutoscalingGroupsConfig(acctest.RandInt(), acctest.RandInt(), acctest.RandInt()), diff --git a/aws/data_source_aws_availability_zone_test.go b/aws/data_source_aws_availability_zone_test.go index 9f0f28f8f440..99db4bf475c4 100644 --- a/aws/data_source_aws_availability_zone_test.go +++ b/aws/data_source_aws_availability_zone_test.go @@ -14,8 +14,9 @@ func TestAccDataSourceAwsAvailabilityZone_AllAvailabilityZones(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigAllAvailabilityZones(), @@ -41,8 +42,9 @@ func TestAccDataSourceAwsAvailabilityZone_Filter(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigFilter(), @@ -68,8 +70,9 @@ func TestAccDataSourceAwsAvailabilityZone_LocalZone(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSLocalZoneAvailable(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSLocalZoneAvailable(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigZoneType("local-zone"), @@ -95,8 +98,9 @@ func TestAccDataSourceAwsAvailabilityZone_Name(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigName(), @@ -122,8 +126,9 @@ func TestAccDataSourceAwsAvailabilityZone_WavelengthZone(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigZoneType("wavelength-zone"), @@ -149,8 +154,9 @@ func TestAccDataSourceAwsAvailabilityZone_ZoneId(t *testing.T) { dataSourceName := "data.aws_availability_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsAvailabilityZoneConfigZoneId(), diff --git a/aws/data_source_aws_availability_zones_test.go b/aws/data_source_aws_availability_zones_test.go index 28ed3881ad12..c9e77a232d95 100644 --- a/aws/data_source_aws_availability_zones_test.go +++ b/aws/data_source_aws_availability_zones_test.go @@ -75,8 +75,9 @@ func TestAvailabilityZonesSort(t *testing.T) { func TestAccAWSAvailabilityZones_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesConfig, @@ -92,8 +93,9 @@ func TestAccAWSAvailabilityZones_AllAvailabilityZones(t *testing.T) { dataSourceName := "data.aws_availability_zones.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesConfigAllAvailabilityZones(), @@ -109,8 +111,9 @@ func TestAccAWSAvailabilityZones_Filter(t *testing.T) { dataSourceName := "data.aws_availability_zones.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesConfigFilter(), @@ -127,8 +130,9 @@ func TestAccAWSAvailabilityZones_ExcludeNames(t *testing.T) { excludeDataSourceName := "data.aws_availability_zones.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesConfigExcludeNames(), @@ -145,8 +149,9 @@ func TestAccAWSAvailabilityZones_ExcludeZoneIds(t *testing.T) { excludeDataSourceName := "data.aws_availability_zones.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesConfigExcludeZoneIds(), @@ -160,8 +165,9 @@ func TestAccAWSAvailabilityZones_ExcludeZoneIds(t *testing.T) { func TestAccAWSAvailabilityZones_stateFilter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsAvailabilityZonesStateConfig, diff --git a/aws/data_source_aws_backup_plan_test.go b/aws/data_source_aws_backup_plan_test.go index 51c501e3beb8..767778cb4694 100644 --- a/aws/data_source_aws_backup_plan_test.go +++ b/aws/data_source_aws_backup_plan_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/backup" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSBackupPlanDataSource_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsBackupPlanDataSourceConfig_nonExistent, diff --git a/aws/data_source_aws_backup_selection_test.go b/aws/data_source_aws_backup_selection_test.go index 9a6adf168764..ec2c1b87bb22 100644 --- a/aws/data_source_aws_backup_selection_test.go +++ b/aws/data_source_aws_backup_selection_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/backup" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSBackupSelectionDataSource_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsBackupSelectionDataSourceConfig_nonExistent, diff --git a/aws/data_source_aws_backup_vault_test.go b/aws/data_source_aws_backup_vault_test.go index 674bb0ef0166..cfc70f6b7aed 100644 --- a/aws/data_source_aws_backup_vault_test.go +++ b/aws/data_source_aws_backup_vault_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/backup" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSBackupVaultDataSource_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsBackupVaultDataSourceConfig_nonExistent, diff --git a/aws/data_source_aws_batch_compute_environment_test.go b/aws/data_source_aws_batch_compute_environment_test.go index 29ada47ad3af..cf87cf289a7f 100644 --- a/aws/data_source_aws_batch_compute_environment_test.go +++ b/aws/data_source_aws_batch_compute_environment_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/batch" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsBatchComputeEnvironment_basic(t *testing.T) { datasourceName := "data.aws_batch_compute_environment.by_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsBatchComputeEnvironmentConfig(rName), diff --git a/aws/data_source_aws_batch_job_queue_test.go b/aws/data_source_aws_batch_job_queue_test.go index ffdf63bddf99..ee1744933d1d 100644 --- a/aws/data_source_aws_batch_job_queue_test.go +++ b/aws/data_source_aws_batch_job_queue_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/batch" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsBatchJobQueue_basic(t *testing.T) { datasourceName := "data.aws_batch_job_queue.by_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsBatchJobQueueConfig(rName), diff --git a/aws/data_source_aws_billing_service_account_test.go b/aws/data_source_aws_billing_service_account_test.go index 189d109b749e..d9a42a647d18 100644 --- a/aws/data_source_aws_billing_service_account_test.go +++ b/aws/data_source_aws_billing_service_account_test.go @@ -12,8 +12,9 @@ func TestAccAWSBillingServiceAccount_basic(t *testing.T) { billingAccountID := "386209384616" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsBillingServiceAccountConfig, diff --git a/aws/data_source_aws_caller_identity.go b/aws/data_source_aws_caller_identity.go index 04b0b8c52d33..366275518ebd 100644 --- a/aws/data_source_aws_caller_identity.go +++ b/aws/data_source_aws_caller_identity.go @@ -39,7 +39,7 @@ func dataSourceAwsCallerIdentityRead(d *schema.ResourceData, meta interface{}) e res, err := client.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { - return fmt.Errorf("Error getting Caller Identity: %w", err) + return fmt.Errorf("getting Caller Identity: %w", err) } log.Printf("[DEBUG] Received Caller Identity: %s", res) diff --git a/aws/data_source_aws_caller_identity_test.go b/aws/data_source_aws_caller_identity_test.go index 12857aa20353..e73435d0ba1a 100644 --- a/aws/data_source_aws_caller_identity_test.go +++ b/aws/data_source_aws_caller_identity_test.go @@ -4,14 +4,16 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccAWSCallerIdentity_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sts.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCallerIdentityConfig_basic, diff --git a/aws/data_source_aws_canonical_user_id_test.go b/aws/data_source_aws_canonical_user_id_test.go index 4f482b0aa10b..27d57b02a5b3 100644 --- a/aws/data_source_aws_canonical_user_id_test.go +++ b/aws/data_source_aws_canonical_user_id_test.go @@ -4,14 +4,16 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccDataSourceAwsCanonicalUserId_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsCanonicalUserIdConfig, diff --git a/aws/data_source_aws_cloudcontrolapi_resource.go b/aws/data_source_aws_cloudcontrolapi_resource.go new file mode 100644 index 000000000000..54d6bf3ce6f6 --- /dev/null +++ b/aws/data_source_aws_cloudcontrolapi_resource.go @@ -0,0 +1,65 @@ +package aws + +import ( + "context" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/cloudcontrol/finder" +) + +func dataSourceAwsCloudControlApiResource() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsCloudControlApiResourceRead, + + Schema: map[string]*schema.Schema{ + "identifier": { + Type: schema.TypeString, + Required: true, + }, + "properties": { + Type: schema.TypeString, + Computed: true, + }, + "role_arn": { + Type: schema.TypeString, + Optional: true, + }, + "type_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}`), "must be three alphanumeric sections separated by double colons (::)"), + }, + "type_version_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsCloudControlApiResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudcontrolapiconn + + identifier := d.Get("identifier").(string) + resourceDescription, err := finder.ResourceByID(ctx, conn, + identifier, + d.Get("type_name").(string), + d.Get("type_version_id").(string), + d.Get("role_arn").(string), + ) + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Cloud Control API Resource (%s): %w", identifier, err)) + } + + d.SetId(aws.StringValue(resourceDescription.Identifier)) + + d.Set("properties", resourceDescription.Properties) + + return nil +} diff --git a/aws/data_source_aws_cloudcontrolapi_resource_test.go b/aws/data_source_aws_cloudcontrolapi_resource_test.go new file mode 100644 index 000000000000..152309a0165f --- /dev/null +++ b/aws/data_source_aws_cloudcontrolapi_resource_test.go @@ -0,0 +1,50 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAwsCloudControlApiResourceDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_cloudcontrolapi_resource.test" + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceDataSourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "properties", resourceName, "properties"), + resource.TestCheckResourceAttrPair(dataSourceName, "type_name", resourceName, "type_name"), + ), + }, + }, + }) +} + +func testAccAwsCloudControlApiResourceDataSourceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = %[1]q + }) +} + +data "aws_cloudcontrolapi_resource" "test" { + identifier = aws_cloudcontrolapi_resource.test.id + type_name = aws_cloudcontrolapi_resource.test.type_name +} +`, rName) +} diff --git a/aws/data_source_aws_cloudformation_export_test.go b/aws/data_source_aws_cloudformation_export_test.go index c5844f89e3e9..92573f4550dd 100644 --- a/aws/data_source_aws_cloudformation_export_test.go +++ b/aws/data_source_aws_cloudformation_export_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccAWSCloudformationExportDataSource_basic(t *testing.T) { dataSourceName := "data.aws_cloudformation_export.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudformationExportConfigStaticValue(rName), @@ -33,8 +35,9 @@ func TestAccAWSCloudformationExportDataSource_ResourceReference(t *testing.T) { resourceName := "aws_cloudformation_stack.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudformationExportConfigResourceReference(rName), diff --git a/aws/data_source_aws_cloudformation_stack_test.go b/aws/data_source_aws_cloudformation_stack_test.go index 843b6903c446..447065ec1f2e 100644 --- a/aws/data_source_aws_cloudformation_stack_test.go +++ b/aws/data_source_aws_cloudformation_stack_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSCloudFormationStack_dataSource_basic(t *testing.T) { resourceName := "data.aws_cloudformation_stack.network" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudFormationStackDataSourceConfig_basic(stackName), @@ -99,8 +101,9 @@ func TestAccAWSCloudFormationStack_dataSource_yaml(t *testing.T) { resourceName := "data.aws_cloudformation_stack.yaml" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudFormationStackDataSourceConfig_yaml(stackName), diff --git a/aws/data_source_aws_cloudformation_type.go b/aws/data_source_aws_cloudformation_type.go new file mode 100644 index 000000000000..cbcc760f0edc --- /dev/null +++ b/aws/data_source_aws_cloudformation_type.go @@ -0,0 +1,163 @@ +package aws + +import ( + "context" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "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" +) + +func dataSourceAwsCloudFormationType() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceAwsCloudFormationTypeRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "default_version_id": { + Type: schema.TypeString, + Computed: true, + }, + "deprecated_status": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "documentation_url": { + Type: schema.TypeString, + Computed: true, + }, + "execution_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "is_default_version": { + Type: schema.TypeBool, + Computed: true, + }, + "logging_config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_group_name": { + Type: schema.TypeString, + Computed: true, + }, + "log_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "provisioning_type": { + Type: schema.TypeString, + Computed: true, + }, + "schema": { + Type: schema.TypeString, + Computed: true, + }, + "source_url": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(cloudformation.RegistryType_Values(), false), + }, + "type_arn": { + Type: schema.TypeString, + Computed: true, + }, + "type_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.All( + validation.StringLenBetween(10, 204), + validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}(::MODULE){0,1}`), "three alphanumeric character sections separated by double colons (::)"), + ), + }, + "version_id": { + Type: schema.TypeString, + Optional: true, + }, + "visibility": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsCloudFormationTypeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cfconn + + input := &cloudformation.DescribeTypeInput{} + + if v, ok := d.GetOk("arn"); ok { + input.Arn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("type"); ok { + input.Type = aws.String(v.(string)) + } + + if v, ok := d.GetOk("type_name"); ok { + input.TypeName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("version_id"); ok { + input.VersionId = aws.String(v.(string)) + } + + output, err := conn.DescribeTypeWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading CloudFormation Type: %w", err)) + } + + if output == nil { + return diag.FromErr(fmt.Errorf("error reading CloudFormation Type: empty response")) + } + + d.SetId(aws.StringValue(output.Arn)) + + d.Set("arn", output.Arn) + d.Set("default_version_id", output.DefaultVersionId) + d.Set("deprecated_status", output.DeprecatedStatus) + d.Set("description", output.Description) + d.Set("documentation_url", output.DocumentationUrl) + d.Set("execution_role_arn", output.ExecutionRoleArn) + d.Set("is_default_version", output.IsDefaultVersion) + if output.LoggingConfig != nil { + if err := d.Set("logging_config", []interface{}{flattenCloudformationLoggingConfig(output.LoggingConfig)}); err != nil { + return diag.FromErr(fmt.Errorf("error setting logging_config: %w", err)) + } + } else { + d.Set("logging_config", nil) + } + d.Set("provisioning_type", output.ProvisioningType) + d.Set("schema", output.Schema) + d.Set("source_url", output.SourceUrl) + d.Set("type", output.Type) + d.Set("type_name", output.TypeName) + d.Set("visibility", output.Visibility) + + return nil +} diff --git a/aws/data_source_aws_cloudformation_type_test.go b/aws/data_source_aws_cloudformation_type_test.go new file mode 100644 index 000000000000..6fdf3b79db03 --- /dev/null +++ b/aws/data_source_aws_cloudformation_type_test.go @@ -0,0 +1,206 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAwsCloudformationTypeDataSource_Arn_Private(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + typeName := fmt.Sprintf("HashiCorp::TerraformAwsProvider::TfAccTest%s", acctest.RandString(8)) + zipPath := testAccAwsCloudformationTypeZipGenerator(t, typeName) + resourceName := "aws_cloudformation_type.test" + dataSourceName := "data.aws_cloudformation_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudformationTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudformationTypeDataSourceConfigArnPrivate(rName, zipPath, typeName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "deprecated_status", resourceName, "deprecated_status"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "documentation_url", resourceName, "documentation_url"), + resource.TestCheckResourceAttrPair(dataSourceName, "execution_role_arn", resourceName, "execution_role_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "is_default_version", resourceName, "is_default_version"), + resource.TestCheckResourceAttrPair(dataSourceName, "logging_config.#", resourceName, "logging_config.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "provisioning_type", resourceName, "provisioning_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "schema", resourceName, "schema"), + resource.TestCheckResourceAttrPair(dataSourceName, "source_url", resourceName, "source_url"), + resource.TestCheckResourceAttrPair(dataSourceName, "type", resourceName, "type"), + resource.TestCheckResourceAttrPair(dataSourceName, "type_name", resourceName, "type_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "visibility", resourceName, "visibility"), + ), + }, + }, + }) +} + +func TestAccAwsCloudformationTypeDataSource_Arn_Public(t *testing.T) { + dataSourceName := "data.aws_cloudformation_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudformationTypeDataSourceConfigArnPublic(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckResourceAttrRegionalARNNoAccount(dataSourceName, "arn", "cloudformation", "type/resource/AWS-Athena-WorkGroup"), + resource.TestCheckResourceAttr(dataSourceName, "deprecated_status", cloudformation.DeprecatedStatusLive), + resource.TestMatchResourceAttr(dataSourceName, "description", regexp.MustCompile(`.*`)), + resource.TestCheckResourceAttr(dataSourceName, "documentation_url", ""), + resource.TestCheckResourceAttr(dataSourceName, "is_default_version", "true"), + resource.TestCheckResourceAttr(dataSourceName, "logging_config.#", "0"), + resource.TestCheckResourceAttr(dataSourceName, "provisioning_type", cloudformation.ProvisioningTypeFullyMutable), + resource.TestMatchResourceAttr(dataSourceName, "schema", regexp.MustCompile(`^\{.*`)), + resource.TestMatchResourceAttr(dataSourceName, "source_url", regexp.MustCompile(`^https://.+`)), + resource.TestCheckResourceAttr(dataSourceName, "type", cloudformation.RegistryTypeResource), + resource.TestCheckResourceAttr(dataSourceName, "type_name", "AWS::Athena::WorkGroup"), + resource.TestCheckResourceAttr(dataSourceName, "visibility", cloudformation.VisibilityPublic), + ), + }, + }, + }) +} + +func TestAccAwsCloudformationTypeDataSource_TypeName_Private(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + typeName := fmt.Sprintf("HashiCorp::TerraformAwsProvider::TfAccTest%s", acctest.RandString(8)) + zipPath := testAccAwsCloudformationTypeZipGenerator(t, typeName) + resourceName := "aws_cloudformation_type.test" + dataSourceName := "data.aws_cloudformation_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudformationTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudformationTypeDataSourceConfigTypeNamePrivate(rName, zipPath, typeName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "deprecated_status", resourceName, "deprecated_status"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "documentation_url", resourceName, "documentation_url"), + resource.TestCheckResourceAttrPair(dataSourceName, "execution_role_arn", resourceName, "execution_role_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "is_default_version", resourceName, "is_default_version"), + resource.TestCheckResourceAttrPair(dataSourceName, "logging_config.#", resourceName, "logging_config.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "provisioning_type", resourceName, "provisioning_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "schema", resourceName, "schema"), + resource.TestCheckResourceAttrPair(dataSourceName, "source_url", resourceName, "source_url"), + resource.TestCheckResourceAttrPair(dataSourceName, "type", resourceName, "type"), + resource.TestCheckResourceAttrPair(dataSourceName, "type_name", resourceName, "type_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "visibility", resourceName, "visibility"), + ), + }, + }, + }) +} + +func TestAccAwsCloudformationTypeDataSource_TypeName_Public(t *testing.T) { + dataSourceName := "data.aws_cloudformation_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudformationTypeDataSourceConfigTypeNamePublic(), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckResourceAttrRegionalARNNoAccount(dataSourceName, "arn", "cloudformation", "type/resource/AWS-Athena-WorkGroup"), + resource.TestCheckResourceAttr(dataSourceName, "deprecated_status", cloudformation.DeprecatedStatusLive), + resource.TestMatchResourceAttr(dataSourceName, "description", regexp.MustCompile(`.*`)), + resource.TestCheckResourceAttr(dataSourceName, "documentation_url", ""), + resource.TestCheckResourceAttr(dataSourceName, "is_default_version", "true"), + resource.TestCheckResourceAttr(dataSourceName, "logging_config.#", "0"), + resource.TestCheckResourceAttr(dataSourceName, "provisioning_type", cloudformation.ProvisioningTypeFullyMutable), + resource.TestMatchResourceAttr(dataSourceName, "schema", regexp.MustCompile(`^\{.*`)), + resource.TestMatchResourceAttr(dataSourceName, "source_url", regexp.MustCompile(`^https://.+`)), + resource.TestCheckResourceAttr(dataSourceName, "type", cloudformation.RegistryTypeResource), + resource.TestCheckResourceAttr(dataSourceName, "type_name", "AWS::Athena::WorkGroup"), + resource.TestCheckResourceAttr(dataSourceName, "visibility", cloudformation.VisibilityPublic), + ), + }, + }, + }) +} + +func testAccCloudformationTypeConfigPrivateBase(rName string, zipPath string, typeName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_s3_bucket_object" "test" { + bucket = aws_s3_bucket.test.bucket + key = "test" + source = %[2]q +} + +resource "aws_cloudformation_type" "test" { + schema_handler_package = "s3://${aws_s3_bucket_object.test.bucket}/${aws_s3_bucket_object.test.key}" + type = "RESOURCE" + type_name = %[3]q +} +`, rName, zipPath, typeName) +} + +func testAccAwsCloudformationTypeDataSourceConfigArnPrivate(rName string, zipPath string, typeName string) string { + return composeConfig( + testAccCloudformationTypeConfigPrivateBase(rName, zipPath, typeName), + ` +data "aws_cloudformation_type" "test" { + arn = aws_cloudformation_type.test.arn +} +`) +} + +func testAccAwsCloudformationTypeDataSourceConfigArnPublic() string { + return ` +data "aws_partition" "current" {} + +data "aws_region" "current" {} + +data "aws_cloudformation_type" "test" { + arn = "arn:${data.aws_partition.current.partition}:cloudformation:${data.aws_region.current.name}::type/resource/AWS-Athena-WorkGroup" +} +` +} + +func testAccAwsCloudformationTypeDataSourceConfigTypeNamePrivate(rName string, zipPath string, typeName string) string { + return composeConfig( + testAccCloudformationTypeConfigPrivateBase(rName, zipPath, typeName), + ` +data "aws_cloudformation_type" "test" { + type = aws_cloudformation_type.test.type + type_name = aws_cloudformation_type.test.type_name +} +`) +} + +func testAccAwsCloudformationTypeDataSourceConfigTypeNamePublic() string { + return ` +data "aws_cloudformation_type" "test" { + type = "RESOURCE" + type_name = "AWS::Athena::WorkGroup" +} +` +} diff --git a/aws/data_source_aws_cloudfront_cache_policy.go b/aws/data_source_aws_cloudfront_cache_policy.go index a60434125f28..98f6f157e729 100644 --- a/aws/data_source_aws_cloudfront_cache_policy.go +++ b/aws/data_source_aws_cloudfront_cache_policy.go @@ -156,7 +156,7 @@ func dataSourceAwsCloudFrontCachePolicyRead(d *schema.ResourceData, meta interfa if err != nil { return fmt.Errorf("unable to retrieve cache policy with ID %s: %s", d.Id(), err.Error()) } - d.Set("etag", aws.StringValue(resp.ETag)) + d.Set("etag", resp.ETag) setCloudFrontCachePolicy(d, resp.CachePolicy.CachePolicyConfig) } @@ -173,7 +173,7 @@ func dataSourceAwsCloudFrontCachePolicyFindByName(d *schema.ResourceData, conn * } for _, policySummary := range resp.CachePolicyList.Items { - if *policySummary.CachePolicy.CachePolicyConfig.Name == d.Get("name").(string) { + if aws.StringValue(policySummary.CachePolicy.CachePolicyConfig.Name) == d.Get("name").(string) { cachePolicy = policySummary.CachePolicy break } diff --git a/aws/data_source_aws_cloudfront_cache_policy_test.go b/aws/data_source_aws_cloudfront_cache_policy_test.go index 4b8f20f93001..c0edfee27f77 100644 --- a/aws/data_source_aws_cloudfront_cache_policy_test.go +++ b/aws/data_source_aws_cloudfront_cache_policy_test.go @@ -15,6 +15,7 @@ func TestAccAWSCloudFrontDataSourceCachePolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudFrontPublicKeyDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_cloudfront_distribution_test.go b/aws/data_source_aws_cloudfront_distribution_test.go index f2f307972dec..9ae311253345 100644 --- a/aws/data_source_aws_cloudfront_distribution_test.go +++ b/aws/data_source_aws_cloudfront_distribution_test.go @@ -1,7 +1,6 @@ package aws import ( - "fmt" "testing" "github.com/aws/aws-sdk-go/service/cloudfront" @@ -12,13 +11,15 @@ import ( func TestAccAWSDataSourceCloudFrontDistribution_basic(t *testing.T) { dataSourceName := "data.aws_cloudfront_distribution.test" resourceName := "aws_cloudfront_distribution.s3_distribution" + rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAWSCloudFrontDistributionData, + Config: testAccAWSCloudFrontDistributionDataConfig(rInt), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceName, "domain_name", resourceName, "domain_name"), @@ -33,10 +34,12 @@ func TestAccAWSDataSourceCloudFrontDistribution_basic(t *testing.T) { }) } -var testAccAWSCloudFrontDistributionData = fmt.Sprintf(` -%s - +func testAccAWSCloudFrontDistributionDataConfig(rInt int) string { + return composeConfig( + testAccAWSCloudFrontDistributionS3ConfigWithTags(rInt), + ` data "aws_cloudfront_distribution" "test" { id = aws_cloudfront_distribution.s3_distribution.id } -`, fmt.Sprintf(testAccAWSCloudFrontDistributionS3ConfigWithTags, acctest.RandInt(), originBucket, logBucket, testAccAWSCloudFrontDistributionRetainConfig())) +`) +} diff --git a/aws/data_source_aws_cloudfront_function.go b/aws/data_source_aws_cloudfront_function.go new file mode 100644 index 000000000000..657edfe033fe --- /dev/null +++ b/aws/data_source_aws_cloudfront_function.go @@ -0,0 +1,102 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudfront/finder" +) + +func dataSourceAwsCloudFrontFunction() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCloudFrontFunctionRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "etag": { + Type: schema.TypeString, + Computed: true, + }, + + "code": { + Type: schema.TypeString, + Computed: true, + }, + + "comment": { + Type: schema.TypeString, + Computed: true, + }, + + "last_modified_time": { + Type: schema.TypeString, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "runtime": { + Type: schema.TypeString, + Computed: true, + }, + + "stage": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(cloudfront.FunctionStage_Values(), false), + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsCloudFrontFunctionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudfrontconn + + name := d.Get("name").(string) + stage := d.Get("stage").(string) + + describeFunctionOutput, err := finder.FunctionByNameAndStage(conn, name, stage) + + if err != nil { + return fmt.Errorf("error describing CloudFront Function (%s/%s): %w", name, stage, err) + } + + d.Set("arn", describeFunctionOutput.FunctionSummary.FunctionMetadata.FunctionARN) + d.Set("comment", describeFunctionOutput.FunctionSummary.FunctionConfig.Comment) + d.Set("etag", describeFunctionOutput.ETag) + d.Set("last_modified_time", describeFunctionOutput.FunctionSummary.FunctionMetadata.LastModifiedTime.Format(time.RFC3339)) + d.Set("name", describeFunctionOutput.FunctionSummary.Name) + d.Set("runtime", describeFunctionOutput.FunctionSummary.FunctionConfig.Runtime) + d.Set("status", describeFunctionOutput.FunctionSummary.Status) + + getFunctionOutput, err := conn.GetFunction(&cloudfront.GetFunctionInput{ + Name: aws.String(name), + Stage: aws.String(stage), + }) + + if err != nil { + return fmt.Errorf("error getting CloudFront Function (%s): %w", d.Id(), err) + } + + d.Set("code", string(getFunctionOutput.FunctionCode)) + + d.SetId(aws.StringValue(describeFunctionOutput.FunctionSummary.Name)) + + return nil +} diff --git a/aws/data_source_aws_cloudfront_function_test.go b/aws/data_source_aws_cloudfront_function_test.go new file mode 100644 index 000000000000..8e62312fce68 --- /dev/null +++ b/aws/data_source_aws_cloudfront_function_test.go @@ -0,0 +1,65 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAWSCloudfrontFunction_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_cloudfront_function.test" + resourceName := "aws_cloudfront_function.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCloudfrontFunctionConfigBasic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "code", resourceName, "code"), + resource.TestCheckResourceAttrPair(dataSourceName, "comment", resourceName, "comment"), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrSet(dataSourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "runtime", resourceName, "runtime"), + resource.TestCheckResourceAttrPair(dataSourceName, "status", resourceName, "status"), + ), + }, + }, + }) +} + +func testAccDataSourceAWSCloudfrontFunctionConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudfront_function" "test" { + name = %[1]q + runtime = "cloudfront-js-1.0" + comment = "test" + code = <<-EOT +function handler(event) { + var response = { + statusCode: 302, + statusDescription: 'Found', + headers: { + 'cloudfront-functions': { value: 'generated-by-CloudFront-Functions' }, + 'location': { value: 'https://aws.amazon.com/cloudfront/' } + } + }; + return response; +} +EOT +} + +data "aws_cloudfront_function" "test" { + name = aws_cloudfront_function.test.name + stage = "LIVE" +} +`, rName) +} diff --git a/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id.go b/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id.go new file mode 100644 index 000000000000..4e7d2887ecfa --- /dev/null +++ b/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id.go @@ -0,0 +1,44 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + // See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsBucketAndFileOwnership. + defaultCloudFrontLogDeliveryCanonicalUserId = "c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0" + + // See https://docs.amazonaws.cn/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsBucketAndFileOwnership. + cnCloudFrontLogDeliveryCanonicalUserId = "a52cb28745c0c06e84ec548334e44bfa7fc2a85c54af20cd59e4969344b7af56" +) + +func dataSourceAwsCloudFrontLogDeliveryCanonicalUserId() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCloudFrontLogDeliveryCanonicalUserIdRead, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsCloudFrontLogDeliveryCanonicalUserIdRead(d *schema.ResourceData, meta interface{}) error { + canonicalId := defaultCloudFrontLogDeliveryCanonicalUserId + + region := meta.(*AWSClient).region + if v, ok := d.GetOk("region"); ok { + region = v.(string) + } + + if v, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); ok && v.ID() == endpoints.AwsCnPartitionID { + canonicalId = cnCloudFrontLogDeliveryCanonicalUserId + } + + d.SetId(canonicalId) + + return nil +} diff --git a/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id_test.go b/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id_test.go new file mode 100644 index 000000000000..94005c114aba --- /dev/null +++ b/aws/data_source_aws_cloudfront_log_delivery_canonical_user_id_test.go @@ -0,0 +1,76 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserId_basic(t *testing.T) { + dataSourceName := "data.aws_cloudfront_log_delivery_canonical_user_id.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserIdConfig(""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "id", "c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0"), + ), + }, + }, + }) +} + +func TestAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserId_default(t *testing.T) { + dataSourceName := "data.aws_cloudfront_log_delivery_canonical_user_id.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserIdConfig(endpoints.UsWest2RegionID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "id", "c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0"), + ), + }, + }, + }) +} + +func TestAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserId_cn(t *testing.T) { + dataSourceName := "data.aws_cloudfront_log_delivery_canonical_user_id.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserIdConfig(endpoints.CnNorthwest1RegionID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "id", "a52cb28745c0c06e84ec548334e44bfa7fc2a85c54af20cd59e4969344b7af56"), + ), + }, + }, + }) +} + +func testAccDataSourceAWSCloudFrontLogDeliveryCanonicalUserIdConfig(region string) string { + if region == "" { + region = "null" + } + + return fmt.Sprintf(` +data "aws_cloudfront_log_delivery_canonical_user_id" "test" { + region = %[1]q +} +`, region) +} diff --git a/aws/data_source_aws_cloudfront_origin_request_policy.go b/aws/data_source_aws_cloudfront_origin_request_policy.go index c5ac2a9babb7..9ceaa2a725e3 100644 --- a/aws/data_source_aws_cloudfront_origin_request_policy.go +++ b/aws/data_source_aws_cloudfront_origin_request_policy.go @@ -126,11 +126,16 @@ func dataSourceAwsCloudFrontOriginRequestPolicyRead(d *schema.ResourceData, meta if err != nil { return fmt.Errorf("Unable to retrieve origin request policy with ID %s: %w", d.Id(), err) } - d.Set("etag", aws.StringValue(resp.ETag)) - originRequestPolicy := *resp.OriginRequestPolicy.OriginRequestPolicyConfig - d.Set("comment", aws.StringValue(originRequestPolicy.Comment)) - d.Set("name", aws.StringValue(originRequestPolicy.Name)) + if resp == nil || resp.OriginRequestPolicy == nil || resp.OriginRequestPolicy.OriginRequestPolicyConfig == nil { + return nil + } + + d.Set("etag", resp.ETag) + + originRequestPolicy := resp.OriginRequestPolicy.OriginRequestPolicyConfig + d.Set("comment", originRequestPolicy.Comment) + d.Set("name", originRequestPolicy.Name) d.Set("cookies_config", flattenCloudFrontOriginRequestPolicyCookiesConfig(originRequestPolicy.CookiesConfig)) d.Set("headers_config", flattenCloudFrontOriginRequestPolicyHeadersConfig(originRequestPolicy.HeadersConfig)) d.Set("query_strings_config", flattenCloudFrontOriginRequestPolicyQueryStringsConfig(originRequestPolicy.QueryStringsConfig)) @@ -148,7 +153,7 @@ func dataSourceAwsCloudFrontOriginRequestPolicyFindByName(d *schema.ResourceData } for _, policySummary := range resp.OriginRequestPolicyList.Items { - if *policySummary.OriginRequestPolicy.OriginRequestPolicyConfig.Name == d.Get("name").(string) { + if aws.StringValue(policySummary.OriginRequestPolicy.OriginRequestPolicyConfig.Name) == d.Get("name").(string) { originRequestPolicy = policySummary.OriginRequestPolicy break } diff --git a/aws/data_source_aws_cloudfront_origin_request_policy_test.go b/aws/data_source_aws_cloudfront_origin_request_policy_test.go index 10b55921ba3a..9d58f5eff1c3 100644 --- a/aws/data_source_aws_cloudfront_origin_request_policy_test.go +++ b/aws/data_source_aws_cloudfront_origin_request_policy_test.go @@ -15,6 +15,7 @@ func TestAccAWSCloudFrontDataSourceOriginRequestPolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloudfront.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloudfront.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCloudFrontPublicKeyDestroy, Steps: []resource.TestStep{ @@ -33,6 +34,7 @@ func TestAccAWSCloudFrontDataSourceOriginRequestPolicy_basic(t *testing.T) { }, }) } + func testAccAWSCloudFrontDataSourceOriginRequestPolicyConfig(rInt int) string { return fmt.Sprintf(` data "aws_cloudfront_origin_request_policy" "example" { diff --git a/aws/data_source_aws_cloudhsm2_cluster_test.go b/aws/data_source_aws_cloudhsm2_cluster_test.go deleted file mode 100644 index afabbe8324ca..000000000000 --- a/aws/data_source_aws_cloudhsm2_cluster_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package aws - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAccDataSourceCloudHsmV2Cluster_basic(t *testing.T) { - resourceName := "aws_cloudhsm_v2_cluster.cluster" - dataSourceName := "data.aws_cloudhsm_v2_cluster.default" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccCheckCloudHsmV2ClusterDataSourceConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceName, "cluster_state", "UNINITIALIZED"), - resource.TestCheckResourceAttrPair(dataSourceName, "cluster_id", resourceName, "cluster_id"), - resource.TestCheckResourceAttrPair(dataSourceName, "cluster_state", resourceName, "cluster_state"), - resource.TestCheckResourceAttrPair(dataSourceName, "security_group_id", resourceName, "security_group_id"), - resource.TestCheckResourceAttrPair(dataSourceName, "subnet_ids.#", resourceName, "subnet_ids.#"), - resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", resourceName, "vpc_id"), - ), - }, - }, - }) -} - -var testAccCheckCloudHsmV2ClusterDataSourceConfig = testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` -variable "subnets" { - default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -resource "aws_vpc" "cloudhsm_v2_test_vpc" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-aws_cloudhsm_v2_cluster-data-source-basic" - } -} - -resource "aws_subnet" "cloudhsm_v2_test_subnets" { - count = 2 - vpc_id = aws_vpc.cloudhsm_v2_test_vpc.id - cidr_block = element(var.subnets, count.index) - map_public_ip_on_launch = false - availability_zone = element(data.aws_availability_zones.available.names, count.index) - - tags = { - Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic" - } -} - -resource "aws_cloudhsm_v2_cluster" "cluster" { - hsm_type = "hsm1.medium" - subnet_ids = aws_subnet.cloudhsm_v2_test_subnets[*].id - - tags = { - Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic-%d" - } -} - -data "aws_cloudhsm_v2_cluster" "default" { - cluster_id = aws_cloudhsm_v2_cluster.cluster.cluster_id -} -`, acctest.RandInt()) diff --git a/aws/data_source_aws_cloudhsm2_cluster.go b/aws/data_source_aws_cloudhsm_v2_cluster.go similarity index 100% rename from aws/data_source_aws_cloudhsm2_cluster.go rename to aws/data_source_aws_cloudhsm_v2_cluster.go diff --git a/aws/data_source_aws_cloudhsm_v2_cluster_test.go b/aws/data_source_aws_cloudhsm_v2_cluster_test.go new file mode 100644 index 000000000000..124ecb61e134 --- /dev/null +++ b/aws/data_source_aws_cloudhsm_v2_cluster_test.go @@ -0,0 +1,74 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func testAccDataSourceCloudHsmV2Cluster_basic(t *testing.T) { + resourceName := "aws_cloudhsm_v2_cluster.cluster" + dataSourceName := "data.aws_cloudhsm_v2_cluster.default" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudhsmv2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudHsmV2ClusterDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "cluster_state", "UNINITIALIZED"), + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_id", resourceName, "cluster_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_state", resourceName, "cluster_state"), + resource.TestCheckResourceAttrPair(dataSourceName, "security_group_id", resourceName, "security_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "subnet_ids.#", resourceName, "subnet_ids.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", resourceName, "vpc_id"), + ), + }, + }, + }) +} + +var testAccCheckCloudHsmV2ClusterDataSourceConfig = composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = list(string) +} + +resource "aws_vpc" "cloudhsm_v2_test_vpc" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "terraform-testacc-aws_cloudhsm_v2_cluster-data-source-basic" + } +} + +resource "aws_subnet" "cloudhsm_v2_test_subnets" { + count = 2 + vpc_id = aws_vpc.cloudhsm_v2_test_vpc.id + cidr_block = element(var.subnets, count.index) + map_public_ip_on_launch = false + availability_zone = element(data.aws_availability_zones.available.names, count.index) + + tags = { + Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic" + } +} + +resource "aws_cloudhsm_v2_cluster" "cluster" { + hsm_type = "hsm1.medium" + subnet_ids = aws_subnet.cloudhsm_v2_test_subnets[*].id + + tags = { + Name = "tf-acc-aws_cloudhsm_v2_cluster-data-source-basic-%d" + } +} + +data "aws_cloudhsm_v2_cluster" "default" { + cluster_id = aws_cloudhsm_v2_cluster.cluster.cluster_id +} +`, acctest.RandInt())) diff --git a/aws/data_source_aws_cloudhsm_v2_test.go b/aws/data_source_aws_cloudhsm_v2_test.go new file mode 100644 index 000000000000..48048321e701 --- /dev/null +++ b/aws/data_source_aws_cloudhsm_v2_test.go @@ -0,0 +1,25 @@ +package aws + +import ( + "testing" +) + +func TestAccDataSourceCloudHsmV2_serial(t *testing.T) { + testCases := map[string]map[string]func(t *testing.T){ + "Cluster": { + "basic": testAccDataSourceCloudHsmV2Cluster_basic, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } + }) + } +} diff --git a/aws/data_source_aws_cloudtrail_service_account.go b/aws/data_source_aws_cloudtrail_service_account.go index 6c4498d69211..9e2a90ffb455 100644 --- a/aws/data_source_aws_cloudtrail_service_account.go +++ b/aws/data_source_aws_cloudtrail_service_account.go @@ -16,7 +16,7 @@ var cloudTrailServiceAccountPerRegionMap = map[string]string{ endpoints.ApEast1RegionID: "119688915426", endpoints.ApNortheast1RegionID: "216624486486", endpoints.ApNortheast2RegionID: "492519147666", - "ap-northeast-3": "765225791966", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "765225791966", endpoints.ApSouth1RegionID: "977081816279", endpoints.ApSoutheast1RegionID: "903692715234", endpoints.ApSoutheast2RegionID: "284668455005", diff --git a/aws/data_source_aws_cloudtrail_service_account_test.go b/aws/data_source_aws_cloudtrail_service_account_test.go index 04a489fb7496..c3b16abf9543 100644 --- a/aws/data_source_aws_cloudtrail_service_account_test.go +++ b/aws/data_source_aws_cloudtrail_service_account_test.go @@ -12,8 +12,9 @@ func TestAccAWSCloudTrailServiceAccount_basic(t *testing.T) { dataSourceName := "data.aws_cloudtrail_service_account.main" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudTrailServiceAccountConfig, @@ -32,8 +33,9 @@ func TestAccAWSCloudTrailServiceAccount_Region(t *testing.T) { dataSourceName := "data.aws_cloudtrail_service_account.regional" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCloudTrailServiceAccountConfigRegion, diff --git a/aws/data_source_aws_cloudwatch_event_connection.go b/aws/data_source_aws_cloudwatch_event_connection.go new file mode 100644 index 000000000000..4c46028e192d --- /dev/null +++ b/aws/data_source_aws_cloudwatch_event_connection.go @@ -0,0 +1,62 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsCloudwatchEventConnection() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCloudwatchEventConnectionRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "authorization_type": { + Type: schema.TypeString, + Computed: true, + }, + "secret_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsCloudwatchEventConnectionRead(d *schema.ResourceData, meta interface{}) error { + d.SetId(d.Get("name").(string)) + + conn := meta.(*AWSClient).cloudwatcheventsconn + + input := &events.DescribeConnectionInput{ + Name: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Reading CloudWatchEvent connection (%s)", d.Id()) + output, err := conn.DescribeConnection(input) + if err != nil { + return fmt.Errorf("error getting CloudWatchEvent connection (%s): %w", d.Id(), err) + } + + if output == nil { + return fmt.Errorf("error getting CloudWatchEvent connection (%s): empty response", d.Id()) + } + + log.Printf("[DEBUG] Found CloudWatchEvent connection: %#v", *output) + d.Set("arn", output.ConnectionArn) + d.Set("secret_arn", output.SecretArn) + d.Set("name", output.Name) + d.Set("authorization_type", output.AuthorizationType) + return nil +} diff --git a/aws/data_source_aws_cloudwatch_event_connection_test.go b/aws/data_source_aws_cloudwatch_event_connection_test.go new file mode 100644 index 000000000000..c148d19bc25a --- /dev/null +++ b/aws/data_source_aws_cloudwatch_event_connection_test.go @@ -0,0 +1,52 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSDataSourceCloudwatch_Event_Connection_basic(t *testing.T) { + dataSourceName := "data.aws_cloudwatch_event_connection.test" + resourceName := "aws_cloudwatch_event_connection.api_key" + + name := acctest.RandomWithPrefix("tf-acc-test") + authorizationType := "API_KEY" + description := acctest.RandomWithPrefix("tf-acc-test") + key := acctest.RandomWithPrefix("tf-acc-test") + value := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudwatch_Event_ConnectionDataConfig( + name, + description, + authorizationType, + key, + value, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "secret_arn", resourceName, "secret_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "authorization_type", resourceName, "authorization_type"), + ), + }, + }, + }) +} + +func testAccAWSCloudwatch_Event_ConnectionDataConfig(name, description, authorizationType, key, value string) string { + return composeConfig( + testAccAWSCloudWatchEventConnectionConfig_apiKey(name, description, authorizationType, key, value), + ` +data "aws_cloudwatch_event_connection" "test" { + name = aws_cloudwatch_event_connection.api_key.name +} +`) +} diff --git a/aws/data_source_aws_cloudwatch_event_source.go b/aws/data_source_aws_cloudwatch_event_source.go new file mode 100644 index 000000000000..b7c1d774fd27 --- /dev/null +++ b/aws/data_source_aws_cloudwatch_event_source.go @@ -0,0 +1,72 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsCloudWatchEventSource() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCloudWatchEventSourceRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsCloudWatchEventSourceRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatcheventsconn + + input := &events.ListEventSourcesInput{} + if v, ok := d.GetOk("name_prefix"); ok { + input.NamePrefix = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Listing cloudwatch Event sources: %s", input) + + resp, err := conn.ListEventSources(input) + if err != nil { + return fmt.Errorf("error listing cloudwatch event sources: %w", err) + } + + if resp == nil || len(resp.EventSources) == 0 { + return fmt.Errorf("no matching partner event source") + } + if len(resp.EventSources) > 1 { + return fmt.Errorf("multiple event sources matched; use additional constraints to reduce matches to a single event source") + } + + es := resp.EventSources[0] + + d.SetId(aws.StringValue(es.Name)) + d.Set("arn", es.Arn) + d.Set("created_by", es.CreatedBy) + d.Set("name", es.Name) + d.Set("state", es.State) + + return nil +} diff --git a/aws/data_source_aws_cloudwatch_event_source_test.go b/aws/data_source_aws_cloudwatch_event_source_test.go new file mode 100644 index 000000000000..b703324d0f41 --- /dev/null +++ b/aws/data_source_aws_cloudwatch_event_source_test.go @@ -0,0 +1,51 @@ +package aws + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsCloudWatchEventSource_basic(t *testing.T) { + key := "EVENT_BRIDGE_PARTNER_EVENT_SOURCE_NAME" + busName := os.Getenv(key) + if busName == "" { + t.Skipf("Environment variable %s is not set", key) + } + + parts := strings.Split(busName, "/") + if len(parts) < 2 { + t.Errorf("unable to parse partner event bus name %s", busName) + } + createdBy := parts[0] + "/" + parts[1] + + dataSourceName := "data.aws_cloudwatch_event_source.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchevents.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourcePartnerEventSourceConfig(busName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "name", busName), + resource.TestCheckResourceAttr(dataSourceName, "created_by", createdBy), + resource.TestCheckResourceAttrSet(dataSourceName, "arn"), + ), + }, + }, + }) +} + +func testAccAwsDataSourcePartnerEventSourceConfig(namePrefix string) string { + return fmt.Sprintf(` +data "aws_cloudwatch_event_source" "test" { + name_prefix = "%s" +} +`, namePrefix) +} diff --git a/aws/data_source_aws_cloudwatch_log_group_test.go b/aws/data_source_aws_cloudwatch_log_group_test.go index bc7c14f072b8..a64db6c89211 100644 --- a/aws/data_source_aws_cloudwatch_log_group_test.go +++ b/aws/data_source_aws_cloudwatch_log_group_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccAWSCloudwatchLogGroupDataSource_basic(t *testing.T) { resourceName := "data.aws_cloudwatch_log_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchlogs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCloudwatchLogGroupDataSourceConfig(rName), @@ -34,8 +36,9 @@ func TestAccAWSCloudwatchLogGroupDataSource_tags(t *testing.T) { resourceName := "data.aws_cloudwatch_log_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchlogs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCloudwatchLogGroupDataSourceConfigTags(rName), @@ -58,8 +61,9 @@ func TestAccAWSCloudwatchLogGroupDataSource_kms(t *testing.T) { resourceName := "data.aws_cloudwatch_log_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchlogs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCloudwatchLogGroupDataSourceConfigKMS(rName), @@ -80,8 +84,9 @@ func TestAccAWSCloudwatchLogGroupDataSource_retention(t *testing.T) { resourceName := "data.aws_cloudwatch_log_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchlogs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCloudwatchLogGroupDataSourceConfigRetention(rName), diff --git a/aws/data_source_aws_cloudwatch_log_groups.go b/aws/data_source_aws_cloudwatch_log_groups.go new file mode 100644 index 000000000000..e54753ebef55 --- /dev/null +++ b/aws/data_source_aws_cloudwatch_log_groups.go @@ -0,0 +1,71 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfcloudwatchlogs "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchlogs" +) + +func dataSourceAwsCloudwatchLogGroups() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCloudwatchLogGroupsRead, + + Schema: map[string]*schema.Schema{ + "arns": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "log_group_name_prefix": { + Type: schema.TypeString, + Required: true, + }, + "log_group_names": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsCloudwatchLogGroupsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchlogsconn + + input := &cloudwatchlogs.DescribeLogGroupsInput{ + LogGroupNamePrefix: aws.String(d.Get("log_group_name_prefix").(string)), + } + + var results []*cloudwatchlogs.LogGroup + + err := conn.DescribeLogGroupsPages(input, func(page *cloudwatchlogs.DescribeLogGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + results = append(results, page.LogGroups...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error reading CloudWatch Log Groups: %w", err) + } + + d.SetId(meta.(*AWSClient).region) + + var arns, logGroupNames []string + + for _, r := range results { + arns = append(arns, tfcloudwatchlogs.TrimLogGroupARNWildcardSuffix(aws.StringValue(r.Arn))) + logGroupNames = append(logGroupNames, aws.StringValue(r.LogGroupName)) + } + + d.Set("arns", arns) + d.Set("log_group_names", logGroupNames) + + return nil +} diff --git a/aws/data_source_aws_cloudwatch_log_groups_test.go b/aws/data_source_aws_cloudwatch_log_groups_test.go new file mode 100644 index 000000000000..f833b27416d3 --- /dev/null +++ b/aws/data_source_aws_cloudwatch_log_groups_test.go @@ -0,0 +1,52 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSCloudwatchLogGroupsDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "data.aws_cloudwatch_log_groups.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudwatchlogs.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAWSCloudwatchLogGroupsDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "arns.#", "2"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "arns.*", "aws_cloudwatch_log_group.test1", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "arns.*", "aws_cloudwatch_log_group.test2", "arn"), + resource.TestCheckResourceAttr(resourceName, "log_group_names.#", "2"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "log_group_names.*", "aws_cloudwatch_log_group.test1", "name"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "log_group_names.*", "aws_cloudwatch_log_group.test2", "name"), + ), + }, + }, + }) +} + +func testAccCheckAWSCloudwatchLogGroupsDataSourceConfig(rName string) string { + return fmt.Sprintf(` +resource aws_cloudwatch_log_group "test1" { + name = "%[1]s/1" +} + +resource aws_cloudwatch_log_group "test2" { + name = "%[1]s/2" +} + +data aws_cloudwatch_log_groups "test" { + log_group_name_prefix = %[1]q + + depends_on = [aws_cloudwatch_log_group.test1,aws_cloudwatch_log_group.test2] +} +`, rName) +} diff --git a/aws/data_source_aws_codeartifact_authorization_token.go b/aws/data_source_aws_codeartifact_authorization_token.go index 4f39c41d9444..9fb75285e45d 100644 --- a/aws/data_source_aws_codeartifact_authorization_token.go +++ b/aws/data_source_aws_codeartifact_authorization_token.go @@ -71,7 +71,7 @@ func dataSourceAwsCodeArtifactAuthorizationTokenRead(d *schema.ResourceData, met log.Printf("[DEBUG] CodeArtifact authorization token: %#v", out) d.SetId(fmt.Sprintf("%s:%s", domainOwner, domain)) - d.Set("authorization_token", aws.StringValue(out.AuthorizationToken)) + d.Set("authorization_token", out.AuthorizationToken) d.Set("expiration", aws.TimeValue(out.Expiration).Format(time.RFC3339)) d.Set("domain_owner", domainOwner) diff --git a/aws/data_source_aws_codeartifact_authorization_token_test.go b/aws/data_source_aws_codeartifact_authorization_token_test.go index 9c42f339a7e9..8026a7564a58 100644 --- a/aws/data_source_aws_codeartifact_authorization_token_test.go +++ b/aws/data_source_aws_codeartifact_authorization_token_test.go @@ -14,8 +14,9 @@ func TestAccAWSCodeArtifactAuthorizationTokenDataSource_basic(t *testing.T) { dataSourceName := "data.aws_codeartifact_authorization_token.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codeartifact.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCodeArtifactAuthorizationTokenBasicConfig(rName), @@ -34,8 +35,9 @@ func TestAccAWSCodeArtifactAuthorizationTokenDataSource_owner(t *testing.T) { dataSourceName := "data.aws_codeartifact_authorization_token.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codeartifact.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCodeArtifactAuthorizationTokenOwnerConfig(rName), @@ -54,8 +56,9 @@ func TestAccAWSCodeArtifactAuthorizationTokenDataSource_duration(t *testing.T) { dataSourceName := "data.aws_codeartifact_authorization_token.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codeartifact.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCodeArtifactAuthorizationTokenDurationConfig(rName), diff --git a/aws/data_source_aws_codeartifact_repository_endpoint_test.go b/aws/data_source_aws_codeartifact_repository_endpoint_test.go index 6e7e0ed6d242..d35716b6a8f4 100644 --- a/aws/data_source_aws_codeartifact_repository_endpoint_test.go +++ b/aws/data_source_aws_codeartifact_repository_endpoint_test.go @@ -14,8 +14,9 @@ func TestAccAWSCodeArtifactRepositoryEndpointDataSource_basic(t *testing.T) { dataSourceName := "data.aws_codeartifact_repository_endpoint.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codeartifact.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCodeArtifactRepositoryEndpointBasicConfig(rName, "npm"), @@ -54,8 +55,9 @@ func TestAccAWSCodeArtifactRepositoryEndpointDataSource_owner(t *testing.T) { dataSourceName := "data.aws_codeartifact_repository_endpoint.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codeartifact.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codeartifact.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSCodeArtifactRepositoryEndpointOwnerConfig(rName), diff --git a/aws/data_source_aws_codecommit_repository_test.go b/aws/data_source_aws_codecommit_repository_test.go index e1a6ce85ea8f..c33b8fb58713 100644 --- a/aws/data_source_aws_codecommit_repository_test.go +++ b/aws/data_source_aws_codecommit_repository_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/codecommit" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSCodeCommitRepositoryDataSource_basic(t *testing.T) { datasourceName := "data.aws_codecommit_repository.default" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, codecommit.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsCodeCommitRepositoryDataSourceConfig(rName), diff --git a/aws/data_source_aws_codestarconnections_connection.go b/aws/data_source_aws_codestarconnections_connection.go new file mode 100644 index 000000000000..1c7a635b8d26 --- /dev/null +++ b/aws/data_source_aws_codestarconnections_connection.go @@ -0,0 +1,76 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/codestarconnections/finder" +) + +func dataSourceAwsCodeStarConnectionsConnection() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsCodeStarConnectionsConnectionRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + }, + + "connection_status": { + Type: schema.TypeString, + Computed: true, + }, + + "host_arn": { + Type: schema.TypeString, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "provider_type": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsCodeStarConnectionsConnectionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).codestarconnectionsconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + arn := d.Get("arn").(string) + + log.Printf("[DEBUG] Getting CodeStar Connection") + connection, err := finder.ConnectionByArn(conn, arn) + if err != nil { + return fmt.Errorf("error getting CodeStar Connection (%s): %w", arn, err) + } + log.Printf("[DEBUG] CodeStar Connection: %#v", connection) + + d.SetId(arn) + d.Set("connection_status", connection.ConnectionStatus) + d.Set("host_arn", connection.HostArn) + d.Set("name", connection.ConnectionName) + d.Set("provider_type", connection.ProviderType) + + tags, err := keyvaluetags.CodestarconnectionsListTags(conn, arn) + if err != nil { + return fmt.Errorf("error listing tags for CodeStar Connection (%s): %w", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags for CodeStar Connection (%s): %w", arn, err) + } + + return nil +} diff --git a/aws/data_source_aws_codestarconnections_connection_test.go b/aws/data_source_aws_codestarconnections_connection_test.go new file mode 100644 index 000000000000..4c817aa39cb9 --- /dev/null +++ b/aws/data_source_aws_codestarconnections_connection_test.go @@ -0,0 +1,86 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/codestarconnections" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsCodeStarConnectionsConnection_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_codestarconnections_connection.test" + resourceName := "aws_codestarconnections_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codestarconnections.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codestarconnections.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCodeStarConnectionsConnectionConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "provider_type", dataSourceName, "provider_type"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "connection_status", dataSourceName, "connection_status"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsCodeStarConnectionsConnection_tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_codestarconnections_connection.test" + resourceName := "aws_codestarconnections_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(codestarconnections.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, codestarconnections.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSCodeStarConnectionsConnectionConfigTags(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + ), + }, + }, + }) +} + +func testAccDataSourceAWSCodeStarConnectionsConnectionConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_codestarconnections_connection" "test" { + name = %[1]q + provider_type = "Bitbucket" +} + +data "aws_codestarconnections_connection" "test" { + arn = aws_codestarconnections_connection.test.arn +} +`, rName) +} + +func testAccDataSourceAWSCodeStarConnectionsConnectionConfigTags(rName string) string { + return fmt.Sprintf(` +resource "aws_codestarconnections_connection" "test" { + name = %[1]q + provider_type = "Bitbucket" + + tags = { + "key1" = "value1" + "key2" = "value2" + } +} + +data "aws_codestarconnections_connection" "test" { + arn = aws_codestarconnections_connection.test.arn +} +`, rName) +} diff --git a/aws/data_source_aws_cognito_user_pools_test.go b/aws/data_source_aws_cognito_user_pools_test.go index da850d68f56c..3b940f26f03b 100644 --- a/aws/data_source_aws_cognito_user_pools_test.go +++ b/aws/data_source_aws_cognito_user_pools_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ import ( func TestAccDataSourceAwsCognitoUserPools_basic(t *testing.T) { rName := fmt.Sprintf("tf_acc_ds_cognito_user_pools_%s", acctest.RandString(7)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCognitoIdentityProvider(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCognitoIdentityProvider(t) }, + ErrorCheck: testAccErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsCognitoUserPoolsConfig_basic(rName), diff --git a/aws/data_source_aws_connect_contact_flow.go b/aws/data_source_aws_connect_contact_flow.go new file mode 100644 index 000000000000..c8b3bd549fac --- /dev/null +++ b/aws/data_source_aws_connect_contact_flow.go @@ -0,0 +1,144 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfconnect "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/connect" +) + +func dataSourceAwsConnectContactFlow() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsConnectContactFlowRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "contact_flow_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"contact_flow_id", "name"}, + }, + "content": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "instance_id": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"name", "contact_flow_id"}, + }, + "tags": tagsSchemaComputed(), + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsConnectContactFlowRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).connectconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + instanceID := d.Get("instance_id").(string) + + input := &connect.DescribeContactFlowInput{ + InstanceId: aws.String(instanceID), + } + + if v, ok := d.GetOk("contact_flow_id"); ok { + input.ContactFlowId = aws.String(v.(string)) + } else if v, ok := d.GetOk("name"); ok { + name := v.(string) + contactFlowSummary, err := dataSourceAwsConnectGetConnectContactFlowSummaryByName(ctx, conn, instanceID, name) + + if err != nil { + return diag.FromErr(fmt.Errorf("error finding Connect Contact Flow Summary by name (%s): %w", name, err)) + } + + if contactFlowSummary == nil { + return diag.FromErr(fmt.Errorf("error finding Connect Contact Flow Summary by name (%s): not found", name)) + } + + input.ContactFlowId = contactFlowSummary.Id + } + + resp, err := conn.DescribeContactFlow(input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error getting Connect Contact Flow: %w", err)) + } + + if resp == nil || resp.ContactFlow == nil { + return diag.FromErr(fmt.Errorf("error getting Connect Contact Flow: empty response")) + } + + contactFlow := resp.ContactFlow + + d.Set("arn", contactFlow.Arn) + d.Set("instance_id", instanceID) + d.Set("contact_flow_id", contactFlow.Id) + d.Set("name", contactFlow.Name) + d.Set("description", contactFlow.Description) + d.Set("content", contactFlow.Content) + d.Set("type", contactFlow.Type) + + if err := d.Set("tags", keyvaluetags.ConnectKeyValueTags(contactFlow.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %s", err)) + } + + d.SetId(fmt.Sprintf("%s:%s", instanceID, aws.StringValue(contactFlow.Id))) + + return nil +} + +func dataSourceAwsConnectGetConnectContactFlowSummaryByName(ctx context.Context, conn *connect.Connect, instanceID, name string) (*connect.ContactFlowSummary, error) { + var result *connect.ContactFlowSummary + + input := &connect.ListContactFlowsInput{ + InstanceId: aws.String(instanceID), + MaxResults: aws.Int64(tfconnect.ListContactFlowsMaxResults), + } + + err := conn.ListContactFlowsPagesWithContext(ctx, input, func(page *connect.ListContactFlowsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, cf := range page.ContactFlowSummaryList { + if cf == nil { + continue + } + + if aws.StringValue(cf.Name) == name { + result = cf + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/aws/data_source_aws_connect_contact_flow_test.go b/aws/data_source_aws_connect_contact_flow_test.go new file mode 100644 index 000000000000..9a91b3cb9410 --- /dev/null +++ b/aws/data_source_aws_connect_contact_flow_test.go @@ -0,0 +1,109 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAwsConnectContactFlowDataSource_ContactFlowId(t *testing.T) { + rName := acctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_connect_contact_flow.test" + datasourceName := "data.aws_connect_contact_flow.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, connect.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsConnectContactFlowDataSourceConfig_ContactFlowId(rName, resourceName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "contact_flow_id", resourceName, "contact_flow_id"), + resource.TestCheckResourceAttrPair(datasourceName, "instance_id", resourceName, "instance_id"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "content", resourceName, "content"), + resource.TestCheckResourceAttrPair(datasourceName, "type", resourceName, "type"), + resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), + ), + }, + }, + }) +} + +func TestAccAwsConnectContactFlowDataSource_Name(t *testing.T) { + rName := acctest.RandomWithPrefix("resource-test-terraform") + rName2 := acctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_connect_contact_flow.test" + datasourceName := "data.aws_connect_contact_flow.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, connect.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsConnectContactFlowDataSourceConfig_Name(rName, rName2), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "contact_flow_id", resourceName, "contact_flow_id"), + resource.TestCheckResourceAttrPair(datasourceName, "instance_id", resourceName, "instance_id"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "content", resourceName, "content"), + resource.TestCheckResourceAttrPair(datasourceName, "type", resourceName, "type"), + resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), + ), + }, + }, + }) +} + +func testAccAwsConnectContactFlowDataSourceBaseConfig(rName, rName2 string) string { + return fmt.Sprintf(` +resource "aws_connect_instance" "test" { + identity_management_type = "CONNECT_MANAGED" + inbound_calls_enabled = true + instance_alias = %[1]q + outbound_calls_enabled = true +} + +resource "aws_connect_contact_flow" "test" { + instance_id = aws_connect_instance.test.id + name = %[2]q + description = "Test Contact Flow Description" + type = "CONTACT_FLOW" + content = file("./testdata/service/connect/connect_contact_flow.json") + tags = { + "Name" = "Test Contact Flow", + "Application" = "Terraform", + "Method" = "Create" + } +} + `, rName, rName2) +} + +func testAccAwsConnectContactFlowDataSourceConfig_ContactFlowId(rName, rName2 string) string { + return fmt.Sprintf(testAccAwsConnectContactFlowDataSourceBaseConfig(rName, rName2) + ` +data "aws_connect_contact_flow" "test" { + instance_id = aws_connect_instance.test.id + contact_flow_id = aws_connect_contact_flow.test.contact_flow_id +} +`) +} + +func testAccAwsConnectContactFlowDataSourceConfig_Name(rName, rName2 string) string { + return fmt.Sprintf(testAccAwsConnectContactFlowDataSourceBaseConfig(rName, rName2) + ` +data "aws_connect_contact_flow" "test" { + instance_id = aws_connect_instance.test.id + name = aws_connect_contact_flow.test.name +} +`) +} diff --git a/aws/data_source_aws_connect_instance.go b/aws/data_source_aws_connect_instance.go new file mode 100644 index 000000000000..89c804927514 --- /dev/null +++ b/aws/data_source_aws_connect_instance.go @@ -0,0 +1,210 @@ +package aws + +import ( + "context" + "fmt" + "log" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfconnect "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/connect" +) + +func dataSourceAwsConnectInstance() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsConnectInstanceRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "auto_resolve_best_voices_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "contact_flow_logs_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "contact_lens_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "early_media_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "identity_management_type": { + Type: schema.TypeString, + Computed: true, + }, + "inbound_calls_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "instance_alias": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"instance_alias", "instance_id"}, + }, + "instance_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"instance_id", "instance_alias"}, + }, + "outbound_calls_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "service_role": { + Type: schema.TypeString, + Computed: true, + }, + // "use_custom_tts_voices_enabled": { + // Type: schema.TypeBool, + // Computed: true, + // }, + }, + } +} + +func dataSourceAwsConnectInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).connectconn + + var matchedInstance *connect.Instance + + if v, ok := d.GetOk("instance_id"); ok { + instanceId := v.(string) + + input := connect.DescribeInstanceInput{ + InstanceId: aws.String(instanceId), + } + + log.Printf("[DEBUG] Reading Connect Instance by instance_id: %s", input) + + output, err := conn.DescribeInstance(&input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error getting Connect Instance by instance_id (%s): %w", instanceId, err)) + } + + if output == nil { + return diag.FromErr(fmt.Errorf("error getting Connect Instance by instance_id (%s): empty output", instanceId)) + } + + matchedInstance = output.Instance + + } else if v, ok := d.GetOk("instance_alias"); ok { + instanceAlias := v.(string) + + instanceSummary, err := dataSourceAwsConnectGetConnectInstanceSummaryByInstanceAlias(ctx, conn, instanceAlias) + + if err != nil { + return diag.FromErr(fmt.Errorf("error finding Connect Instance Summary by instance_alias (%s): %w", instanceAlias, err)) + } + + if instanceSummary == nil { + return diag.FromErr(fmt.Errorf("error finding Connect Instance Summary by instance_alias (%s): not found", instanceAlias)) + } + + matchedInstance = &connect.Instance{ + Arn: instanceSummary.Arn, + CreatedTime: instanceSummary.CreatedTime, + Id: instanceSummary.Id, + IdentityManagementType: instanceSummary.IdentityManagementType, + InboundCallsEnabled: instanceSummary.InboundCallsEnabled, + InstanceAlias: instanceSummary.InstanceAlias, + InstanceStatus: instanceSummary.InstanceStatus, + OutboundCallsEnabled: instanceSummary.OutboundCallsEnabled, + ServiceRole: instanceSummary.ServiceRole, + } + } + + if matchedInstance == nil { + return diag.FromErr(fmt.Errorf("no Connect Instance found for query, try adjusting your search criteria")) + } + + d.SetId(aws.StringValue(matchedInstance.Id)) + + d.Set("arn", matchedInstance.Arn) + d.Set("created_time", matchedInstance.CreatedTime.Format(time.RFC3339)) + d.Set("identity_management_type", matchedInstance.IdentityManagementType) + d.Set("inbound_calls_enabled", matchedInstance.InboundCallsEnabled) + d.Set("instance_alias", matchedInstance.InstanceAlias) + d.Set("outbound_calls_enabled", matchedInstance.OutboundCallsEnabled) + d.Set("service_role", matchedInstance.ServiceRole) + d.Set("status", matchedInstance.InstanceStatus) + + for att := range tfconnect.InstanceAttributeMapping() { + value, err := dataResourceAwsConnectInstanceReadAttribute(ctx, conn, d.Id(), att) + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Connect Instance (%s) attribute (%s): %w", d.Id(), att, err)) + } + d.Set(tfconnect.InstanceAttributeMapping()[att], value) + } + + return nil +} + +func dataSourceAwsConnectGetConnectInstanceSummaryByInstanceAlias(ctx context.Context, conn *connect.Connect, instanceAlias string) (*connect.InstanceSummary, error) { + var result *connect.InstanceSummary + + input := &connect.ListInstancesInput{ + MaxResults: aws.Int64(tfconnect.ListInstancesMaxResults), + } + + err := conn.ListInstancesPagesWithContext(ctx, input, func(page *connect.ListInstancesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, is := range page.InstanceSummaryList { + if is == nil { + continue + } + + if aws.StringValue(is.InstanceAlias) == instanceAlias { + result = is + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +func dataResourceAwsConnectInstanceReadAttribute(ctx context.Context, conn *connect.Connect, instanceID string, attributeType string) (bool, error) { + input := &connect.DescribeInstanceAttributeInput{ + InstanceId: aws.String(instanceID), + AttributeType: aws.String(attributeType), + } + + out, err := conn.DescribeInstanceAttributeWithContext(ctx, input) + + if err != nil { + return false, err + } + + result, parseErr := strconv.ParseBool(*out.Attribute.Value) + return result, parseErr +} diff --git a/aws/data_source_aws_connect_instance_test.go b/aws/data_source_aws_connect_instance_test.go new file mode 100644 index 000000000000..cb18c4b7944c --- /dev/null +++ b/aws/data_source_aws_connect_instance_test.go @@ -0,0 +1,108 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAwsConnectInstanceDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("datasource-test-terraform") + dataSourceName := "data.aws_connect_instance.test" + resourceName := "aws_connect_instance.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, connect.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsConnectInstanceDataSourceConfig_nonExistentId, + ExpectError: regexp.MustCompile(`error getting Connect Instance by instance_id`), + }, + { + Config: testAccAwsConnectInstanceDataSourceConfig_nonExistentAlias, + ExpectError: regexp.MustCompile(`error finding Connect Instance Summary by instance_alias`), + }, + { + Config: testAccAwsConnectInstanceDataSourceConfigBasic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_time", dataSourceName, "created_time"), + resource.TestCheckResourceAttrPair(resourceName, "identity_management_type", dataSourceName, "identity_management_type"), + resource.TestCheckResourceAttrPair(resourceName, "instance_alias", dataSourceName, "instance_alias"), + resource.TestCheckResourceAttrPair(resourceName, "inbound_calls_enabled", dataSourceName, "inbound_calls_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "outbound_calls_enabled", dataSourceName, "outbound_calls_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "contact_flow_logs_enabled", dataSourceName, "contact_flow_logs_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "contact_lens_enabled", dataSourceName, "contact_lens_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "auto_resolve_best_voices_enabled", dataSourceName, "auto_resolve_best_voices_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "early_media_enabled", dataSourceName, "early_media_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", dataSourceName, "service_role"), + ), + }, + { + Config: testAccAwsConnectInstanceDataSourceConfigAlias(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_time", dataSourceName, "created_time"), + resource.TestCheckResourceAttrPair(resourceName, "identity_management_type", dataSourceName, "identity_management_type"), + resource.TestCheckResourceAttrPair(resourceName, "instance_alias", dataSourceName, "instance_alias"), + resource.TestCheckResourceAttrPair(resourceName, "inbound_calls_enabled", dataSourceName, "inbound_calls_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "outbound_calls_enabled", dataSourceName, "outbound_calls_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "contact_flow_logs_enabled", dataSourceName, "contact_flow_logs_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "contact_lens_enabled", dataSourceName, "contact_lens_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "auto_resolve_best_voices_enabled", dataSourceName, "auto_resolve_best_voices_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "early_media_enabled", dataSourceName, "early_media_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", dataSourceName, "service_role"), + ), + }, + }, + }) +} + +const testAccAwsConnectInstanceDataSourceConfig_nonExistentId = ` +data "aws_connect_instance" "test" { + instance_id = "97afc98d-101a-ba98-ab97-ae114fc115ec" +} +` + +const testAccAwsConnectInstanceDataSourceConfig_nonExistentAlias = ` +data "aws_connect_instance" "test" { + instance_alias = "tf-acc-test-does-not-exist" +} +` + +func testAccAwsConnectInstanceDataSourceConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_connect_instance" "test" { + instance_alias = %[1]q + identity_management_type = "CONNECT_MANAGED" + inbound_calls_enabled = true + outbound_calls_enabled = true +} + +data "aws_connect_instance" "test" { + instance_id = aws_connect_instance.test.id +} +`, rName) +} + +func testAccAwsConnectInstanceDataSourceConfigAlias(rName string) string { + return fmt.Sprintf(` +resource "aws_connect_instance" "test" { + instance_alias = %[1]q + identity_management_type = "CONNECT_MANAGED" + inbound_calls_enabled = true + outbound_calls_enabled = true +} + +data "aws_connect_instance" "test" { + instance_alias = aws_connect_instance.test.instance_alias +} +`, rName) +} diff --git a/aws/data_source_aws_cur_report_definition.go b/aws/data_source_aws_cur_report_definition.go index 239bffd56aa3..adbffae3ef3a 100644 --- a/aws/data_source_aws_cur_report_definition.go +++ b/aws/data_source_aws_cur_report_definition.go @@ -1,7 +1,11 @@ package aws import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/costandusagereportservice/finder" ) func dataSourceAwsCurReportDefinition() *schema.Resource { @@ -62,6 +66,32 @@ func dataSourceAwsCurReportDefinition() *schema.Resource { } func dataSourceAwsCurReportDefinitionRead(d *schema.ResourceData, meta interface{}) error { - d.SetId(d.Get("report_name").(string)) - return resourceAwsCurReportDefinitionRead(d, meta) + conn := meta.(*AWSClient).costandusagereportconn + + reportName := d.Get("report_name").(string) + + reportDefinition, err := finder.ReportDefinitionByName(conn, reportName) + + if err != nil { + return fmt.Errorf("error reading Report Definition (%s): %w", reportName, err) + } + + if reportDefinition == nil { + return fmt.Errorf("error reading Report Definition (%s): not found", reportName) + } + + d.SetId(aws.StringValue(reportDefinition.ReportName)) + d.Set("report_name", reportDefinition.ReportName) + d.Set("time_unit", reportDefinition.TimeUnit) + d.Set("format", reportDefinition.Format) + d.Set("compression", reportDefinition.Compression) + d.Set("additional_schema_elements", aws.StringValueSlice(reportDefinition.AdditionalSchemaElements)) + d.Set("s3_bucket", reportDefinition.S3Bucket) + d.Set("s3_prefix", reportDefinition.S3Prefix) + d.Set("s3_region", reportDefinition.S3Region) + d.Set("additional_artifacts", aws.StringValueSlice(reportDefinition.AdditionalArtifacts)) + d.Set("refresh_closed_reports", reportDefinition.RefreshClosedReports) + d.Set("report_versioning", reportDefinition.ReportVersioning) + + return nil } diff --git a/aws/data_source_aws_cur_report_definition_test.go b/aws/data_source_aws_cur_report_definition_test.go index 97d1013e93bd..5a07215fd711 100644 --- a/aws/data_source_aws_cur_report_definition_test.go +++ b/aws/data_source_aws_cur_report_definition_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/costandusagereportservice" "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" @@ -18,6 +19,7 @@ func TestAccDataSourceAwsCurReportDefinition_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckCur(t) }, + ErrorCheck: testAccErrorCheck(t, costandusagereportservice.EndpointsID), ProviderFactories: testAccProviderFactories, CheckDestroy: testAccCheckAwsCurReportDefinitionDestroy, Steps: []resource.TestStep{ @@ -48,6 +50,7 @@ func TestAccDataSourceAwsCurReportDefinition_additional(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckCur(t) }, + ErrorCheck: testAccErrorCheck(t, costandusagereportservice.EndpointsID), ProviderFactories: testAccProviderFactories, CheckDestroy: testAccCheckAwsCurReportDefinitionDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_customer_gateway.go b/aws/data_source_aws_customer_gateway.go index bdce6f41e796..c2ce72c954b0 100644 --- a/aws/data_source_aws_customer_gateway.go +++ b/aws/data_source_aws_customer_gateway.go @@ -102,7 +102,7 @@ func dataSourceAwsCustomerGatewayRead(d *schema.ResourceData, meta interface{}) arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, AccountID: meta.(*AWSClient).accountid, Resource: fmt.Sprintf("customer-gateway/%s", d.Id()), diff --git a/aws/data_source_aws_customer_gateway_test.go b/aws/data_source_aws_customer_gateway_test.go index 3acf5c49c119..c2a94ca5f58e 100644 --- a/aws/data_source_aws_customer_gateway_test.go +++ b/aws/data_source_aws_customer_gateway_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -16,9 +17,8 @@ func TestAccAWSCustomerGatewayDataSource_Filter(t *testing.T) { hostOctet := acctest.RandIntRange(1, 254) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCustomerGatewayDestroy, Steps: []resource.TestStep{ @@ -44,9 +44,8 @@ func TestAccAWSCustomerGatewayDataSource_ID(t *testing.T) { hostOctet := acctest.RandIntRange(1, 254) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckCustomerGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_db_cluster_snapshot_test.go b/aws/data_source_aws_db_cluster_snapshot_test.go index f83d4b7632cb..8f62b6e23a62 100644 --- a/aws/data_source_aws_db_cluster_snapshot_test.go +++ b/aws/data_source_aws_db_cluster_snapshot_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/rds" "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" @@ -15,8 +16,9 @@ func TestAccAWSDbClusterSnapshotDataSource_DbClusterSnapshotIdentifier(t *testin resourceName := "aws_db_cluster_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsDbClusterSnapshotDataSourceConfig_DbClusterSnapshotIdentifier(rName), @@ -51,8 +53,9 @@ func TestAccAWSDbClusterSnapshotDataSource_DbClusterIdentifier(t *testing.T) { resourceName := "aws_db_cluster_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsDbClusterSnapshotDataSourceConfig_DbClusterIdentifier(rName), @@ -87,8 +90,9 @@ func TestAccAWSDbClusterSnapshotDataSource_MostRecent(t *testing.T) { resourceName := "aws_db_cluster_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsDbClusterSnapshotDataSourceConfig_MostRecent(rName), diff --git a/aws/data_source_aws_db_event_categories_test.go b/aws/data_source_aws_db_event_categories_test.go index 29139c540945..628c1ce9094f 100644 --- a/aws/data_source_aws_db_event_categories_test.go +++ b/aws/data_source_aws_db_event_categories_test.go @@ -2,130 +2,76 @@ package aws import ( "fmt" - "reflect" - "regexp" - "sort" - "strconv" "testing" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccAWSDbEventCategories_basic(t *testing.T) { + dataSourceName := "data.aws_db_event_categories.test" + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccCheckAwsDbEventCategoriesConfig, - Check: resource.ComposeTestCheckFunc( - testAccAwsDbEventCategoriesAttrCheck("data.aws_db_event_categories.example", - completeEventCategoriesList), + Config: testAccCheckAwsDbEventCategoriesConfig(), + Check: resource.ComposeAggregateTestCheckFunc( + // These checks are not meant to be exhaustive, as regions have different support. + // Instead these are generally to indicate that filtering works as expected. + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "availability"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "backup"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "configuration change"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "creation"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "deletion"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "failover"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "failure"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "low storage"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "maintenance"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "notification"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "recovery"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "restoration"), ), }, }, }) } -func TestAccAWSDbEventCategories_sourceType(t *testing.T) { +func TestAccAWSDbEventCategories_SourceType(t *testing.T) { + dataSourceName := "data.aws_db_event_categories.test" + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccCheckAwsDbEventCategoriesConfig_sourceType, - Check: resource.ComposeTestCheckFunc( - testAccAwsDbEventCategoriesAttrCheck("data.aws_db_event_categories.example", - DbSnapshotEventCategoriesList), + Config: testAccCheckAwsDbEventCategoriesConfigSourceType("db-snapshot"), + Check: resource.ComposeAggregateTestCheckFunc( + // These checks are not meant to be exhaustive, as regions have different support. + // Instead these are generally to indicate that filtering works as expected. + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "creation"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "deletion"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "notification"), + resource.TestCheckTypeSetElemAttr(dataSourceName, "event_categories.*", "restoration"), ), }, }, }) } -func testAccAwsDbEventCategoriesAttrCheck(n string, expected []string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Can't find DB Event Categories: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("DB Event Categories resource ID not set.") - } - - actual, err := testAccCheckAwsDbEventCategoriesBuild(rs.Primary.Attributes) - if err != nil { - return err - } - - sort.Strings(actual) - sort.Strings(expected) - if !reflect.DeepEqual(expected, actual) { - return fmt.Errorf("DB Event Categories not matched: expected %v, got %v", expected, actual) - } - - return nil - } -} - -func testAccCheckAwsDbEventCategoriesBuild(attrs map[string]string) ([]string, error) { - v, ok := attrs["event_categories.#"] - if !ok { - return nil, fmt.Errorf("DB Event Categories list is missing.") - } - - qty, err := strconv.Atoi(v) - if err != nil { - return nil, err - } - if qty < 1 { - return nil, fmt.Errorf("No DB Event Categories found.") - } - - var eventCategories []string - r := regexp.MustCompile("event_categories.[0-9]+") - for k, v := range attrs { - matched := r.MatchString(k) - if matched { - eventCategories = append(eventCategories, v) - } - } - - return eventCategories, nil -} - -var testAccCheckAwsDbEventCategoriesConfig = ` -data "aws_db_event_categories" "example" {} +func testAccCheckAwsDbEventCategoriesConfig() string { + return ` +data "aws_db_event_categories" "test" {} ` - -var completeEventCategoriesList = []string{ - "notification", - "deletion", - "failover", - "maintenance", - "availability", - "read replica", - "failure", - "configuration change", - "recovery", - "low storage", - "backup", - "creation", - "backtrack", - "restoration", } -var testAccCheckAwsDbEventCategoriesConfig_sourceType = ` -data "aws_db_event_categories" "example" { - source_type = "db-snapshot" +func testAccCheckAwsDbEventCategoriesConfigSourceType(sourceType string) string { + return fmt.Sprintf(` +data "aws_db_event_categories" "test" { + source_type = %[1]q } -` - -var DbSnapshotEventCategoriesList = []string{ - "notification", - "deletion", - "creation", - "restoration", +`, sourceType) } diff --git a/aws/data_source_aws_db_instance.go b/aws/data_source_aws_db_instance.go index 9c856bc7dd3c..02491560d817 100644 --- a/aws/data_source_aws_db_instance.go +++ b/aws/data_source_aws_db_instance.go @@ -230,14 +230,14 @@ func dataSourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error return err } - if len(resp.DBInstances) < 1 { + if resp == nil || len(resp.DBInstances) < 1 || resp.DBInstances[0] == nil { return fmt.Errorf("Your query returned no results. Please change your search criteria and try again.") } if len(resp.DBInstances) > 1 { return fmt.Errorf("Your query returned more than one result. Please try a more specific search criteria.") } - dbInstance := *resp.DBInstances[0] + dbInstance := resp.DBInstances[0] d.SetId(d.Get("db_instance_identifier").(string)) diff --git a/aws/data_source_aws_db_instance_test.go b/aws/data_source_aws_db_instance_test.go index d98d5912588f..af024079defa 100644 --- a/aws/data_source_aws_db_instance_test.go +++ b/aws/data_source_aws_db_instance_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ import ( func TestAccAWSDbInstanceDataSource_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDBInstanceDataSourceConfig(rInt), @@ -45,6 +47,7 @@ func TestAccAWSDbInstanceDataSource_ec2Classic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_db_proxy.go b/aws/data_source_aws_db_proxy.go new file mode 100644 index 000000000000..606f4d4f00a5 --- /dev/null +++ b/aws/data_source_aws_db_proxy.go @@ -0,0 +1,113 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" +) + +func dataSourceAwsDbProxy() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsDbProxyRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "auth": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "auth_scheme": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "iam_auth": { + Type: schema.TypeString, + Computed: true, + }, + "secret_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "debug_logging": { + Type: schema.TypeBool, + Computed: true, + }, + "endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "engine_family": { + Type: schema.TypeString, + Computed: true, + }, + "idle_client_timeout": { + Type: schema.TypeInt, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "require_tls": { + Type: schema.TypeBool, + Computed: true, + }, + "role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_security_group_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "vpc_subnet_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsDbProxyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + name := d.Get("name").(string) + dbProxy, err := finder.DBProxyByName(conn, name) + + if err != nil { + return fmt.Errorf("error reading RDS DB Proxy (%s): %w", name, err) + } + + d.SetId(name) + d.Set("arn", dbProxy.DBProxyArn) + d.Set("auth", flattenDbProxyAuths(dbProxy.Auth)) + d.Set("debug_logging", dbProxy.DebugLogging) + d.Set("endpoint", dbProxy.Endpoint) + d.Set("engine_family", dbProxy.EngineFamily) + d.Set("idle_client_timeout", dbProxy.IdleClientTimeout) + d.Set("require_tls", dbProxy.RequireTLS) + d.Set("role_arn", dbProxy.RoleArn) + d.Set("vpc_id", dbProxy.VpcId) + d.Set("vpc_security_group_ids", aws.StringValueSlice(dbProxy.VpcSecurityGroupIds)) + d.Set("vpc_subnet_ids", aws.StringValueSlice(dbProxy.VpcSubnetIds)) + + return nil +} diff --git a/aws/data_source_aws_db_proxy_test.go b/aws/data_source_aws_db_proxy_test.go new file mode 100644 index 000000000000..153a4a9d5d87 --- /dev/null +++ b/aws/data_source_aws_db_proxy_test.go @@ -0,0 +1,164 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSDBProxyDataSource_basic(t *testing.T) { + dataSourceName := "data.aws_db_proxy.test" + resourceName := "aws_db_proxy.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSDBProxyDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "auth", resourceName, "auth"), + resource.TestCheckResourceAttrPair(dataSourceName, "debug_logging", resourceName, "debug_logging"), + resource.TestCheckResourceAttrPair(dataSourceName, "endpoint", resourceName, "endpoint"), + resource.TestCheckResourceAttrPair(dataSourceName, "engine_family", resourceName, "engine_family"), + resource.TestCheckResourceAttrPair(dataSourceName, "idle_client_timeout", resourceName, "idle_client_timeout"), + resource.TestCheckResourceAttrPair(dataSourceName, "require_tls", resourceName, "require_tls"), + resource.TestCheckResourceAttrPair(dataSourceName, "role_arn", resourceName, "role_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", "aws_vpc.test", "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "vpc_security_group_ids", resourceName, "vpc_security_group_ids"), + resource.TestCheckResourceAttrPair(dataSourceName, "vpc_subnet_ids", resourceName, "vpc_subnet_ids"), + ), + }, + }, + }) +} + +func testAccAWSDBProxyDataSourceConfig(rName string) string { + return fmt.Sprintf(` +# Secrets Manager setup + +resource "aws_secretsmanager_secret" "test" { + name = %[1]q + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = "{\"username\":\"db_user\",\"password\":\"db_user_password\"}" +} + +# IAM setup + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = data.aws_iam_policy_document.assume.json +} + +data "aws_iam_policy_document" "assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["rds.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy" "test" { + role = aws_iam_role.test.id + policy = data.aws_iam_policy_document.test.json +} + +data "aws_iam_policy_document" "test" { + statement { + actions = [ + "secretsmanager:GetRandomPassword", + "secretsmanager:CreateSecret", + "secretsmanager:ListSecrets", + ] + resources = ["*"] + } + + statement { + actions = ["secretsmanager:*"] + resources = [aws_secretsmanager_secret.test.arn] + } +} + +# VPC setup + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + count = 2 + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_db_proxy" "test" { + depends_on = [ + aws_secretsmanager_secret_version.test, + aws_iam_role_policy.test + ] + + name = %[1]q + debug_logging = false + engine_family = "MYSQL" + idle_client_timeout = 1800 + require_tls = true + role_arn = aws_iam_role.test.arn + vpc_security_group_ids = [aws_security_group.test.id] + vpc_subnet_ids = aws_subnet.test.*.id + + auth { + auth_scheme = "SECRETS" + description = "test" + iam_auth = "DISABLED" + secret_arn = aws_secretsmanager_secret.test.arn + } + + tags = { + Name = %[1]q + } +} + +data "aws_db_proxy" "test" { + name = aws_db_proxy.test.name +} +`, rName) +} diff --git a/aws/data_source_aws_db_snapshot_test.go b/aws/data_source_aws_db_snapshot_test.go index bd5629f8df2c..1fb153e8224f 100644 --- a/aws/data_source_aws_db_snapshot_test.go +++ b/aws/data_source_aws_db_snapshot_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/rds" "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" @@ -12,8 +13,9 @@ import ( func TestAccAWSDbSnapshotDataSource_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsDbSnapshotDataSourceConfig(rInt), @@ -43,7 +45,7 @@ func testAccCheckAwsDbSnapshotDataSourceConfig(rInt int) string { return fmt.Sprintf(` resource "aws_db_instance" "bar" { allocated_storage = 10 - engine = "MySQL" + engine = "mysql" engine_version = "5.6.35" instance_class = "db.t2.micro" name = "baz" diff --git a/aws/data_source_aws_db_subnet_group_test.go b/aws/data_source_aws_db_subnet_group_test.go index d4c3837d93f3..1186876c4cbc 100644 --- a/aws/data_source_aws_db_subnet_group_test.go +++ b/aws/data_source_aws_db_subnet_group_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -16,6 +17,7 @@ func TestAccAWSDbSubnetGroupDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { @@ -36,6 +38,7 @@ func TestAccAWSDbSubnetGroupDataSource_basic(t *testing.T) { func TestAccAWSDbSubnetGroupDataSource_nonexistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_default_tags.go b/aws/data_source_aws_default_tags.go new file mode 100644 index 000000000000..750359f41edb --- /dev/null +++ b/aws/data_source_aws_default_tags.go @@ -0,0 +1,36 @@ +package aws + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsDefaultTags() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsDefaultTagsRead, + + Schema: map[string]*schema.Schema{ + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsDefaultTagsRead(d *schema.ResourceData, meta interface{}) error { + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + d.SetId(meta.(*AWSClient).partition) + + tags := defaultTagsConfig.GetTags() + + if tags != nil { + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + } else { + d.Set("tags", nil) + } + + return nil +} diff --git a/aws/data_source_aws_default_tags_test.go b/aws/data_source_aws_default_tags_test.go new file mode 100644 index 000000000000..f7d1a0625b35 --- /dev/null +++ b/aws/data_source_aws_default_tags_test.go @@ -0,0 +1,122 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccAWSDefaultTagsDataSource_basic(t *testing.T) { + var providers []*schema.Provider + + dataSourceName := "data.aws_default_tags.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("first", "value"), + testAccAWSDefaultTagsDataSource(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(dataSourceName, "tags.first", "value"), + ), + }, + }, + }) +} + +func TestAccAWSDefaultTagsDataSource_empty(t *testing.T) { + var providers []*schema.Provider + + dataSourceName := "data.aws_default_tags.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags0(), + testAccAWSDefaultTagsDataSource(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccAWSDefaultTagsDataSource_multiple(t *testing.T) { + var providers []*schema.Provider + + dataSourceName := "data.aws_default_tags.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags2("nuera", "hijo", "escalofrios", "calambres"), + testAccAWSDefaultTagsDataSource(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(dataSourceName, "tags.nuera", "hijo"), + resource.TestCheckResourceAttr(dataSourceName, "tags.escalofrios", "calambres"), + ), + }, + }, + }) +} + +func TestAccAWSDefaultTagsDataSource_ignore(t *testing.T) { + var providers []*schema.Provider + + dataSourceName := "data.aws_default_tags.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("Tabac", "Louis Chiron"), + testAccAWSDefaultTagsDataSource(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(dataSourceName, "tags.Tabac", "Louis Chiron"), + ), + }, + { + Config: composeConfig( + testAccProviderConfigDefaultAndIgnoreTagsKeys1("Tabac", "Louis Chiron"), + testAccAWSDefaultTagsDataSource(), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func testAccAWSDefaultTagsDataSource() string { + return `data "aws_default_tags" "test" {}` +} diff --git a/aws/data_source_aws_directory_service_directory.go b/aws/data_source_aws_directory_service_directory.go index 78860fb210d8..8cd92cb946e7 100644 --- a/aws/data_source_aws_directory_service_directory.go +++ b/aws/data_source_aws_directory_service_directory.go @@ -182,12 +182,12 @@ func dataSourceAwsDirectoryServiceDirectoryRead(d *schema.ResourceData, meta int d.Set("enable_sso", dir.SsoEnabled) var securityGroupId *string - if aws.StringValue(dir.Type) == directoryservice.DirectoryTypeAdconnector { + if aws.StringValue(dir.Type) == directoryservice.DirectoryTypeAdconnector && dir.ConnectSettings != nil { securityGroupId = dir.ConnectSettings.SecurityGroupId - } else { + } else if dir.VpcSettings != nil { securityGroupId = dir.VpcSettings.SecurityGroupId } - d.Set("security_group_id", aws.StringValue(securityGroupId)) + d.Set("security_group_id", securityGroupId) tags, err := keyvaluetags.DirectoryserviceListTags(conn, d.Id()) if err != nil { diff --git a/aws/data_source_aws_directory_service_directory_test.go b/aws/data_source_aws_directory_service_directory_test.go index a8c01c953149..326c743eb480 100644 --- a/aws/data_source_aws_directory_service_directory_test.go +++ b/aws/data_source_aws_directory_service_directory_test.go @@ -13,8 +13,9 @@ import ( func TestAccDataSourceAwsDirectoryServiceDirectory_NonExistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, directoryservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsDirectoryServiceDirectoryConfig_NonExistent, @@ -29,16 +30,19 @@ func TestAccDataSourceAwsDirectoryServiceDirectory_SimpleAD(t *testing.T) { resourceName := "aws_directory_service_directory.test-simple-ad" dataSourceName := "data.aws_directory_service_directory.test-simple-ad" + domainName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDirectoryServiceSimpleDirectory(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDirectoryServiceSimpleDirectory(t) }, + ErrorCheck: testAccErrorCheck(t, directoryservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsDirectoryServiceDirectoryConfig_SimpleAD(alias), + Config: testAccDataSourceAwsDirectoryServiceDirectoryConfig_SimpleAD(alias, domainName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "type", directoryservice.DirectoryTypeSimpleAd), resource.TestCheckResourceAttr(dataSourceName, "size", "Small"), - resource.TestCheckResourceAttr(dataSourceName, "name", "tf-testacc-corp.neverland.com"), + resource.TestCheckResourceAttr(dataSourceName, "name", domainName), resource.TestCheckResourceAttr(dataSourceName, "description", "tf-testacc SimpleAD"), resource.TestCheckResourceAttr(dataSourceName, "short_name", "corp"), resource.TestCheckResourceAttr(dataSourceName, "alias", alias), @@ -60,16 +64,19 @@ func TestAccDataSourceAwsDirectoryServiceDirectory_MicrosoftAD(t *testing.T) { resourceName := "aws_directory_service_directory.test-microsoft-ad" dataSourceName := "data.aws_directory_service_directory.test-microsoft-ad" + domainName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, directoryservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsDirectoryServiceDirectoryConfig_MicrosoftAD(alias), + Config: testAccDataSourceAwsDirectoryServiceDirectoryConfig_MicrosoftAD(alias, domainName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "type", directoryservice.DirectoryTypeMicrosoftAd), resource.TestCheckResourceAttr(dataSourceName, "edition", "Standard"), - resource.TestCheckResourceAttr(dataSourceName, "name", "tf-testacc-corp.neverland.com"), + resource.TestCheckResourceAttr(dataSourceName, "name", domainName), resource.TestCheckResourceAttr(dataSourceName, "description", "tf-testacc MicrosoftAD"), resource.TestCheckResourceAttr(dataSourceName, "short_name", "corp"), resource.TestCheckResourceAttr(dataSourceName, "alias", alias), @@ -86,20 +93,23 @@ func TestAccDataSourceAwsDirectoryServiceDirectory_MicrosoftAD(t *testing.T) { }) } -func TestAccDataSourceAWSDirectoryServiceDirectory_connector(t *testing.T) { +func TestAccDataSourceAwsDirectoryServiceDirectory_connector(t *testing.T) { resourceName := "aws_directory_service_directory.connector" dataSourceName := "data.aws_directory_service_directory.test-ad-connector" + domainName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSDirectoryService(t) testAccPreCheckAWSDirectoryServiceSimpleDirectory(t) }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, directoryservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceDirectoryServiceDirectoryConfig_connector(), + Config: testAccDataSourceDirectoryServiceDirectoryConfig_connector(domainName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "connect_settings.0.connect_ips", resourceName, "connect_settings.0.connect_ips"), ), @@ -146,17 +156,17 @@ resource "aws_subnet" "secondary" { `, adType)) } -func testAccDataSourceAwsDirectoryServiceDirectoryConfig_SimpleAD(alias string) string { +func testAccDataSourceAwsDirectoryServiceDirectoryConfig_SimpleAD(alias, domain string) string { return composeConfig(testAccDataSourceAwsDirectoryServiceDirectoryConfig_Prerequisites("simple-ad"), fmt.Sprintf(` resource "aws_directory_service_directory" "test-simple-ad" { type = "SimpleAD" size = "Small" - name = "tf-testacc-corp.neverland.com" + name = %[2]q description = "tf-testacc SimpleAD" short_name = "corp" password = "#S1ncerely" - alias = %q + alias = %[1]q enable_sso = false vpc_settings { @@ -168,20 +178,20 @@ resource "aws_directory_service_directory" "test-simple-ad" { data "aws_directory_service_directory" "test-simple-ad" { directory_id = aws_directory_service_directory.test-simple-ad.id } -`, alias)) +`, alias, domain)) } -func testAccDataSourceAwsDirectoryServiceDirectoryConfig_MicrosoftAD(alias string) string { +func testAccDataSourceAwsDirectoryServiceDirectoryConfig_MicrosoftAD(alias, domain string) string { return composeConfig(testAccDataSourceAwsDirectoryServiceDirectoryConfig_Prerequisites("microsoft-ad"), fmt.Sprintf(` resource "aws_directory_service_directory" "test-microsoft-ad" { type = "MicrosoftAD" edition = "Standard" - name = "tf-testacc-corp.neverland.com" + name = %[2]q description = "tf-testacc MicrosoftAD" short_name = "corp" password = "#S1ncerely" - alias = %q + alias = %[1]q enable_sso = false vpc_settings { @@ -193,14 +203,15 @@ resource "aws_directory_service_directory" "test-microsoft-ad" { data "aws_directory_service_directory" "test-microsoft-ad" { directory_id = aws_directory_service_directory.test-microsoft-ad.id } -`, alias)) +`, alias, domain)) } -func testAccDataSourceDirectoryServiceDirectoryConfig_connector() string { - return composeConfig(testAccAvailableAZsNoOptInConfig(), - ` +func testAccDataSourceDirectoryServiceDirectoryConfig_connector(domain string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` resource "aws_directory_service_directory" "test" { - name = "corp.notexample.com" + name = %[1]q password = "SuperSecretPassw0rd" size = "Small" @@ -211,7 +222,7 @@ resource "aws_directory_service_directory" "test" { } resource "aws_directory_service_directory" "connector" { - name = "corp.notexample.com" + name = %[1]q password = "SuperSecretPassw0rd" size = "Small" type = "ADConnector" @@ -255,5 +266,5 @@ resource "aws_subnet" "test" { data "aws_directory_service_directory" "test-ad-connector" { directory_id = aws_directory_service_directory.connector.id } -`) +`, domain)) } diff --git a/aws/data_source_aws_docdb_engine_version_test.go b/aws/data_source_aws_docdb_engine_version_test.go index a0bdb8111c79..b5e0e9b22ab8 100644 --- a/aws/data_source_aws_docdb_engine_version_test.go +++ b/aws/data_source_aws_docdb_engine_version_test.go @@ -17,6 +17,7 @@ func TestAccAWSDocDBEngineVersionDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSDocDBEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, docdb.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -42,6 +43,7 @@ func TestAccAWSDocDBEngineVersionDataSource_preferred(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSDocDBEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, docdb.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -60,6 +62,7 @@ func TestAccAWSDocDBEngineVersionDataSource_defaultOnly(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSDocDBEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, docdb.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_docdb_orderable_db_instance_test.go b/aws/data_source_aws_docdb_orderable_db_instance_test.go index c581f95083fd..54565acaf77e 100644 --- a/aws/data_source_aws_docdb_orderable_db_instance_test.go +++ b/aws/data_source_aws_docdb_orderable_db_instance_test.go @@ -18,6 +18,7 @@ func TestAccAWSDocdbOrderableDbInstanceDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDocdbOrderableDbInstance(t) }, + ErrorCheck: testAccErrorCheck(t, docdb.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -43,6 +44,7 @@ func TestAccAWSDocdbOrderableDbInstanceDataSource_preferred(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDocdbOrderableDbInstance(t) }, + ErrorCheck: testAccErrorCheck(t, docdb.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_dx_connection.go b/aws/data_source_aws_dx_connection.go new file mode 100644 index 000000000000..28a77edefd95 --- /dev/null +++ b/aws/data_source_aws_dx_connection.go @@ -0,0 +1,110 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsDxConnection() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsDxConnectionRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "aws_device": { + Type: schema.TypeString, + Computed: true, + }, + "bandwidth": { + Type: schema.TypeString, + Computed: true, + }, + "location": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "owner_account_id": { + Type: schema.TypeString, + Computed: true, + }, + "provider_name": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsDxConnectionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + var connections []*directconnect.Connection + input := &directconnect.DescribeConnectionsInput{} + name := d.Get("name").(string) + + // DescribeConnections is not paginated. + output, err := conn.DescribeConnections(input) + + if err != nil { + return fmt.Errorf("error reading Direct Connect Connections: %w", err) + } + + for _, connection := range output.Connections { + if aws.StringValue(connection.ConnectionName) == name { + connections = append(connections, connection) + } + } + + switch count := len(connections); count { + case 0: + return fmt.Errorf("no matching Direct Connect Connection found") + case 1: + default: + return fmt.Errorf("%d Direct Connect Connections matched; use additional constraints to reduce matches to a single Direct Connect Connection", count) + } + + connection := connections[0] + + d.SetId(aws.StringValue(connection.ConnectionId)) + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Region: aws.StringValue(connection.Region), + Service: "directconnect", + AccountID: aws.StringValue(connection.OwnerAccount), + Resource: fmt.Sprintf("dxcon/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("aws_device", connection.AwsDeviceV2) + d.Set("bandwidth", connection.Bandwidth) + d.Set("location", connection.Location) + d.Set("name", connection.ConnectionName) + d.Set("owner_account_id", connection.OwnerAccount) + d.Set("provider_name", connection.ProviderName) + + tags, err := keyvaluetags.DirectconnectListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for Direct Connect Connection (%s): %w", arn, err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_dx_connection_test.go b/aws/data_source_aws_dx_connection_test.go new file mode 100644 index 000000000000..637b58c61b04 --- /dev/null +++ b/aws/data_source_aws_dx_connection_test.go @@ -0,0 +1,53 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsDxConnection_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_dx_connection.test" + datasourceName := "data.aws_dx_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, directconnect.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsDxConnectionConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "aws_device", resourceName, "aws_device"), + resource.TestCheckResourceAttrPair(datasourceName, "bandwidth", resourceName, "bandwidth"), + resource.TestCheckResourceAttrPair(datasourceName, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasourceName, "location", resourceName, "location"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "owner_account_id", resourceName, "owner_account_id"), + resource.TestCheckResourceAttrPair(datasourceName, "provider_name", resourceName, "provider_name"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsDxConnectionConfig(rName string) string { + return fmt.Sprintf(` +data "aws_dx_locations" "test" {} + +resource "aws_dx_connection" "test" { + name = %[1]q + bandwidth = "1Gbps" + location = tolist(data.aws_dx_locations.test.location_codes)[0] +} + +data "aws_dx_connection" "test" { + name = aws_dx_connection.test.name +} +`, rName) +} diff --git a/aws/data_source_aws_dx_gateway.go b/aws/data_source_aws_dx_gateway.go index c05d87d99ddb..71fac3970587 100644 --- a/aws/data_source_aws_dx_gateway.go +++ b/aws/data_source_aws_dx_gateway.go @@ -65,7 +65,7 @@ func dataSourceAwsDxGatewayRead(d *schema.ResourceData, meta interface{}) error d.SetId(aws.StringValue(gateway.DirectConnectGatewayId)) d.Set("amazon_side_asn", strconv.FormatInt(aws.Int64Value(gateway.AmazonSideAsn), 10)) - d.Set("owner_account_id", aws.StringValue(gateway.OwnerAccount)) + d.Set("owner_account_id", gateway.OwnerAccount) return nil } diff --git a/aws/data_source_aws_dx_gateway_test.go b/aws/data_source_aws_dx_gateway_test.go index d197da217e8a..4c57614f03d9 100644 --- a/aws/data_source_aws_dx_gateway_test.go +++ b/aws/data_source_aws_dx_gateway_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/directconnect" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsDxGateway_basic(t *testing.T) { datasourceName := "data.aws_dx_gateway.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, directconnect.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsDxGatewayConfig_NonExistent, diff --git a/aws/data_source_aws_dx_location.go b/aws/data_source_aws_dx_location.go new file mode 100644 index 000000000000..0e144e86e38f --- /dev/null +++ b/aws/data_source_aws_dx_location.go @@ -0,0 +1,63 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/directconnect/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func dataSourceAwsDxLocation() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsDxLocationRead, + + Schema: map[string]*schema.Schema{ + "available_port_speeds": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "available_providers": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "location_code": { + Type: schema.TypeString, + Required: true, + }, + + "location_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsDxLocationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + locationCode := d.Get("location_code").(string) + + location, err := finder.LocationByCode(conn, locationCode) + + if tfresource.NotFound(err) { + return fmt.Errorf("no Direct Connect location matched; change the search criteria and try again") + } + + if err != nil { + return fmt.Errorf("error reading Direct Connect location (%s): %w", locationCode, err) + } + + d.SetId(locationCode) + d.Set("available_port_speeds", aws.StringValueSlice(location.AvailablePortSpeeds)) + d.Set("available_providers", aws.StringValueSlice(location.AvailableProviders)) + d.Set("location_code", location.LocationCode) + d.Set("location_name", location.LocationName) + + return nil +} diff --git a/aws/data_source_aws_dx_location_test.go b/aws/data_source_aws_dx_location_test.go new file mode 100644 index 000000000000..c39d30d3368a --- /dev/null +++ b/aws/data_source_aws_dx_location_test.go @@ -0,0 +1,37 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsDxLocation_basic(t *testing.T) { + dsResourceName := "data.aws_dx_location.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, directconnect.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceDxLocationConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(dsResourceName, "available_port_speeds.#"), + resource.TestCheckResourceAttrSet(dsResourceName, "available_providers.#"), + resource.TestCheckResourceAttrSet(dsResourceName, "location_code"), + resource.TestCheckResourceAttrSet(dsResourceName, "location_name"), + ), + }, + }, + }) +} + +const testAccDataSourceDxLocationConfig_basic = ` +data "aws_dx_locations" "test" {} + +data "aws_dx_location" "test" { + location_code = tolist(data.aws_dx_locations.test.location_codes)[0] +} +` diff --git a/aws/data_source_aws_dx_locations.go b/aws/data_source_aws_dx_locations.go new file mode 100644 index 000000000000..06d1f9f0d2d6 --- /dev/null +++ b/aws/data_source_aws_dx_locations.go @@ -0,0 +1,45 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/directconnect/finder" +) + +func dataSourceAwsDxLocations() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsDxLocationsRead, + + Schema: map[string]*schema.Schema{ + "location_codes": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsDxLocationsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dxconn + + locations, err := finder.Locations(conn, &directconnect.DescribeLocationsInput{}) + + if err != nil { + return fmt.Errorf("error reading Direct Connect locations: %w", err) + } + + var locationCodes []*string + + for _, location := range locations { + locationCodes = append(locationCodes, location.LocationCode) + } + + d.SetId(meta.(*AWSClient).region) + d.Set("location_codes", aws.StringValueSlice(locationCodes)) + + return nil +} diff --git a/aws/data_source_aws_dynamodb_table.go b/aws/data_source_aws_dynamodb_table.go index 60dae0aaac67..004bf179abbb 100644 --- a/aws/data_source_aws_dynamodb_table.go +++ b/aws/data_source_aws_dynamodb_table.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" @@ -167,6 +168,10 @@ func dataSourceAwsDynamoDbTable() *schema.Resource { Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "kms_key_arn": { + Type: schema.TypeString, + Computed: true, + }, "region_name": { Type: schema.TypeString, Computed: true, @@ -216,34 +221,106 @@ func dataSourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) er conn := meta.(*AWSClient).dynamodbconn ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + name := d.Get("name").(string) + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(d.Get("name").(string)), + TableName: aws.String(name), }) if err != nil { - return fmt.Errorf("Error retrieving DynamoDB table: %w", err) + return fmt.Errorf("error reading Dynamodb Table (%s): %w", name, err) } - d.SetId(aws.StringValue(result.Table.TableName)) + if result == nil || result.Table == nil { + return fmt.Errorf("error reading Dynamodb Table (%s): not found", name) + } - err = flattenAwsDynamoDbTableResource(d, result.Table) - if err != nil { - return err + table := result.Table + + d.SetId(aws.StringValue(table.TableName)) + + d.Set("arn", table.TableArn) + d.Set("name", table.TableName) + + if table.BillingModeSummary != nil { + d.Set("billing_mode", table.BillingModeSummary.BillingMode) + } else { + d.Set("billing_mode", dynamodb.BillingModeProvisioned) + } + + if table.ProvisionedThroughput != nil { + d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits) + d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits) + } + + if err := d.Set("attribute", flattenDynamoDbTableAttributeDefinitions(table.AttributeDefinitions)); err != nil { + return fmt.Errorf("error setting attribute: %w", err) + } + + for _, attribute := range table.KeySchema { + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash { + d.Set("hash_key", attribute.AttributeName) + } + + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { + d.Set("range_key", attribute.AttributeName) + } + } + + if err := d.Set("local_secondary_index", flattenDynamoDbTableLocalSecondaryIndex(table.LocalSecondaryIndexes)); err != nil { + return fmt.Errorf("error setting local_secondary_index: %w", err) + } + + if err := d.Set("global_secondary_index", flattenDynamoDbTableGlobalSecondaryIndex(table.GlobalSecondaryIndexes)); err != nil { + return fmt.Errorf("error setting global_secondary_index: %w", err) + } + + if table.StreamSpecification != nil { + d.Set("stream_view_type", table.StreamSpecification.StreamViewType) + d.Set("stream_enabled", table.StreamSpecification.StreamEnabled) + } else { + d.Set("stream_view_type", "") + d.Set("stream_enabled", false) + } + + d.Set("stream_arn", table.LatestStreamArn) + d.Set("stream_label", table.LatestStreamLabel) + + if err := d.Set("server_side_encryption", flattenDynamodDbTableServerSideEncryption(table.SSEDescription)); err != nil { + return fmt.Errorf("error setting server_side_encryption: %w", err) + } + + if err := d.Set("replica", flattenDynamoDbReplicaDescriptions(table.Replicas)); err != nil { + return fmt.Errorf("error setting replica: %w", err) + } + + pitrOut, err := conn.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{ + TableName: aws.String(d.Id()), + }) + + if err != nil && !tfawserr.ErrCodeEquals(err, "UnknownOperationException") { + return fmt.Errorf("error describing DynamoDB Table (%s) Continuous Backups: %w", d.Id(), err) + } + + if err := d.Set("point_in_time_recovery", flattenDynamoDbPitr(pitrOut)); err != nil { + return fmt.Errorf("error setting point_in_time_recovery: %w", err) } ttlOut, err := conn.DescribeTimeToLive(&dynamodb.DescribeTimeToLiveInput{ TableName: aws.String(d.Id()), }) + if err != nil { return fmt.Errorf("error describing DynamoDB Table (%s) Time to Live: %w", d.Id(), err) } + if err := d.Set("ttl", flattenDynamoDbTtl(ttlOut)); err != nil { return fmt.Errorf("error setting ttl: %w", err) } tags, err := keyvaluetags.DynamodbListTags(conn, d.Get("arn").(string)) - if err != nil && !isAWSErr(err, "UnknownOperationException", "Tagging is not currently supported in DynamoDB Local.") { + if err != nil && !tfawserr.ErrMessageContains(err, "UnknownOperationException", "Tagging is not currently supported in DynamoDB Local.") { return fmt.Errorf("error listing tags for DynamoDB Table (%s): %w", d.Get("arn").(string), err) } @@ -251,13 +328,5 @@ func dataSourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error setting tags: %w", err) } - pitrOut, err := conn.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{ - TableName: aws.String(d.Id()), - }) - if err != nil && !isAWSErr(err, "UnknownOperationException", "") { - return err - } - d.Set("point_in_time_recovery", flattenDynamoDbPitr(pitrOut)) - return nil } diff --git a/aws/data_source_aws_dynamodb_table_test.go b/aws/data_source_aws_dynamodb_table_test.go index 03a3d66c4a68..9177af369ae4 100644 --- a/aws/data_source_aws_dynamodb_table_test.go +++ b/aws/data_source_aws_dynamodb_table_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsDynamoDbTable_basic(t *testing.T) { tableName := fmt.Sprintf("testaccawsdynamodbtable-basic-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsDynamoDbTableConfigBasic(tableName), diff --git a/aws/data_source_aws_ebs_default_kms_key_test.go b/aws/data_source_aws_ebs_default_kms_key_test.go index 4342d8c45c35..54313ca826a1 100644 --- a/aws/data_source_aws_ebs_default_kms_key_test.go +++ b/aws/data_source_aws_ebs_default_kms_key_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -11,8 +12,9 @@ import ( func TestAccDataSourceAwsEBSDefaultKmsKey_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEBSDefaultKmsKeyConfig, @@ -44,8 +46,8 @@ func testAccCheckDataSourceAwsEBSDefaultKmsKey(n string) resource.TestCheckFunc attr := rs.Primary.Attributes["key_arn"] - if attr != *actual.KmsKeyId { - return fmt.Errorf("EBS default KMS key is not the expected value (%v)", actual.KmsKeyId) + if attr != aws.StringValue(actual.KmsKeyId) { + return fmt.Errorf("EBS default KMS key is not the expected value (%s)", aws.StringValue(actual.KmsKeyId)) } return nil diff --git a/aws/data_source_aws_ebs_encryption_by_default_test.go b/aws/data_source_aws_ebs_encryption_by_default_test.go index 5404f4912f63..0cbdc5c06cef 100644 --- a/aws/data_source_aws_ebs_encryption_by_default_test.go +++ b/aws/data_source_aws_ebs_encryption_by_default_test.go @@ -5,6 +5,7 @@ import ( "strconv" "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -12,8 +13,9 @@ import ( func TestAccDataSourceAwsEBSEncryptionByDefault_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEBSEncryptionByDefaultConfig, @@ -45,8 +47,8 @@ func testAccCheckDataSourceAwsEBSEncryptionByDefault(n string) resource.TestChec attr, _ := strconv.ParseBool(rs.Primary.Attributes["enabled"]) - if attr != *actual.EbsEncryptionByDefault { - return fmt.Errorf("EBS encryption by default is not in expected state (%t)", *actual.EbsEncryptionByDefault) + if attr != aws.BoolValue(actual.EbsEncryptionByDefault) { + return fmt.Errorf("EBS encryption by default is not in expected state (%t)", aws.BoolValue(actual.EbsEncryptionByDefault)) } return nil diff --git a/aws/data_source_aws_ebs_snapshot.go b/aws/data_source_aws_ebs_snapshot.go index 971864c3e864..2f22155fa511 100644 --- a/aws/data_source_aws_ebs_snapshot.go +++ b/aws/data_source_aws_ebs_snapshot.go @@ -162,7 +162,7 @@ func snapshotDescriptionAttributes(d *schema.ResourceData, snapshot *ec2.Snapsho Partition: meta.(*AWSClient).partition, Region: meta.(*AWSClient).region, Resource: fmt.Sprintf("snapshot/%s", d.Id()), - Service: "ec2", + Service: ec2.ServiceName, }.String() d.Set("arn", snapshotArn) diff --git a/aws/data_source_aws_ebs_snapshot_ids_test.go b/aws/data_source_aws_ebs_snapshot_ids_test.go index 1bca667a4755..f52756247225 100644 --- a/aws/data_source_aws_ebs_snapshot_ids_test.go +++ b/aws/data_source_aws_ebs_snapshot_ids_test.go @@ -4,14 +4,16 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsEbsSnapshotIds_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEbsSnapshotIdsConfig_basic(), @@ -27,8 +29,9 @@ func TestAccDataSourceAwsEbsSnapshotIds_sorted(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEbsSnapshotIdsConfig_sorted1(rName), @@ -56,8 +59,9 @@ func TestAccDataSourceAwsEbsSnapshotIds_sorted(t *testing.T) { func TestAccDataSourceAwsEbsSnapshotIds_empty(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEbsSnapshotIdsConfig_empty, diff --git a/aws/data_source_aws_ebs_snapshot_test.go b/aws/data_source_aws_ebs_snapshot_test.go index b8df0fd13a4e..baaa94f6aa78 100644 --- a/aws/data_source_aws_ebs_snapshot_test.go +++ b/aws/data_source_aws_ebs_snapshot_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -13,8 +14,9 @@ func TestAccAWSEbsSnapshotDataSource_basic(t *testing.T) { resourceName := "aws_ebs_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEbsSnapshotDataSourceConfig, @@ -41,8 +43,9 @@ func TestAccAWSEbsSnapshotDataSource_Filter(t *testing.T) { resourceName := "aws_ebs_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEbsSnapshotDataSourceConfigFilter, @@ -60,8 +63,9 @@ func TestAccAWSEbsSnapshotDataSource_MostRecent(t *testing.T) { resourceName := "aws_ebs_snapshot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEbsSnapshotDataSourceConfigMostRecent, diff --git a/aws/data_source_aws_ebs_volume.go b/aws/data_source_aws_ebs_volume.go index 8e1988f0ee33..496198e3bb02 100644 --- a/aws/data_source_aws_ebs_volume.go +++ b/aws/data_source_aws_ebs_volume.go @@ -122,8 +122,8 @@ type volumeSort []*ec2.Volume func (a volumeSort) Len() int { return len(a) } func (a volumeSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a volumeSort) Less(i, j int) bool { - itime := *a[i].CreateTime - jtime := *a[j].CreateTime + itime := aws.TimeValue(a[i].CreateTime) + jtime := aws.TimeValue(a[j].CreateTime) return itime.Unix() < jtime.Unix() } @@ -140,7 +140,7 @@ func volumeDescriptionAttributes(d *schema.ResourceData, client *AWSClient, volu arn := arn.ARN{ Partition: client.partition, Region: client.region, - Service: "ec2", + Service: ec2.ServiceName, AccountID: client.accountid, Resource: fmt.Sprintf("volume/%s", d.Id()), } diff --git a/aws/data_source_aws_ebs_volume_test.go b/aws/data_source_aws_ebs_volume_test.go index a956929510d6..515e6c2406ab 100644 --- a/aws/data_source_aws_ebs_volume_test.go +++ b/aws/data_source_aws_ebs_volume_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -13,8 +14,9 @@ func TestAccAWSEbsVolumeDataSource_basic(t *testing.T) { dataSourceName := "data.aws_ebs_volume.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEbsVolumeDataSourceConfig, @@ -37,8 +39,9 @@ func TestAccAWSEbsVolumeDataSource_multipleFilters(t *testing.T) { dataSourceName := "data.aws_ebs_volume.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEbsVolumeDataSourceConfigWithMultipleFilters, diff --git a/aws/data_source_aws_ebs_volumes_test.go b/aws/data_source_aws_ebs_volumes_test.go index 9f35936ab9a3..548596efe6a7 100644 --- a/aws/data_source_aws_ebs_volumes_test.go +++ b/aws/data_source_aws_ebs_volumes_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccDataSourceAwsEbsVolumes_basic(t *testing.T) { rInt := acctest.RandIntRange(0, 256) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVolumeDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_coip_pool.go b/aws/data_source_aws_ec2_coip_pool.go index f6d21f9bab4f..93177f1ec369 100644 --- a/aws/data_source_aws_ec2_coip_pool.go +++ b/aws/data_source_aws_ec2_coip_pool.go @@ -34,6 +34,11 @@ func dataSourceAwsEc2CoipPool() *schema.Resource { Computed: true, }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), "filter": ec2CustomFiltersSchema(), @@ -90,6 +95,7 @@ func dataSourceAwsEc2CoipPoolRead(d *schema.ResourceData, meta interface{}) erro d.SetId(aws.StringValue(coip.PoolId)) d.Set("local_gateway_route_table_id", coip.LocalGatewayRouteTableId) + d.Set("arn", coip.PoolArn) if err := d.Set("pool_cidrs", aws.StringValueSlice(coip.PoolCidrs)); err != nil { return fmt.Errorf("error setting pool_cidrs: %w", err) diff --git a/aws/data_source_aws_ec2_coip_pool_test.go b/aws/data_source_aws_ec2_coip_pool_test.go index 9cb93025c89b..bb18392aa5f7 100644 --- a/aws/data_source_aws_ec2_coip_pool_test.go +++ b/aws/data_source_aws_ec2_coip_pool_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ func TestAccDataSourceAwsEc2CoipPool_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_coip_pool.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2CoipPoolDataSourceConfigFilter(), @@ -30,14 +32,16 @@ func TestAccDataSourceAwsEc2CoipPool_Id(t *testing.T) { dataSourceName := "data.aws_ec2_coip_pool.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2CoipPoolDataSourceConfigId(), Check: resource.ComposeTestCheckFunc( resource.TestMatchResourceAttr(dataSourceName, "local_gateway_route_table_id", regexp.MustCompile(`^lgw-rtb-`)), resource.TestMatchResourceAttr(dataSourceName, "pool_id", regexp.MustCompile(`^ipv4pool-coip-`)), + testAccMatchResourceAttrRegionalARN(dataSourceName, "arn", "ec2", regexp.MustCompile(`coip-pool/ipv4pool-coip-.+$`)), testCheckResourceAttrGreaterThanValue(dataSourceName, "pool_cidrs.#", "0"), ), }, diff --git a/aws/data_source_aws_ec2_coip_pools_test.go b/aws/data_source_aws_ec2_coip_pools_test.go index b152a7cb5eb0..e31c70bbf6ef 100644 --- a/aws/data_source_aws_ec2_coip_pools_test.go +++ b/aws/data_source_aws_ec2_coip_pools_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -10,8 +11,9 @@ func TestAccDataSourceAwsEc2CoipPools_basic(t *testing.T) { dataSourceName := "data.aws_ec2_coip_pools.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2CoipPoolsConfig(), @@ -27,8 +29,9 @@ func TestAccDataSourceAwsEc2CoipPools_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_coip_pools.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2CoipPoolsConfigFilter(), diff --git a/aws/data_source_aws_ec2_host.go b/aws/data_source_aws_ec2_host.go new file mode 100644 index 000000000000..e2e1a85e930c --- /dev/null +++ b/aws/data_source_aws_ec2_host.go @@ -0,0 +1,107 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func dataSourceAwsEc2Host() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAwsEc2HostRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "auto_placement": { + Type: schema.TypeString, + Computed: true, + }, + "availability_zone": { + Type: schema.TypeString, + Computed: true, + }, + "cores": { + Type: schema.TypeInt, + Computed: true, + }, + "filter": dataSourceFiltersSchema(), + "host_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "host_recovery": { + Type: schema.TypeString, + Computed: true, + }, + "instance_family": { + Type: schema.TypeString, + Computed: true, + }, + "instance_type": { + Type: schema.TypeString, + Computed: true, + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "sockets": { + Type: schema.TypeInt, + Computed: true, + }, + "tags": tagsSchemaComputed(), + "total_vcpus": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func dataSourceAwsAwsEc2HostRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + host, err := finder.HostByIDAndFilters(conn, d.Get("host_id").(string), buildAwsDataSourceFilters(d.Get("filter").(*schema.Set))) + + if err != nil { + return tfresource.SingularDataSourceFindError("EC2 Host", err) + } + + d.SetId(aws.StringValue(host.HostId)) + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: ec2.ServiceName, + Region: meta.(*AWSClient).region, + AccountID: aws.StringValue(host.OwnerId), + Resource: fmt.Sprintf("dedicated-host/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("auto_placement", host.AutoPlacement) + d.Set("availability_zone", host.AvailabilityZone) + d.Set("cores", host.HostProperties.Cores) + d.Set("host_id", host.HostId) + d.Set("host_recovery", host.HostRecovery) + d.Set("instance_family", host.HostProperties.InstanceFamily) + d.Set("instance_type", host.HostProperties.InstanceType) + d.Set("owner_id", host.OwnerId) + d.Set("sockets", host.HostProperties.Sockets) + d.Set("total_vcpus", host.HostProperties.TotalVCpus) + + if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(host.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_ec2_host_test.go b/aws/data_source_aws_ec2_host_test.go new file mode 100644 index 000000000000..7ae297a0de0d --- /dev/null +++ b/aws/data_source_aws_ec2_host_test.go @@ -0,0 +1,119 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSEc2HostDataSource_basic(t *testing.T) { + dataSourceName := "data.aws_ec2_host.test" + resourceName := "aws_ec2_host.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSEc2HostConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "auto_placement", resourceName, "auto_placement"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone", resourceName, "availability_zone"), + resource.TestCheckResourceAttrSet(dataSourceName, "cores"), + resource.TestCheckResourceAttrPair(dataSourceName, "host_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "host_recovery", resourceName, "host_recovery"), + resource.TestCheckResourceAttrPair(dataSourceName, "instance_family", resourceName, "instance_family"), + resource.TestCheckResourceAttrPair(dataSourceName, "instance_type", resourceName, "instance_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner_id", resourceName, "owner_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "sockets"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrSet(dataSourceName, "total_vcpus"), + ), + }, + }, + }) +} + +func TestAccAWSEc2HostDataSource_Filter(t *testing.T) { + dataSourceName := "data.aws_ec2_host.test" + resourceName := "aws_ec2_host.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSEc2HostConfigFilter(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "auto_placement", resourceName, "auto_placement"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone", resourceName, "availability_zone"), + resource.TestCheckResourceAttrSet(dataSourceName, "cores"), + resource.TestCheckResourceAttrPair(dataSourceName, "host_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "host_recovery", resourceName, "host_recovery"), + resource.TestCheckResourceAttrPair(dataSourceName, "instance_family", resourceName, "instance_family"), + resource.TestCheckResourceAttrPair(dataSourceName, "instance_type", resourceName, "instance_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner_id", resourceName, "owner_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "sockets"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrSet(dataSourceName, "total_vcpus"), + ), + }, + }, + }) +} + +func testAccDataSourceAWSEc2HostConfig(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_ec2_host" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + instance_type = "a1.large" + + tags = { + Name = %[1]q + } +} + +data "aws_ec2_host" "test" { + host_id = aws_ec2_host.test.id +} +`, rName)) +} + +func testAccDataSourceAWSEc2HostConfigFilter(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_ec2_host" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + instance_type = "a1.large" + + tags = { + %[1]q = "True" + } +} + +data "aws_ec2_host" "test" { + filter { + name = "availability-zone" + values = [aws_ec2_host.test.availability_zone] + } + + filter { + name = "instance-type" + values = [aws_ec2_host.test.instance_type] + } + + filter { + name = "tag-key" + values = [%[1]q] + } +} +`, rName)) +} diff --git a/aws/data_source_aws_ec2_instance_type_offering_test.go b/aws/data_source_aws_ec2_instance_type_offering_test.go index 43657eea38e9..3115aada695e 100644 --- a/aws/data_source_aws_ec2_instance_type_offering_test.go +++ b/aws/data_source_aws_ec2_instance_type_offering_test.go @@ -13,6 +13,7 @@ func TestAccAWSEc2InstanceTypeOfferingDataSource_Filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -31,6 +32,7 @@ func TestAccAWSEc2InstanceTypeOfferingDataSource_LocationType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -49,6 +51,7 @@ func TestAccAWSEc2InstanceTypeOfferingDataSource_PreferredInstanceTypes(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOffering(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_instance_type_offerings.go b/aws/data_source_aws_ec2_instance_type_offerings.go index 19143031bb7f..5a17f30fec8f 100644 --- a/aws/data_source_aws_ec2_instance_type_offerings.go +++ b/aws/data_source_aws_ec2_instance_type_offerings.go @@ -16,18 +16,24 @@ func dataSourceAwsEc2InstanceTypeOfferings() *schema.Resource { Schema: map[string]*schema.Schema{ "filter": dataSourceFiltersSchema(), "instance_types": { - Type: schema.TypeSet, + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "locations": { + Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "location_type": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - ec2.LocationTypeAvailabilityZone, - ec2.LocationTypeAvailabilityZoneId, - ec2.LocationTypeRegion, - }, false), + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(ec2.LocationType_Values(), false), + }, + "location_types": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, }, } @@ -47,36 +53,40 @@ func dataSourceAwsEc2InstanceTypeOfferingsRead(d *schema.ResourceData, meta inte } var instanceTypes []string + var locations []string + var locationTypes []string - for { - output, err := conn.DescribeInstanceTypeOfferings(input) - - if err != nil { - return fmt.Errorf("error reading EC2 Instance Type Offerings: %w", err) - } - - if output == nil { - break + err := conn.DescribeInstanceTypeOfferingsPages(input, func(page *ec2.DescribeInstanceTypeOfferingsOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - for _, instanceTypeOffering := range output.InstanceTypeOfferings { + for _, instanceTypeOffering := range page.InstanceTypeOfferings { if instanceTypeOffering == nil { continue } instanceTypes = append(instanceTypes, aws.StringValue(instanceTypeOffering.InstanceType)) + locations = append(locations, aws.StringValue(instanceTypeOffering.Location)) + locationTypes = append(locationTypes, aws.StringValue(instanceTypeOffering.LocationType)) } - if aws.StringValue(output.NextToken) == "" { - break - } + return !lastPage + }) - input.NextToken = output.NextToken + if err != nil { + return fmt.Errorf("error reading EC2 Instance Type Offerings: %w", err) } if err := d.Set("instance_types", instanceTypes); err != nil { return fmt.Errorf("error setting instance_types: %w", err) } + if err := d.Set("locations", locations); err != nil { + return fmt.Errorf("error setting locations: %w", err) + } + if err := d.Set("location_types", locationTypes); err != nil { + return fmt.Errorf("error setting location_types: %w", err) + } d.SetId(meta.(*AWSClient).region) diff --git a/aws/data_source_aws_ec2_instance_type_offerings_test.go b/aws/data_source_aws_ec2_instance_type_offerings_test.go index 73d122360cd5..bbeab9864776 100644 --- a/aws/data_source_aws_ec2_instance_type_offerings_test.go +++ b/aws/data_source_aws_ec2_instance_type_offerings_test.go @@ -15,6 +15,7 @@ func TestAccAWSEc2InstanceTypeOfferingsDataSource_Filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOfferings(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -33,6 +34,7 @@ func TestAccAWSEc2InstanceTypeOfferingsDataSource_LocationType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2InstanceTypeOfferings(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -40,6 +42,7 @@ func TestAccAWSEc2InstanceTypeOfferingsDataSource_LocationType(t *testing.T) { Config: testAccAWSEc2InstanceTypeOfferingsDataSourceConfigLocationType(), Check: resource.ComposeTestCheckFunc( testAccCheckEc2InstanceTypeOfferingsInstanceTypes(dataSourceName), + testAccCheckEc2InstanceTypeOfferingsLocations(dataSourceName), ), }, }, @@ -57,6 +60,29 @@ func testAccCheckEc2InstanceTypeOfferingsInstanceTypes(dataSourceName string) re return fmt.Errorf("expected at least one instance_types result, got none") } + if v := rs.Primary.Attributes["locations.#"]; v == "0" { + return fmt.Errorf("expected at least one locations result, got none") + } + + if v := rs.Primary.Attributes["location_types.#"]; v == "0" { + return fmt.Errorf("expected at least one location_types result, got none") + } + + return nil + } +} + +func testAccCheckEc2InstanceTypeOfferingsLocations(dataSourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("Not found: %s", dataSourceName) + } + + if v := rs.Primary.Attributes["locations.#"]; v == "0" { + return fmt.Errorf("expected at least one locations result, got none") + } + return nil } } diff --git a/aws/data_source_aws_ec2_instance_type_test.go b/aws/data_source_aws_ec2_instance_type_test.go index 0a362929b001..9fcc38f7cb6e 100644 --- a/aws/data_source_aws_ec2_instance_type_test.go +++ b/aws/data_source_aws_ec2_instance_type_test.go @@ -3,48 +3,50 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsEc2InstanceType_basic(t *testing.T) { resourceBasic := "data.aws_ec2_instance_type.basic" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceEc2InstanceTypeBasic, - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceBasic, "auto_recovery_supported", "true"), resource.TestCheckResourceAttr(resourceBasic, "bare_metal", "false"), - resource.TestCheckResourceAttr(resourceBasic, "burstable_performance_supported", "true"), + resource.TestCheckResourceAttr(resourceBasic, "burstable_performance_supported", "false"), resource.TestCheckResourceAttr(resourceBasic, "current_generation", "true"), - resource.TestCheckResourceAttr(resourceBasic, "dedicated_hosts_supported", "false"), + resource.TestCheckResourceAttr(resourceBasic, "dedicated_hosts_supported", "true"), resource.TestCheckResourceAttr(resourceBasic, "default_cores", "1"), - resource.TestCheckResourceAttr(resourceBasic, "default_threads_per_core", "1"), - resource.TestCheckResourceAttr(resourceBasic, "default_vcpus", "1"), + resource.TestCheckResourceAttr(resourceBasic, "default_threads_per_core", "2"), + resource.TestCheckResourceAttr(resourceBasic, "default_vcpus", "2"), resource.TestCheckResourceAttr(resourceBasic, "ebs_encryption_support", "supported"), - resource.TestCheckResourceAttr(resourceBasic, "ebs_nvme_support", "unsupported"), - resource.TestCheckResourceAttr(resourceBasic, "ebs_optimized_support", "unsupported"), + resource.TestCheckResourceAttr(resourceBasic, "ebs_nvme_support", "required"), + resource.TestCheckResourceAttr(resourceBasic, "ebs_optimized_support", "default"), resource.TestCheckResourceAttr(resourceBasic, "efa_supported", "false"), - resource.TestCheckResourceAttr(resourceBasic, "ena_support", "unsupported"), - resource.TestCheckResourceAttr(resourceBasic, "free_tier_eligible", "true"), + resource.TestCheckResourceAttr(resourceBasic, "ena_support", "required"), + resource.TestCheckResourceAttr(resourceBasic, "free_tier_eligible", "false"), resource.TestCheckResourceAttr(resourceBasic, "hibernation_supported", "true"), - resource.TestCheckResourceAttr(resourceBasic, "hypervisor", "xen"), + resource.TestCheckResourceAttr(resourceBasic, "hypervisor", "nitro"), resource.TestCheckResourceAttr(resourceBasic, "instance_storage_supported", "false"), - resource.TestCheckResourceAttr(resourceBasic, "instance_type", "t2.micro"), + resource.TestCheckResourceAttr(resourceBasic, "instance_type", "m5.large"), resource.TestCheckResourceAttr(resourceBasic, "ipv6_supported", "true"), - resource.TestCheckResourceAttr(resourceBasic, "maximum_ipv4_addresses_per_interface", "2"), - resource.TestCheckResourceAttr(resourceBasic, "maximum_ipv6_addresses_per_interface", "2"), - resource.TestCheckResourceAttr(resourceBasic, "maximum_network_interfaces", "2"), - resource.TestCheckResourceAttr(resourceBasic, "memory_size", "1024"), - resource.TestCheckResourceAttr(resourceBasic, "network_performance", "Low to Moderate"), - resource.TestCheckResourceAttr(resourceBasic, "supported_architectures.#", "2"), - resource.TestCheckResourceAttr(resourceBasic, "supported_architectures.0", "i386"), - resource.TestCheckResourceAttr(resourceBasic, "supported_architectures.1", "x86_64"), - resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.#", "2"), - resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.0", "partition"), - resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.1", "spread"), + resource.TestCheckResourceAttr(resourceBasic, "maximum_ipv4_addresses_per_interface", "10"), + resource.TestCheckResourceAttr(resourceBasic, "maximum_ipv6_addresses_per_interface", "10"), + resource.TestCheckResourceAttr(resourceBasic, "maximum_network_interfaces", "3"), + resource.TestCheckResourceAttr(resourceBasic, "memory_size", "8192"), + resource.TestCheckResourceAttr(resourceBasic, "network_performance", "Up to 10 Gigabit"), + resource.TestCheckResourceAttr(resourceBasic, "supported_architectures.#", "1"), + resource.TestCheckResourceAttr(resourceBasic, "supported_architectures.0", "x86_64"), + resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.#", "3"), + resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.0", "cluster"), + resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.1", "partition"), + resource.TestCheckResourceAttr(resourceBasic, "supported_placement_strategies.2", "spread"), resource.TestCheckResourceAttr(resourceBasic, "supported_root_device_types.#", "1"), resource.TestCheckResourceAttr(resourceBasic, "supported_root_device_types.0", "ebs"), resource.TestCheckResourceAttr(resourceBasic, "supported_usages_classes.#", "2"), @@ -52,11 +54,12 @@ func TestAccDataSourceAwsEc2InstanceType_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceBasic, "supported_usages_classes.1", "spot"), resource.TestCheckResourceAttr(resourceBasic, "supported_virtualization_types.#", "1"), resource.TestCheckResourceAttr(resourceBasic, "supported_virtualization_types.0", "hvm"), - resource.TestCheckResourceAttr(resourceBasic, "sustained_clock_speed", "2.5"), - resource.TestCheckResourceAttr(resourceBasic, "valid_cores.#", "1"), + resource.TestCheckResourceAttr(resourceBasic, "sustained_clock_speed", "3.1"), resource.TestCheckResourceAttr(resourceBasic, "valid_cores.#", "1"), - resource.TestCheckResourceAttr(resourceBasic, "valid_threads_per_core.#", "1"), + resource.TestCheckResourceAttr(resourceBasic, "valid_cores.0", "1"), + resource.TestCheckResourceAttr(resourceBasic, "valid_threads_per_core.#", "2"), resource.TestCheckResourceAttr(resourceBasic, "valid_threads_per_core.0", "1"), + resource.TestCheckResourceAttr(resourceBasic, "valid_threads_per_core.1", "2"), ), }, }, @@ -66,8 +69,9 @@ func TestAccDataSourceAwsEc2InstanceType_basic(t *testing.T) { func TestAccDataSourceAwsEc2InstanceType_metal(t *testing.T) { resourceMetal := "data.aws_ec2_instance_type.metal" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceEc2InstanceTypeMetal, @@ -92,8 +96,9 @@ func TestAccDataSourceAwsEc2InstanceType_metal(t *testing.T) { func TestAccDataSourceAwsEc2InstanceType_gpu(t *testing.T) { resourceGpu := "data.aws_ec2_instance_type.gpu" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceEc2InstanceTypeGpu, @@ -112,8 +117,9 @@ func TestAccDataSourceAwsEc2InstanceType_gpu(t *testing.T) { func TestAccDataSourceAwsEc2InstanceType_fpga(t *testing.T) { resourceFpga := "data.aws_ec2_instance_type.fpga" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceEc2InstanceTypeFgpa, @@ -132,7 +138,7 @@ func TestAccDataSourceAwsEc2InstanceType_fpga(t *testing.T) { const testAccDataSourceEc2InstanceTypeBasic = ` data "aws_ec2_instance_type" "basic" { - instance_type = "t2.micro" + instance_type = "m5.large" } ` diff --git a/aws/data_source_aws_ec2_local_gateway_route_table_test.go b/aws/data_source_aws_ec2_local_gateway_route_table_test.go index 77aab4f6f78a..76b3b38b8586 100644 --- a/aws/data_source_aws_ec2_local_gateway_route_table_test.go +++ b/aws/data_source_aws_ec2_local_gateway_route_table_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTable_basic(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_route_table.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTableConfigLocalGatewayRouteTableId(), @@ -31,8 +33,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTable_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_route_table.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTableConfigFilter(), @@ -51,8 +54,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTable_LocalGatewayId(t *testing.T) dataSourceName := "data.aws_ec2_local_gateway_route_table.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTableConfigLocalGatewayId(), @@ -71,8 +75,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTable_OutpostArn(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_route_table.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTableConfigOutpostArn(), diff --git a/aws/data_source_aws_ec2_local_gateway_route_tables_test.go b/aws/data_source_aws_ec2_local_gateway_route_tables_test.go index 92e4f7b801c7..372fd77269ac 100644 --- a/aws/data_source_aws_ec2_local_gateway_route_tables_test.go +++ b/aws/data_source_aws_ec2_local_gateway_route_tables_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -10,8 +11,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTables_basic(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_route_tables.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTablesConfig(), @@ -27,8 +29,9 @@ func TestAccDataSourceAwsEc2LocalGatewayRouteTables_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_route_tables.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayRouteTablesConfigFilter(), diff --git a/aws/data_source_aws_ec2_local_gateway_test.go b/aws/data_source_aws_ec2_local_gateway_test.go index 9d94560f7e7d..1ae8f2d90899 100644 --- a/aws/data_source_aws_ec2_local_gateway_test.go +++ b/aws/data_source_aws_ec2_local_gateway_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ func TestAccDataSourceAwsEc2LocalGateway_basic(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayConfigId(), diff --git a/aws/data_source_aws_ec2_local_gateway_virtual_interface_group_test.go b/aws/data_source_aws_ec2_local_gateway_virtual_interface_group_test.go index 604931b17e43..774f913edc80 100644 --- a/aws/data_source_aws_ec2_local_gateway_virtual_interface_group_test.go +++ b/aws/data_source_aws_ec2_local_gateway_virtual_interface_group_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroup_Filter(t *testing. dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupConfigFilter(), @@ -32,8 +34,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroup_LocalGatewayId(t * dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupConfigLocalGatewayId(), @@ -53,8 +56,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroup_Tags(t *testing.T) dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupConfigTags(rName), diff --git a/aws/data_source_aws_ec2_local_gateway_virtual_interface_groups_test.go b/aws/data_source_aws_ec2_local_gateway_virtual_interface_groups_test.go index 5cd34d09d2f9..35f6249f79fa 100644 --- a/aws/data_source_aws_ec2_local_gateway_virtual_interface_groups_test.go +++ b/aws/data_source_aws_ec2_local_gateway_virtual_interface_groups_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroups_basic(t *testing. dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_groups.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupsConfig(), @@ -30,8 +32,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroups_Filter(t *testing dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_groups.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupsConfigFilter(), @@ -49,8 +52,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroups_Tags(t *testing.T dataSourceName := "data.aws_ec2_local_gateway_virtual_interface_groups.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceGroupsConfigTags(rName), diff --git a/aws/data_source_aws_ec2_local_gateway_virtual_interface_test.go b/aws/data_source_aws_ec2_local_gateway_virtual_interface_test.go index 5c9734330c58..a8ae27809103 100644 --- a/aws/data_source_aws_ec2_local_gateway_virtual_interface_test.go +++ b/aws/data_source_aws_ec2_local_gateway_virtual_interface_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterface_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_virtual_interface.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceConfigFilter(), @@ -36,8 +38,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterface_Id(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_virtual_interface.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceConfigId(), @@ -61,8 +64,9 @@ func TestAccDataSourceAwsEc2LocalGatewayVirtualInterface_Tags(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateway_virtual_interface.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewayVirtualInterfaceConfigTags(rName), diff --git a/aws/data_source_aws_ec2_local_gateways_test.go b/aws/data_source_aws_ec2_local_gateways_test.go index 18a30150d6d6..2bef51d2e0b4 100644 --- a/aws/data_source_aws_ec2_local_gateways_test.go +++ b/aws/data_source_aws_ec2_local_gateways_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -10,8 +11,9 @@ func TestAccDataSourceAwsEc2LocalGateways_basic(t *testing.T) { dataSourceName := "data.aws_ec2_local_gateways.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2LocalGatewaysConfig(), diff --git a/aws/data_source_aws_ec2_managed_prefix_list.go b/aws/data_source_aws_ec2_managed_prefix_list.go index cbee0c9e144b..1b550ec8e74b 100644 --- a/aws/data_source_aws_ec2_managed_prefix_list.go +++ b/aws/data_source_aws_ec2_managed_prefix_list.go @@ -93,7 +93,7 @@ func dataSourceAwsEc2ManagedPrefixListRead(ctx context.Context, d *schema.Resour return diag.Errorf("error describing EC2 Managed Prefix Lists: %s", err) } - if len(out.PrefixLists) < 1 { + if out == nil || len(out.PrefixLists) < 1 || out.PrefixLists[0] == nil { return diag.Errorf("no managed prefix lists matched the given criteria") } @@ -101,7 +101,7 @@ func dataSourceAwsEc2ManagedPrefixListRead(ctx context.Context, d *schema.Resour return diag.Errorf("more than 1 prefix list matched the given criteria") } - pl := *out.PrefixLists[0] + pl := out.PrefixLists[0] d.SetId(aws.StringValue(pl.PrefixListId)) d.Set("name", pl.PrefixListName) @@ -121,7 +121,7 @@ func dataSourceAwsEc2ManagedPrefixListRead(ctx context.Context, d *schema.Resour &ec2.GetManagedPrefixListEntriesInput{ PrefixListId: pl.PrefixListId, }, - func(output *ec2.GetManagedPrefixListEntriesOutput, last bool) bool { + func(output *ec2.GetManagedPrefixListEntriesOutput, lastPage bool) bool { for _, entry := range output.Entries { entries = append(entries, map[string]interface{}{ "cidr": aws.StringValue(entry.Cidr), @@ -129,7 +129,7 @@ func dataSourceAwsEc2ManagedPrefixListRead(ctx context.Context, d *schema.Resour }) } - return true + return !lastPage }, ) diff --git a/aws/data_source_aws_ec2_managed_prefix_list_test.go b/aws/data_source_aws_ec2_managed_prefix_list_test.go index f313a68589fc..412a7f22e86e 100644 --- a/aws/data_source_aws_ec2_managed_prefix_list_test.go +++ b/aws/data_source_aws_ec2_managed_prefix_list_test.go @@ -44,8 +44,9 @@ func TestAccDataSourceAwsEc2ManagedPrefixList_basic(t *testing.T) { prefixListResourceName := "data.aws_prefix_list.s3_by_id" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2ManagedPrefixListConfig_basic, @@ -98,8 +99,9 @@ func TestAccDataSourceAwsEc2ManagedPrefixList_filter(t *testing.T) { resourceById := "data.aws_ec2_managed_prefix_list.s3_by_id" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEc2ManagedPrefixListConfig_filter, @@ -149,8 +151,9 @@ data "aws_ec2_managed_prefix_list" "s3_by_id" { func TestAccDataSourceAwsEc2ManagedPrefixList_matchesTooMany(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsPrefixListConfig_matchesTooMany, diff --git a/aws/data_source_aws_ec2_spot_price_test.go b/aws/data_source_aws_ec2_spot_price_test.go index 9fa7ede1b6cd..c02f5f8c91c7 100644 --- a/aws/data_source_aws_ec2_spot_price_test.go +++ b/aws/data_source_aws_ec2_spot_price_test.go @@ -14,6 +14,7 @@ func TestAccAwsEc2SpotPriceDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAwsEc2SpotPrice(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -33,6 +34,7 @@ func TestAccAwsEc2SpotPriceDataSource_Filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAwsEc2SpotPrice(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway.go b/aws/data_source_aws_ec2_transit_gateway.go index 3a327cd87856..633c981d4d9e 100644 --- a/aws/data_source_aws_ec2_transit_gateway.go +++ b/aws/data_source_aws_ec2_transit_gateway.go @@ -110,7 +110,7 @@ func dataSourceAwsEc2TransitGatewayRead(d *schema.ResourceData, meta interface{} return errors.New("error reading EC2 Transit Gateway: missing options") } - d.Set("amazon_side_asn", aws.Int64Value(transitGateway.Options.AmazonSideAsn)) + d.Set("amazon_side_asn", transitGateway.Options.AmazonSideAsn) d.Set("arn", transitGateway.TransitGatewayArn) d.Set("association_default_route_table_id", transitGateway.Options.AssociationDefaultRouteTableId) d.Set("auto_accept_shared_attachments", transitGateway.Options.AutoAcceptSharedAttachments) diff --git a/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment.go b/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment.go index 30fa4bd4f913..f0423a3c2a26 100644 --- a/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment.go +++ b/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment.go @@ -89,8 +89,8 @@ func dataSourceAwsEc2TransitGatewayDxGatewayAttachmentRead(d *schema.ResourceDat return fmt.Errorf("error setting tags: %w", err) } - d.Set("transit_gateway_id", aws.StringValue(transitGatewayAttachment.TransitGatewayId)) - d.Set("dx_gateway_id", aws.StringValue(transitGatewayAttachment.ResourceId)) + d.Set("transit_gateway_id", transitGatewayAttachment.TransitGatewayId) + d.Set("dx_gateway_id", transitGatewayAttachment.ResourceId) d.SetId(aws.StringValue(transitGatewayAttachment.TransitGatewayAttachmentId)) diff --git a/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment_test.go b/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment_test.go index bf0191e562dc..4c6270267532 100644 --- a/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_dx_gateway_attachment_test.go @@ -4,22 +4,24 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_TransitGatewayIdAndDxGatewayId(t *testing.T) { +func testAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_TransitGatewayIdAndDxGatewayId(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") rBgpAsn := acctest.RandIntRange(64512, 65534) dataSourceName := "data.aws_ec2_transit_gateway_dx_gateway_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" dxGatewayResourceName := "aws_dx_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -35,18 +37,19 @@ func TestAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_TransitGatewayIdAn }) } -func TestAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_filter(t *testing.T) { +func testAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_filter(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") rBgpAsn := acctest.RandIntRange(64512, 65534) dataSourceName := "data.aws_ec2_transit_gateway_dx_gateway_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" dxGatewayResourceName := "aws_dx_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway_peering_attachment_test.go b/aws/data_source_aws_ec2_transit_gateway_peering_attachment_test.go index ab6a6f52a4a0..e407d2fd667e 100644 --- a/aws/data_source_aws_ec2_transit_gateway_peering_attachment_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_peering_attachment_test.go @@ -3,22 +3,25 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "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/helper/schema" ) -func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_sameAccount(t *testing.T) { +func testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_sameAccount(t *testing.T) { var providers []*schema.Provider rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_ec2_transit_gateway_peering_attachment.test" resourceName := "aws_ec2_transit_gateway_peering_attachment.test" - resource.ParallelTest(t, resource.TestCase{ + + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) testAccMultipleRegionPreCheck(t, 2) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -35,19 +38,22 @@ func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_sameAccount(t }, }) } -func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_differentAccount(t *testing.T) { + +func testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_differentAccount(t *testing.T) { var providers []*schema.Provider rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_ec2_transit_gateway_peering_attachment.test" resourceName := "aws_ec2_transit_gateway_peering_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) testAccMultipleRegionPreCheck(t, 2) testAccAlternateAccountPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -63,17 +69,20 @@ func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_differentAcco }, }) } -func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_sameAccount(t *testing.T) { + +func testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_sameAccount(t *testing.T) { var providers []*schema.Provider rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_ec2_transit_gateway_peering_attachment.test" resourceName := "aws_ec2_transit_gateway_peering_attachment.test" - resource.ParallelTest(t, resource.TestCase{ + + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) testAccMultipleRegionPreCheck(t, 2) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -90,19 +99,22 @@ func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_sameAccount(t *te }, }) } -func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_differentAccount(t *testing.T) { + +func testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_differentAccount(t *testing.T) { var providers []*schema.Provider rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_ec2_transit_gateway_peering_attachment.test" resourceName := "aws_ec2_transit_gateway_peering_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) testAccMultipleRegionPreCheck(t, 2) testAccAlternateAccountPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -118,17 +130,20 @@ func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_differentAccount( }, }) } -func TestAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Tags(t *testing.T) { + +func testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Tags(t *testing.T) { var providers []*schema.Provider rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_ec2_transit_gateway_peering_attachment.test" resourceName := "aws_ec2_transit_gateway_peering_attachment.test" - resource.ParallelTest(t, resource.TestCase{ + + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) testAccMultipleRegionPreCheck(t, 2) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway_route_table.go b/aws/data_source_aws_ec2_transit_gateway_route_table.go index 68776d1da5fc..82f988542ac6 100644 --- a/aws/data_source_aws_ec2_transit_gateway_route_table.go +++ b/aws/data_source_aws_ec2_transit_gateway_route_table.go @@ -79,20 +79,20 @@ func dataSourceAwsEc2TransitGatewayRouteTableRead(d *schema.ResourceData, meta i return errors.New("error reading EC2 Transit Gateway Route Table: empty result") } - d.Set("default_association_route_table", aws.BoolValue(transitGatewayRouteTable.DefaultAssociationRouteTable)) - d.Set("default_propagation_route_table", aws.BoolValue(transitGatewayRouteTable.DefaultPropagationRouteTable)) + d.Set("default_association_route_table", transitGatewayRouteTable.DefaultAssociationRouteTable) + d.Set("default_propagation_route_table", transitGatewayRouteTable.DefaultPropagationRouteTable) if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(transitGatewayRouteTable.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } - d.Set("transit_gateway_id", aws.StringValue(transitGatewayRouteTable.TransitGatewayId)) + d.Set("transit_gateway_id", transitGatewayRouteTable.TransitGatewayId) d.SetId(aws.StringValue(transitGatewayRouteTable.TransitGatewayRouteTableId)) arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, AccountID: meta.(*AWSClient).accountid, Resource: fmt.Sprintf("transit-gateway-route-table/%s", d.Id()), diff --git a/aws/data_source_aws_ec2_transit_gateway_route_table_test.go b/aws/data_source_aws_ec2_transit_gateway_route_table_test.go index fa1d266159c1..538d673dc3b2 100644 --- a/aws/data_source_aws_ec2_transit_gateway_route_table_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_route_table_test.go @@ -3,15 +3,17 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSEc2TransitGatewayRouteTableDataSource_Filter(t *testing.T) { +func testAccAWSEc2TransitGatewayRouteTableDataSource_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway_route_table.test" resourceName := "aws_ec2_transit_gateway_route_table.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -29,12 +31,13 @@ func TestAccAWSEc2TransitGatewayRouteTableDataSource_Filter(t *testing.T) { }) } -func TestAccAWSEc2TransitGatewayRouteTableDataSource_ID(t *testing.T) { +func testAccAWSEc2TransitGatewayRouteTableDataSource_ID(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway_route_table.test" resourceName := "aws_ec2_transit_gateway_route_table.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway_route_tables.go b/aws/data_source_aws_ec2_transit_gateway_route_tables.go new file mode 100644 index 000000000000..5be6e306ec9a --- /dev/null +++ b/aws/data_source_aws_ec2_transit_gateway_route_tables.go @@ -0,0 +1,86 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsEc2TransitGatewayRouteTables() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEc2TransitGatewayRouteTablesRead, + + Schema: map[string]*schema.Schema{ + "filter": ec2CustomFiltersSchema(), + + "ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsEc2TransitGatewayRouteTablesRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + input := &ec2.DescribeTransitGatewayRouteTablesInput{} + + input.Filters = append(input.Filters, buildEC2TagFilterList( + keyvaluetags.New(d.Get("tags").(map[string]interface{})).Ec2Tags(), + )...) + + input.Filters = append(input.Filters, buildEC2CustomFilterList( + d.Get("filter").(*schema.Set), + )...) + + if len(input.Filters) == 0 { + // Don't send an empty filters list; the EC2 API won't accept it. + input.Filters = nil + } + + var transitGatewayRouteTables []*ec2.TransitGatewayRouteTable + + err := conn.DescribeTransitGatewayRouteTablesPages(input, func(page *ec2.DescribeTransitGatewayRouteTablesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + transitGatewayRouteTables = append(transitGatewayRouteTables, page.TransitGatewayRouteTables...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error describing EC2 Transit Gateway Route Tables: %w", err) + } + + if len(transitGatewayRouteTables) == 0 { + return fmt.Errorf("no matching EC2 Transit Gateway Route Tables found") + } + + var ids []string + + for _, transitGatewayRouteTable := range transitGatewayRouteTables { + if transitGatewayRouteTable == nil { + continue + } + + ids = append(ids, aws.StringValue(transitGatewayRouteTable.TransitGatewayRouteTableId)) + } + + d.SetId(meta.(*AWSClient).region) + + if err = d.Set("ids", ids); err != nil { + return fmt.Errorf("error setting ids: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_ec2_transit_gateway_route_tables_test.go b/aws/data_source_aws_ec2_transit_gateway_route_tables_test.go new file mode 100644 index 000000000000..04799074dc60 --- /dev/null +++ b/aws/data_source_aws_ec2_transit_gateway_route_tables_test.go @@ -0,0 +1,73 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func testAccDataSourceAwsEc2TransitGatewayRouteTables_basic(t *testing.T) { + dataSourceName := "data.aws_ec2_transit_gateway_route_tables.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsEc2TransitGatewayRouteTablesConfig, + Check: resource.ComposeTestCheckFunc( + testCheckResourceAttrGreaterThanValue(dataSourceName, "ids.#", "0"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsEc2TransitGatewayRouteTables_Filter(t *testing.T) { + dataSourceName := "data.aws_ec2_transit_gateway_route_tables.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsEc2TransitGatewayRouteTablesTransitGatewayFilter, + Check: resource.ComposeTestCheckFunc( + testCheckResourceAttrGreaterThanValue(dataSourceName, "ids.#", "0"), + ), + }, + }, + }) +} + +const testAccDataSourceAwsEc2TransitGatewayRouteTablesConfig = ` +resource "aws_ec2_transit_gateway" "test" {} + +resource "aws_ec2_transit_gateway_route_table" "test" { + transit_gateway_id = aws_ec2_transit_gateway.test.id +} + +data "aws_ec2_transit_gateway_route_tables" "test" { + depends_on = [aws_ec2_transit_gateway_route_table.test] +} +` + +const testAccDataSourceAwsEc2TransitGatewayRouteTablesTransitGatewayFilter = ` +resource "aws_ec2_transit_gateway" "test" {} + +resource "aws_ec2_transit_gateway_route_table" "test" { + transit_gateway_id = aws_ec2_transit_gateway.test.id +} + +data "aws_ec2_transit_gateway_route_tables" "test" { + filter { + name = "transit-gateway-id" + values = [aws_ec2_transit_gateway.test.id] + } + + depends_on = [aws_ec2_transit_gateway_route_table.test] +} +` diff --git a/aws/data_source_aws_ec2_transit_gateway_test.go b/aws/data_source_aws_ec2_transit_gateway_test.go index f681df83cf61..026c05b529cd 100644 --- a/aws/data_source_aws_ec2_transit_gateway_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_test.go @@ -3,15 +3,65 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSEc2TransitGatewayDataSource_Filter(t *testing.T) { +func TestAccAWSEc2TransitGatewayDataSource_serial(t *testing.T) { + testCases := map[string]map[string]func(t *testing.T){ + "DxGatewayAttachment": { + "Filter": testAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_filter, + "TransitGatewayIdAndDxGatewayId": testAccAWSEc2TransitGatewayDxGatewayAttachmentDataSource_TransitGatewayIdAndDxGatewayId, + }, + "Gateway": { + "Filter": testAccAWSEc2TransitGatewayDataSource_Filter, + "ID": testAccAWSEc2TransitGatewayDataSource_ID, + }, + "PeeringAttachment": { + "FilterSameAccount": testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_sameAccount, + "FilterDifferentAccount": testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Filter_differentAccount, + "IDSameAccount": testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_sameAccount, + "IDDifferentAccount": testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_ID_differentAccount, + "Tags": testAccAWSEc2TransitGatewayPeeringAttachmentDataSource_Tags, + }, + "RouteTable": { + "Filter": testAccAWSEc2TransitGatewayRouteTableDataSource_Filter, + "ID": testAccAWSEc2TransitGatewayRouteTableDataSource_ID, + }, + "RouteTables": { + "basic": testAccDataSourceAwsEc2TransitGatewayRouteTables_basic, + "Filter": testAccDataSourceAwsEc2TransitGatewayRouteTables_Filter, + }, + "VpcAttachment": { + "Filter": testAccAWSEc2TransitGatewayVpcAttachmentDataSource_Filter, + "ID": testAccAWSEc2TransitGatewayVpcAttachmentDataSource_ID, + }, + "VpnAttachment": { + "Filter": testAccAWSEc2TransitGatewayVpnAttachmentDataSource_filter, + "TransitGatewayIdAndVpnConnectionId": testAccAWSEc2TransitGatewayVpnAttachmentDataSource_TransitGatewayIdAndVpnConnectionId, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } + }) + } +} + +func testAccAWSEc2TransitGatewayDataSource_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway.test" resourceName := "aws_ec2_transit_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -36,12 +86,13 @@ func TestAccAWSEc2TransitGatewayDataSource_Filter(t *testing.T) { }) } -func TestAccAWSEc2TransitGatewayDataSource_ID(t *testing.T) { +func testAccAWSEc2TransitGatewayDataSource_ID(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway.test" resourceName := "aws_ec2_transit_gateway.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway_vpc_attachment.go b/aws/data_source_aws_ec2_transit_gateway_vpc_attachment.go index d743558d9bbe..90e76c3f1e3b 100644 --- a/aws/data_source_aws_ec2_transit_gateway_vpc_attachment.go +++ b/aws/data_source_aws_ec2_transit_gateway_vpc_attachment.go @@ -107,9 +107,9 @@ func dataSourceAwsEc2TransitGatewayVpcAttachmentRead(d *schema.ResourceData, met return fmt.Errorf("error setting tags: %w", err) } - d.Set("transit_gateway_id", aws.StringValue(transitGatewayVpcAttachment.TransitGatewayId)) - d.Set("vpc_id", aws.StringValue(transitGatewayVpcAttachment.VpcId)) - d.Set("vpc_owner_id", aws.StringValue(transitGatewayVpcAttachment.VpcOwnerId)) + d.Set("transit_gateway_id", transitGatewayVpcAttachment.TransitGatewayId) + d.Set("vpc_id", transitGatewayVpcAttachment.VpcId) + d.Set("vpc_owner_id", transitGatewayVpcAttachment.VpcOwnerId) d.SetId(aws.StringValue(transitGatewayVpcAttachment.TransitGatewayAttachmentId)) diff --git a/aws/data_source_aws_ec2_transit_gateway_vpc_attachment_test.go b/aws/data_source_aws_ec2_transit_gateway_vpc_attachment_test.go index d2a419f7b117..78d3d3bcc004 100644 --- a/aws/data_source_aws_ec2_transit_gateway_vpc_attachment_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_vpc_attachment_test.go @@ -3,15 +3,17 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSEc2TransitGatewayVpcAttachmentDataSource_Filter(t *testing.T) { +func testAccAWSEc2TransitGatewayVpcAttachmentDataSource_Filter(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway_vpc_attachment.test" resourceName := "aws_ec2_transit_gateway_vpc_attachment.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -32,12 +34,13 @@ func TestAccAWSEc2TransitGatewayVpcAttachmentDataSource_Filter(t *testing.T) { }) } -func TestAccAWSEc2TransitGatewayVpcAttachmentDataSource_ID(t *testing.T) { +func testAccAWSEc2TransitGatewayVpcAttachmentDataSource_ID(t *testing.T) { dataSourceName := "data.aws_ec2_transit_gateway_vpc_attachment.test" resourceName := "aws_ec2_transit_gateway_vpc_attachment.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ec2_transit_gateway_vpn_attachment.go b/aws/data_source_aws_ec2_transit_gateway_vpn_attachment.go index d044185a7d04..3f27387f3c97 100644 --- a/aws/data_source_aws_ec2_transit_gateway_vpn_attachment.go +++ b/aws/data_source_aws_ec2_transit_gateway_vpn_attachment.go @@ -89,8 +89,8 @@ func dataSourceAwsEc2TransitGatewayVpnAttachmentRead(d *schema.ResourceData, met return fmt.Errorf("error setting tags: %w", err) } - d.Set("transit_gateway_id", aws.StringValue(transitGatewayAttachment.TransitGatewayId)) - d.Set("vpn_connection_id", aws.StringValue(transitGatewayAttachment.ResourceId)) + d.Set("transit_gateway_id", transitGatewayAttachment.TransitGatewayId) + d.Set("vpn_connection_id", transitGatewayAttachment.ResourceId) d.SetId(aws.StringValue(transitGatewayAttachment.TransitGatewayAttachmentId)) diff --git a/aws/data_source_aws_ec2_transit_gateway_vpn_attachment_test.go b/aws/data_source_aws_ec2_transit_gateway_vpn_attachment_test.go index c8d7a5f92a3c..35f6ed53d997 100644 --- a/aws/data_source_aws_ec2_transit_gateway_vpn_attachment_test.go +++ b/aws/data_source_aws_ec2_transit_gateway_vpn_attachment_test.go @@ -4,21 +4,23 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSEc2TransitGatewayVpnAttachmentDataSource_TransitGatewayIdAndVpnConnectionId(t *testing.T) { +func testAccAWSEc2TransitGatewayVpnAttachmentDataSource_TransitGatewayIdAndVpnConnectionId(t *testing.T) { rBgpAsn := acctest.RandIntRange(64512, 65534) dataSourceName := "data.aws_ec2_transit_gateway_vpn_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" vpnConnectionResourceName := "aws_vpn_connection.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ @@ -34,17 +36,18 @@ func TestAccAWSEc2TransitGatewayVpnAttachmentDataSource_TransitGatewayIdAndVpnCo }) } -func TestAccAWSEc2TransitGatewayVpnAttachmentDataSource_filter(t *testing.T) { +func testAccAWSEc2TransitGatewayVpnAttachmentDataSource_filter(t *testing.T) { rBgpAsn := acctest.RandIntRange(64512, 65534) dataSourceName := "data.aws_ec2_transit_gateway_vpn_attachment.test" transitGatewayResourceName := "aws_ec2_transit_gateway.test" vpnConnectionResourceName := "aws_vpn_connection.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckAWSEc2TransitGateway(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEc2TransitGatewayDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ecr_authorization_token_test.go b/aws/data_source_aws_ecr_authorization_token_test.go index 2994d1a6dba5..40a935196869 100644 --- a/aws/data_source_aws_ecr_authorization_token_test.go +++ b/aws/data_source_aws_ecr_authorization_token_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ecr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSEcrAuthorizationTokenDataSource_basic(t *testing.T) { dataSourceName := "data.aws_ecr_authorization_token.repo" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecr.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcrAuthorizationTokenDataSourceBasicConfig, diff --git a/aws/data_source_aws_ecr_image.go b/aws/data_source_aws_ecr_image.go index 7b7ddcb552f3..b26986a644df 100644 --- a/aws/data_source_aws_ecr_image.go +++ b/aws/data_source_aws_ecr_image.go @@ -98,18 +98,10 @@ func dataSourceAwsEcrImageRead(d *schema.ResourceData, meta interface{}) error { image := imageDetails[0] d.SetId(aws.StringValue(image.ImageDigest)) - if err = d.Set("registry_id", aws.StringValue(image.RegistryId)); err != nil { - return fmt.Errorf("failed to set registry_id: %w", err) - } - if err = d.Set("image_digest", aws.StringValue(image.ImageDigest)); err != nil { - return fmt.Errorf("failed to set image_digest: %w", err) - } - if err = d.Set("image_pushed_at", image.ImagePushedAt.Unix()); err != nil { - return fmt.Errorf("failed to set image_pushed_at: %w", err) - } - if err = d.Set("image_size_in_bytes", aws.Int64Value(image.ImageSizeInBytes)); err != nil { - return fmt.Errorf("failed to set image_size_in_bytes: %w", err) - } + d.Set("registry_id", image.RegistryId) + d.Set("image_digest", image.ImageDigest) + d.Set("image_pushed_at", image.ImagePushedAt.Unix()) + d.Set("image_size_in_bytes", image.ImageSizeInBytes) if err := d.Set("image_tags", aws.StringValueSlice(image.ImageTags)); err != nil { return fmt.Errorf("failed to set image_tags: %w", err) } diff --git a/aws/data_source_aws_ecr_image_test.go b/aws/data_source_aws_ecr_image_test.go index 1d9c1712eb78..e2eb89a6648e 100644 --- a/aws/data_source_aws_ecr_image_test.go +++ b/aws/data_source_aws_ecr_image_test.go @@ -5,6 +5,7 @@ import ( "strconv" "testing" + "github.com/aws/aws-sdk-go/service/ecr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -15,8 +16,9 @@ func TestAccAWSEcrDataSource_ecrImage(t *testing.T) { resourceByDigest := "data.aws_ecr_image.by_digest" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecr.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcrImageDataSourceConfig(registry, repo, tag), diff --git a/aws/data_source_aws_ecr_repository_test.go b/aws/data_source_aws_ecr_repository_test.go index 34114e7f2ade..abf0f80343c1 100644 --- a/aws/data_source_aws_ecr_repository_test.go +++ b/aws/data_source_aws_ecr_repository_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ecr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSEcrRepositoryDataSource_basic(t *testing.T) { dataSourceName := "data.aws_ecr_repository.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecr.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcrRepositoryDataSourceConfig(rName), @@ -40,8 +42,9 @@ func TestAccAWSEcrRepositoryDataSource_encryption(t *testing.T) { dataSourceName := "data.aws_ecr_repository.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecr.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcrRepositoryDataSourceConfig_encryption(rName), @@ -64,8 +67,9 @@ func TestAccAWSEcrRepositoryDataSource_encryption(t *testing.T) { func TestAccAWSEcrRepositoryDataSource_nonExistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecr.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcrRepositoryDataSourceConfig_NonExistent, diff --git a/aws/data_source_aws_ecs_cluster_test.go b/aws/data_source_aws_ecs_cluster_test.go index e65ddd4aab04..c608d36ef048 100644 --- a/aws/data_source_aws_ecs_cluster_test.go +++ b/aws/data_source_aws_ecs_cluster_test.go @@ -4,23 +4,29 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccAWSEcsDataSource_ecsCluster(t *testing.T) { + dataSourceName := "data.aws_ecs_cluster.test" + resourceName := "aws_ecs_cluster.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccCheckAwsEcsClusterDataSourceConfig, + Config: testAccCheckAwsEcsClusterDataSourceConfig(rName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "status", "ACTIVE"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "pending_tasks_count", "0"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "running_tasks_count", "0"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "registered_container_instances_count", "0"), - resource.TestCheckResourceAttrSet("data.aws_ecs_cluster.default", "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttr(dataSourceName, "pending_tasks_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "registered_container_instances_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "running_tasks_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "status", "ACTIVE"), ), }, }, @@ -28,38 +34,46 @@ func TestAccAWSEcsDataSource_ecsCluster(t *testing.T) { } func TestAccAWSEcsDataSource_ecsClusterContainerInsights(t *testing.T) { + dataSourceName := "data.aws_ecs_cluster.test" + resourceName := "aws_ecs_cluster.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccCheckAwsEcsClusterDataSourceConfigContainerInsights, + Config: testAccCheckAwsEcsClusterDataSourceConfigContainerInsights(rName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "status", "ACTIVE"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "pending_tasks_count", "0"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "running_tasks_count", "0"), - resource.TestCheckResourceAttr("data.aws_ecs_cluster.default", "registered_container_instances_count", "0"), - resource.TestCheckResourceAttrSet("data.aws_ecs_cluster.default", "arn"), - resource.TestCheckResourceAttrPair("data.aws_ecs_cluster.default", "setting.#", "aws_ecs_cluster.default", "setting.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttr(dataSourceName, "pending_tasks_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "registered_container_instances_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "running_tasks_count", "0"), + resource.TestCheckResourceAttr(dataSourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttrPair(dataSourceName, "setting.#", resourceName, "setting.#"), ), }, }, }) } -var testAccCheckAwsEcsClusterDataSourceConfig = fmt.Sprintf(` -resource "aws_ecs_cluster" "default" { - name = "default-%d" +func testAccCheckAwsEcsClusterDataSourceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "test" { + name = %[1]q } -data "aws_ecs_cluster" "default" { - cluster_name = aws_ecs_cluster.default.name +data "aws_ecs_cluster" "test" { + cluster_name = aws_ecs_cluster.test.name +} +`, rName) } -`, acctest.RandInt()) -var testAccCheckAwsEcsClusterDataSourceConfigContainerInsights = fmt.Sprintf(` -resource "aws_ecs_cluster" "default" { - name = "default-%d" +func testAccCheckAwsEcsClusterDataSourceConfigContainerInsights(rName string) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "test" { + name = %[1]q setting { name = "containerInsights" @@ -67,7 +81,8 @@ resource "aws_ecs_cluster" "default" { } } -data "aws_ecs_cluster" "default" { - cluster_name = aws_ecs_cluster.default.name +data "aws_ecs_cluster" "test" { + cluster_name = aws_ecs_cluster.test.name +} +`, rName) } -`, acctest.RandInt()) diff --git a/aws/data_source_aws_ecs_container_definition.go b/aws/data_source_aws_ecs_container_definition.go index 7c3b71048b6b..85c15ba7526a 100644 --- a/aws/data_source_aws_ecs_container_definition.go +++ b/aws/data_source_aws_ecs_container_definition.go @@ -72,25 +72,29 @@ func dataSourceAwsEcsContainerDefinitionRead(d *schema.ResourceData, meta interf desc, err := conn.DescribeTaskDefinition(params) if err != nil { - return err + return fmt.Errorf("error reading ECS Task Definition: %w", err) } - taskDefinition := *desc.TaskDefinition + if desc == nil || desc.TaskDefinition == nil { + return fmt.Errorf("error reading ECS Task Definition: empty response") + } + + taskDefinition := desc.TaskDefinition for _, def := range taskDefinition.ContainerDefinitions { if aws.StringValue(def.Name) != d.Get("container_name").(string) { continue } d.SetId(fmt.Sprintf("%s/%s", aws.StringValue(taskDefinition.TaskDefinitionArn), d.Get("container_name").(string))) - d.Set("image", aws.StringValue(def.Image)) + d.Set("image", def.Image) image := aws.StringValue(def.Image) if strings.Contains(image, ":") { d.Set("image_digest", strings.Split(image, ":")[1]) } - d.Set("cpu", aws.Int64Value(def.Cpu)) - d.Set("memory", aws.Int64Value(def.Memory)) - d.Set("memory_reservation", aws.Int64Value(def.MemoryReservation)) - d.Set("disable_networking", aws.BoolValue(def.DisableNetworking)) + d.Set("cpu", def.Cpu) + d.Set("memory", def.Memory) + d.Set("memory_reservation", def.MemoryReservation) + d.Set("disable_networking", def.DisableNetworking) d.Set("docker_labels", aws.StringValueMap(def.DockerLabels)) var environment = map[string]string{} diff --git a/aws/data_source_aws_ecs_container_definition_test.go b/aws/data_source_aws_ecs_container_definition_test.go index d26183b14f64..34d7afc7f346 100644 --- a/aws/data_source_aws_ecs_container_definition_test.go +++ b/aws/data_source_aws_ecs_container_definition_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSEcsDataSource_ecsContainerDefinition(t *testing.T) { tdName := fmt.Sprintf("tf_acc_td_ds_ecs_containter_definition_%s", rString) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEcsContainerDefinitionDataSourceConfig(clusterName, tdName, svcName), diff --git a/aws/data_source_aws_ecs_service_test.go b/aws/data_source_aws_ecs_service_test.go index a7516974ad33..d45038aff9a4 100644 --- a/aws/data_source_aws_ecs_service_test.go +++ b/aws/data_source_aws_ecs_service_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,13 +12,15 @@ import ( func TestAccAWSEcsServiceDataSource_basic(t *testing.T) { dataSourceName := "data.aws_ecs_service.test" resourceName := "aws_ecs_service.test" + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccCheckAwsEcsServiceDataSourceConfig, + Config: testAccCheckAwsEcsServiceDataSourceConfig(rName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "arn"), resource.TestCheckResourceAttrPair(resourceName, "desired_count", dataSourceName, "desired_count"), @@ -31,13 +34,14 @@ func TestAccAWSEcsServiceDataSource_basic(t *testing.T) { }) } -var testAccCheckAwsEcsServiceDataSourceConfig = fmt.Sprintf(` +func testAccCheckAwsEcsServiceDataSourceConfig(rName string) string { + return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { - name = "tf-acc-%d" + name = %[1]q } resource "aws_ecs_task_definition" "test" { - family = "mongodb" + family = %[1]q container_definitions = < 1 { - return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(describeResp.FileSystems)) + var results []*efs.FileSystemDescription + + if len(tagsToMatch) > 0 { + + var fileSystems []*efs.FileSystemDescription + + for _, fileSystem := range describeResp.FileSystems { + + tags := keyvaluetags.EfsKeyValueTags(fileSystem.Tags) + + if !tags.ContainsAll(tagsToMatch) { + continue + } + + fileSystems = append(fileSystems, fileSystem) + } + + results = fileSystems + } else { + results = describeResp.FileSystems + } + + if len(results) > 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) } - fs := describeResp.FileSystems[0] + fs := results[0] d.SetId(aws.StringValue(fs.FileSystemId)) + d.Set("availability_zone_id", fs.AvailabilityZoneId) + d.Set("availability_zone_name", fs.AvailabilityZoneName) d.Set("creation_token", fs.CreationToken) d.Set("performance_mode", fs.PerformanceMode) - - fsARN := arn.ARN{ - AccountID: meta.(*AWSClient).accountid, - Partition: meta.(*AWSClient).partition, - Region: meta.(*AWSClient).region, - Resource: fmt.Sprintf("file-system/%s", aws.StringValue(fs.FileSystemId)), - Service: "elasticfilesystem", - }.String() - - d.Set("arn", fsARN) + d.Set("arn", fs.FileSystemArn) d.Set("file_system_id", fs.FileSystemId) d.Set("encrypted", fs.Encrypted) d.Set("kms_key_id", fs.KmsKeyId) diff --git a/aws/data_source_aws_efs_file_system_test.go b/aws/data_source_aws_efs_file_system_test.go index 767bc310495d..3979ddc5c187 100644 --- a/aws/data_source_aws_efs_file_system_test.go +++ b/aws/data_source_aws_efs_file_system_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/efs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsEfsFileSystem_id(t *testing.T) { resourceName := "aws_efs_file_system.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEfsFileSystemIDConfig, @@ -38,13 +40,44 @@ func TestAccDataSourceAwsEfsFileSystem_id(t *testing.T) { }) } +func TestAccDataSourceAwsEfsFileSystem_tags(t *testing.T) { + dataSourceName := "data.aws_efs_file_system.test" + resourceName := "aws_efs_file_system.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsEfsFileSystemTagsConfig, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsEfsFileSystemCheck(dataSourceName, resourceName), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "performance_mode", resourceName, "performance_mode"), + resource.TestCheckResourceAttrPair(dataSourceName, "creation_token", resourceName, "creation_token"), + resource.TestCheckResourceAttrPair(dataSourceName, "encrypted", resourceName, "encrypted"), + resource.TestCheckResourceAttrPair(dataSourceName, "kms_key_id", resourceName, "kms_key_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags", resourceName, "tags"), + resource.TestCheckResourceAttrPair(dataSourceName, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "provisioned_throughput_in_mibps", resourceName, "provisioned_throughput_in_mibps"), + resource.TestCheckResourceAttrPair(dataSourceName, "throughput_mode", resourceName, "throughput_mode"), + resource.TestCheckResourceAttrPair(dataSourceName, "lifecycle_policy", resourceName, "lifecycle_policy"), + resource.TestMatchResourceAttr(dataSourceName, "size_in_bytes", regexp.MustCompile(`^\d+$`)), + ), + }, + }, + }) +} + func TestAccDataSourceAwsEfsFileSystem_name(t *testing.T) { dataSourceName := "data.aws_efs_file_system.test" resourceName := "aws_efs_file_system.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEfsFileSystemNameConfig, @@ -67,11 +100,33 @@ func TestAccDataSourceAwsEfsFileSystem_name(t *testing.T) { }) } +func TestAccDataSourceAwsEfsFileSystem_availabilityZone(t *testing.T) { + dataSourceName := "data.aws_efs_file_system.test" + resourceName := "aws_efs_file_system.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsEfsFileSystemAvailabilityZoneConfig, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsEfsFileSystemCheck(dataSourceName, resourceName), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_id", resourceName, "availability_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_name", resourceName, "availability_zone_name"), + ), + }, + }, + }) +} + func TestAccDataSourceAwsEfsFileSystem_NonExistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsEfsFileSystemIDConfig_NonExistent, @@ -136,3 +191,43 @@ data "aws_efs_file_system" "test" { file_system_id = aws_efs_file_system.test.id } ` + +const testAccDataSourceAwsEfsFileSystemTagsConfig = ` +resource "aws_efs_file_system" "test" { + tags = { + Name = "default-efs" + Environment = "dev" + } +} + +resource "aws_efs_file_system" "wrong-env" { + tags = { + Environment = "test" + } +} + +resource "aws_efs_file_system" "no-tags" {} + +data "aws_efs_file_system" "test" { + tags = aws_efs_file_system.test.tags +} +` + +const testAccDataSourceAwsEfsFileSystemAvailabilityZoneConfig = ` +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_efs_file_system" "test" { + availability_zone_name = data.aws_availability_zones.available.names[0] +} + +data "aws_efs_file_system" "test" { + file_system_id = aws_efs_file_system.test.id +} +` diff --git a/aws/data_source_aws_efs_mount_target.go b/aws/data_source_aws_efs_mount_target.go index b69c054a176a..c42ed0a74bdb 100644 --- a/aws/data_source_aws_efs_mount_target.go +++ b/aws/data_source_aws_efs_mount_target.go @@ -15,52 +15,58 @@ func dataSourceAwsEfsMountTarget() *schema.Resource { Read: dataSourceAwsEfsMountTargetRead, Schema: map[string]*schema.Schema{ - "mount_target_id": { + "access_point_id": { Type: schema.TypeString, - Required: true, + Optional: true, }, - "file_system_arn": { + "availability_zone_id": { Type: schema.TypeString, Computed: true, }, - "file_system_id": { + "availability_zone_name": { Type: schema.TypeString, Computed: true, }, - "ip_address": { + "dns_name": { Type: schema.TypeString, Computed: true, }, - "security_groups": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, + "file_system_arn": { + Type: schema.TypeString, Computed: true, }, - "subnet_id": { + "file_system_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, - "network_interface_id": { + "ip_address": { Type: schema.TypeString, Computed: true, }, - "dns_name": { + "mount_target_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, "mount_target_dns_name": { Type: schema.TypeString, Computed: true, }, - "availability_zone_name": { + "network_interface_id": { Type: schema.TypeString, Computed: true, }, - "availability_zone_id": { + "owner_id": { Type: schema.TypeString, Computed: true, }, - "owner_id": { + "security_groups": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "subnet_id": { Type: schema.TypeString, Computed: true, }, @@ -71,20 +77,32 @@ func dataSourceAwsEfsMountTarget() *schema.Resource { func dataSourceAwsEfsMountTargetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).efsconn - describeEfsOpts := &efs.DescribeMountTargetsInput{ - MountTargetId: aws.String(d.Get("mount_target_id").(string)), + input := &efs.DescribeMountTargetsInput{} + + if v, ok := d.GetOk("access_point_id"); ok { + input.AccessPointId = aws.String(v.(string)) } - log.Printf("[DEBUG] Reading EFS Mount Target: %s", describeEfsOpts) - resp, err := conn.DescribeMountTargets(describeEfsOpts) + if v, ok := d.GetOk("file_system_id"); ok { + input.FileSystemId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("mount_target_id"); ok { + input.MountTargetId = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Reading EFS Mount Target: %s", input) + output, err := conn.DescribeMountTargets(input) + if err != nil { return fmt.Errorf("Error retrieving EFS Mount Target: %w", err) } - if len(resp.MountTargets) != 1 { - return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(resp.MountTargets)) + + if len(output.MountTargets) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(output.MountTargets)) } - mt := resp.MountTargets[0] + mt := output.MountTargets[0] log.Printf("[DEBUG] Found EFS mount target: %#v", mt) @@ -98,14 +116,17 @@ func dataSourceAwsEfsMountTargetRead(d *schema.ResourceData, meta interface{}) e Service: "elasticfilesystem", }.String() + d.Set("availability_zone_id", mt.AvailabilityZoneId) + d.Set("availability_zone_name", mt.AvailabilityZoneName) + d.Set("dns_name", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.efs", aws.StringValue(mt.FileSystemId)))) d.Set("file_system_arn", fsARN) d.Set("file_system_id", mt.FileSystemId) d.Set("ip_address", mt.IpAddress) - d.Set("subnet_id", mt.SubnetId) + d.Set("mount_target_dns_name", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.%s.efs", aws.StringValue(mt.AvailabilityZoneName), aws.StringValue(mt.FileSystemId)))) + d.Set("mount_target_id", mt.MountTargetId) d.Set("network_interface_id", mt.NetworkInterfaceId) - d.Set("availability_zone_name", mt.AvailabilityZoneName) - d.Set("availability_zone_id", mt.AvailabilityZoneId) d.Set("owner_id", mt.OwnerId) + d.Set("subnet_id", mt.SubnetId) sgResp, err := conn.DescribeMountTargetSecurityGroups(&efs.DescribeMountTargetSecurityGroupsInput{ MountTargetId: aws.String(d.Id()), @@ -118,8 +139,5 @@ func dataSourceAwsEfsMountTargetRead(d *schema.ResourceData, meta interface{}) e return err } - d.Set("dns_name", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.efs", aws.StringValue(mt.FileSystemId)))) - d.Set("mount_target_dns_name", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.%s.efs", aws.StringValue(mt.AvailabilityZoneName), aws.StringValue(mt.FileSystemId)))) - return nil } diff --git a/aws/data_source_aws_efs_mount_target_test.go b/aws/data_source_aws_efs_mount_target_test.go index b0345f9b070a..e554fae2e84a 100644 --- a/aws/data_source_aws_efs_mount_target_test.go +++ b/aws/data_source_aws_efs_mount_target_test.go @@ -4,17 +4,20 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/efs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsEfsMountTarget_basic(t *testing.T) { - rName := acctest.RandString(10) + rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_efs_mount_target.test" resourceName := "aws_efs_mount_target.test" + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsEfsMountTargetConfigByMountTargetId(rName), @@ -36,13 +39,73 @@ func TestAccDataSourceAwsEfsMountTarget_basic(t *testing.T) { }) } -func testAccAwsEfsMountTargetConfigByMountTargetId(ct string) string { - return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` +func TestAccDataSourceAwsEfsMountTarget_byAccessPointID(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_efs_mount_target.test" + resourceName := "aws_efs_mount_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSEFSMountTargetConfigByAccessPointId(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "file_system_arn", resourceName, "file_system_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "file_system_id", resourceName, "file_system_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_address", resourceName, "ip_address"), + resource.TestCheckResourceAttrPair(dataSourceName, "subnet_id", resourceName, "subnet_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "network_interface_id", resourceName, "network_interface_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "mount_target_dns_name", resourceName, "mount_target_dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_name", resourceName, "availability_zone_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_id", resourceName, "availability_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner_id", resourceName, "owner_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "security_groups", resourceName, "security_groups"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsEfsMountTarget_byFileSystemID(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_efs_mount_target.test" + resourceName := "aws_efs_mount_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, efs.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSEFSMountTargetConfigByFileSystemId(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "file_system_arn", resourceName, "file_system_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "file_system_id", resourceName, "file_system_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_address", resourceName, "ip_address"), + resource.TestCheckResourceAttrPair(dataSourceName, "subnet_id", resourceName, "subnet_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "network_interface_id", resourceName, "network_interface_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "mount_target_dns_name", resourceName, "mount_target_dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_name", resourceName, "availability_zone_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_id", resourceName, "availability_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner_id", resourceName, "owner_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "security_groups", resourceName, "security_groups"), + ), + }, + }, + }) +} + +func testAccAwsEfsMountTargetDataSourceBaseConfig(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` resource "aws_efs_file_system" "test" { - creation_token = "%s" + creation_token = %[1]q tags = { - Name = "tf-acc-efs-mount-target-test" + Name = %[1]q } } @@ -55,7 +118,7 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "tf-acc-efs-mount-target-test" + Name = %[1]q } } @@ -65,12 +128,38 @@ resource "aws_subnet" "test" { cidr_block = "10.0.1.0/24" tags = { - Name = "tf-acc-efs-mount-target-test" + Name = %[1]q } } +`, rName)) +} +func testAccAwsEfsMountTargetConfigByMountTargetId(rName string) string { + return composeConfig(testAccAwsEfsMountTargetDataSourceBaseConfig(rName), ` data "aws_efs_mount_target" "test" { mount_target_id = aws_efs_mount_target.test.id } -`, ct) +`) +} + +func testAccAWSEFSMountTargetConfigByAccessPointId(rName string) string { + return composeConfig(testAccAwsEfsMountTargetDataSourceBaseConfig(rName), ` +resource "aws_efs_access_point" "test" { + file_system_id = aws_efs_file_system.test.id +} + +data "aws_efs_mount_target" "test" { + access_point_id = aws_efs_access_point.test.id +} +`) +} + +func testAccAWSEFSMountTargetConfigByFileSystemId(rName string) string { + return composeConfig(testAccAwsEfsMountTargetDataSourceBaseConfig(rName), ` +data "aws_efs_mount_target" "test" { + file_system_id = aws_efs_file_system.test.id + + depends_on = [aws_efs_mount_target.test] +} +`) } diff --git a/aws/data_source_aws_eip.go b/aws/data_source_aws_eip.go index 03dad246c8e4..7ee7ab175561 100644 --- a/aws/data_source_aws_eip.go +++ b/aws/data_source_aws_eip.go @@ -136,16 +136,16 @@ func dataSourceAwsEipRead(d *schema.ResourceData, meta interface{}) error { d.Set("network_interface_id", eip.NetworkInterfaceId) d.Set("network_interface_owner_id", eip.NetworkInterfaceOwnerId) - region := *conn.Config.Region + region := aws.StringValue(conn.Config.Region) d.Set("private_ip", eip.PrivateIpAddress) if eip.PrivateIpAddress != nil { - d.Set("private_dns", fmt.Sprintf("ip-%s.%s", resourceAwsEc2DashIP(*eip.PrivateIpAddress), resourceAwsEc2RegionalPrivateDnsSuffix(region))) + d.Set("private_dns", fmt.Sprintf("ip-%s.%s", resourceAwsEc2DashIP(aws.StringValue(eip.PrivateIpAddress)), resourceAwsEc2RegionalPrivateDnsSuffix(region))) } d.Set("public_ip", eip.PublicIp) if eip.PublicIp != nil { - d.Set("public_dns", meta.(*AWSClient).PartitionHostname(fmt.Sprintf("ec2-%s.%s", resourceAwsEc2DashIP(*eip.PublicIp), resourceAwsEc2RegionalPublicDnsSuffix(region)))) + d.Set("public_dns", meta.(*AWSClient).PartitionHostname(fmt.Sprintf("ec2-%s.%s", resourceAwsEc2DashIP(aws.StringValue(eip.PublicIp)), resourceAwsEc2RegionalPublicDnsSuffix(region)))) } d.Set("public_ipv4_pool", eip.PublicIpv4Pool) d.Set("carrier_ip", eip.CarrierIp) diff --git a/aws/data_source_aws_eip_test.go b/aws/data_source_aws_eip_test.go index 97c35b23b1f3..36c5920513ee 100644 --- a/aws/data_source_aws_eip_test.go +++ b/aws/data_source_aws_eip_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAWSEIP_Filter(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigFilter(rName), @@ -34,8 +36,9 @@ func TestAccDataSourceAWSEIP_Id(t *testing.T) { resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigId, @@ -48,12 +51,14 @@ func TestAccDataSourceAWSEIP_Id(t *testing.T) { }, }) } + func TestAccDataSourceAWSEIP_PublicIP_EC2Classic(t *testing.T) { dataSourceName := "data.aws_eip.test" resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { @@ -73,8 +78,9 @@ func TestAccDataSourceAWSEIP_PublicIP_VPC(t *testing.T) { resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigPublicIpVpc, @@ -95,8 +101,9 @@ func TestAccDataSourceAWSEIP_Tags(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigTags(rName), @@ -115,8 +122,9 @@ func TestAccDataSourceAWSEIP_NetworkInterface(t *testing.T) { resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigNetworkInterface, @@ -137,8 +145,9 @@ func TestAccDataSourceAWSEIP_Instance(t *testing.T) { resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigInstance, @@ -158,8 +167,9 @@ func TestAccDataSourceAWSEIP_CarrierIP(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigCarrierIP(rName), @@ -177,8 +187,9 @@ func TestAccDataSourceAWSEIP_CustomerOwnedIpv4Pool(t *testing.T) { resourceName := "aws_eip.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSEIPConfigCustomerOwnedIpv4Pool(), diff --git a/aws/data_source_aws_eks_addon.go b/aws/data_source_aws_eks_addon.go new file mode 100644 index 000000000000..676ee8c89e57 --- /dev/null +++ b/aws/data_source_aws_eks_addon.go @@ -0,0 +1,82 @@ +package aws + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" +) + +func dataSourceAwsEksAddon() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceAwsEksAddonRead, + Schema: map[string]*schema.Schema{ + "addon_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "addon_version": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateEKSClusterName, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "modified_at": { + Type: schema.TypeString, + Computed: true, + }, + "service_account_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsEksAddonRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + addonName := d.Get("addon_name").(string) + clusterName := d.Get("cluster_name").(string) + id := tfeks.AddonCreateResourceID(clusterName, addonName) + + addon, err := finder.AddonByClusterNameAndAddonName(ctx, conn, clusterName, addonName) + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading EKS Add-On (%s): %w", id, err)) + } + + d.SetId(id) + d.Set("addon_version", addon.AddonVersion) + d.Set("arn", addon.AddonArn) + d.Set("created_at", aws.TimeValue(addon.CreatedAt).Format(time.RFC3339)) + d.Set("modified_at", aws.TimeValue(addon.ModifiedAt).Format(time.RFC3339)) + d.Set("service_account_role_arn", addon.ServiceAccountRoleArn) + + if err := d.Set("tags", keyvaluetags.EksKeyValueTags(addon.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + return nil +} diff --git a/aws/data_source_aws_eks_addon_test.go b/aws/data_source_aws_eks_addon_test.go new file mode 100644 index 000000000000..f1488fa6e0d1 --- /dev/null +++ b/aws/data_source_aws_eks_addon_test.go @@ -0,0 +1,62 @@ +package aws + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSEksAddonDataSource_basic(t *testing.T) { + var addon eks.Addon + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceResourceName := "data.aws_eks_addon.test" + resourceName := "aws_eks_addon.test" + addonName := "vpc-cni" + ctx := context.TODO() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t); testAccPreCheckAWSEksAddon(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAWSEksAddonDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksAddonDataSourceConfig_Basic(rName, addonName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksAddonExists(ctx, dataSourceResourceName, &addon), + testAccMatchResourceAttrRegionalARN(dataSourceResourceName, "arn", "eks", regexp.MustCompile(fmt.Sprintf("addon/%s/%s/.+$", rName, addonName))), + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "addon_version", dataSourceResourceName, "addon_version"), + resource.TestCheckResourceAttrPair(resourceName, "service_account_role_arn", dataSourceResourceName, "service_account_role_arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_at", dataSourceResourceName, "created_at"), + resource.TestCheckResourceAttrPair(resourceName, "modified_at", dataSourceResourceName, "modified_at"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceResourceName, "tags.%"), + ), + }, + }, + }) +} + +func testAccAWSEksAddonDataSourceConfig_Basic(rName, addonName string) string { + return composeConfig(testAccAWSEksAddonConfig_Base(rName), fmt.Sprintf(` +resource "aws_eks_addon" "test" { + addon_name = %[2]q + cluster_name = aws_eks_cluster.test.name +} + +data "aws_eks_addon" "test" { + addon_name = %[2]q + cluster_name = aws_eks_cluster.test.name + + depends_on = [ + aws_eks_addon.test, + aws_eks_cluster.test, + ] +} +`, rName, addonName)) +} diff --git a/aws/data_source_aws_eks_cluster.go b/aws/data_source_aws_eks_cluster.go index db0a5b3e4835..6fc5f3877624 100644 --- a/aws/data_source_aws_eks_cluster.go +++ b/aws/data_source_aws_eks_cluster.go @@ -2,13 +2,11 @@ package aws import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/eks" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" ) func dataSourceAwsEksCluster() *schema.Resource { @@ -81,7 +79,7 @@ func dataSourceAwsEksCluster() *schema.Resource { "name": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.NoZeroValues, + ValidateFunc: validateEKSClusterName, }, "platform_version": { Type: schema.TypeString, @@ -96,6 +94,10 @@ func dataSourceAwsEksCluster() *schema.Resource { Computed: true, }, "tags": tagsSchemaComputed(), + "version": { + Type: schema.TypeString, + Computed: true, + }, "vpc_config": { Type: schema.TypeList, Computed: true, @@ -113,17 +115,17 @@ func dataSourceAwsEksCluster() *schema.Resource { Type: schema.TypeBool, Computed: true, }, - "security_group_ids": { + "public_access_cidrs": { Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "subnet_ids": { + "security_group_ids": { Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "public_access_cidrs": { + "subnet_ids": { Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, @@ -135,10 +137,6 @@ func dataSourceAwsEksCluster() *schema.Resource { }, }, }, - "version": { - Type: schema.TypeString, - Computed: true, - }, }, } } @@ -148,22 +146,12 @@ func dataSourceAwsEksClusterRead(d *schema.ResourceData, meta interface{}) error ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig name := d.Get("name").(string) + cluster, err := finder.ClusterByName(conn, name) - input := &eks.DescribeClusterInput{ - Name: aws.String(name), - } - - log.Printf("[DEBUG] Reading EKS Cluster: %s", input) - output, err := conn.DescribeCluster(input) if err != nil { return fmt.Errorf("error reading EKS Cluster (%s): %w", name, err) } - cluster := output.Cluster - if cluster == nil { - return fmt.Errorf("EKS Cluster (%s) not found", name) - } - d.SetId(name) d.Set("arn", cluster.Arn) @@ -172,32 +160,34 @@ func dataSourceAwsEksClusterRead(d *schema.ResourceData, meta interface{}) error } d.Set("created_at", aws.TimeValue(cluster.CreatedAt).String()) + if err := d.Set("enabled_cluster_log_types", flattenEksEnabledLogTypes(cluster.Logging)); err != nil { return fmt.Errorf("error setting enabled_cluster_log_types: %w", err) } + d.Set("endpoint", cluster.Endpoint) if err := d.Set("identity", flattenEksIdentity(cluster.Identity)); err != nil { return fmt.Errorf("error setting identity: %w", err) } + if err := d.Set("kubernetes_network_config", flattenEksNetworkConfig(cluster.KubernetesNetworkConfig)); err != nil { + return fmt.Errorf("error setting kubernetes_network_config: %w", err) + } + d.Set("name", cluster.Name) d.Set("platform_version", cluster.PlatformVersion) d.Set("role_arn", cluster.RoleArn) d.Set("status", cluster.Status) - if err := d.Set("tags", keyvaluetags.EksKeyValueTags(cluster.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - d.Set("version", cluster.Version) if err := d.Set("vpc_config", flattenEksVpcConfigResponse(cluster.ResourcesVpcConfig)); err != nil { return fmt.Errorf("error setting vpc_config: %w", err) } - if err := d.Set("kubernetes_network_config", flattenEksNetworkConfig(cluster.KubernetesNetworkConfig)); err != nil { - return fmt.Errorf("error setting kubernetes_network_config: %w", err) + if err := d.Set("tags", keyvaluetags.EksKeyValueTags(cluster.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } return nil diff --git a/aws/data_source_aws_eks_cluster_auth_test.go b/aws/data_source_aws_eks_cluster_auth_test.go index f0222c5485a5..8f0fa6c97bc9 100644 --- a/aws/data_source_aws_eks_cluster_auth_test.go +++ b/aws/data_source_aws_eks_cluster_auth_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/eks" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/token" @@ -13,8 +14,9 @@ func TestAccAWSEksClusterAuthDataSource_basic(t *testing.T) { dataSourceResourceName := "data.aws_eks_cluster_auth.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsEksClusterAuthConfig_basic, diff --git a/aws/data_source_aws_eks_cluster_test.go b/aws/data_source_aws_eks_cluster_test.go index c940b7b89f3a..94a0e56dad39 100644 --- a/aws/data_source_aws_eks_cluster_test.go +++ b/aws/data_source_aws_eks_cluster_test.go @@ -1,21 +1,22 @@ package aws import ( - "fmt" "regexp" "testing" + "github.com/aws/aws-sdk-go/service/eks" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccAWSEksClusterDataSource_basic(t *testing.T) { - rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(5)) + rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceResourceName := "data.aws_eks_cluster.test" resourceName := "aws_eks_cluster.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEksClusterDestroy, Steps: []resource.TestStep{ @@ -55,11 +56,9 @@ func TestAccAWSEksClusterDataSource_basic(t *testing.T) { } func testAccAWSEksClusterDataSourceConfig_Basic(rName string) string { - return fmt.Sprintf(` -%[1]s - + return composeConfig(testAccAWSEksClusterConfig_Logging(rName, []string{"api", "audit"}), ` data "aws_eks_cluster" "test" { name = aws_eks_cluster.test.name } -`, testAccAWSEksClusterConfig_Logging(rName, []string{"api", "audit"})) +`) } diff --git a/aws/data_source_aws_eks_clusters.go b/aws/data_source_aws_eks_clusters.go new file mode 100644 index 000000000000..b16993f09697 --- /dev/null +++ b/aws/data_source_aws_eks_clusters.go @@ -0,0 +1,49 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsEksClusters() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEksClustersRead, + + Schema: map[string]*schema.Schema{ + "names": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsEksClustersRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).eksconn + + var clusters []*string + + err := conn.ListClustersPages(&eks.ListClustersInput{}, func(page *eks.ListClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + clusters = append(clusters, page.Clusters...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing EKS Clusters: %w", err) + } + + d.SetId(meta.(*AWSClient).region) + + d.Set("names", aws.StringValueSlice(clusters)) + + return nil +} diff --git a/aws/data_source_aws_eks_clusters_test.go b/aws/data_source_aws_eks_clusters_test.go new file mode 100644 index 000000000000..7272436c7682 --- /dev/null +++ b/aws/data_source_aws_eks_clusters_test.go @@ -0,0 +1,38 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSEksClustersDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceResourceName := "data.aws_eks_clusters.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEksClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksClustersDataSourceConfig_Basic(rName), + Check: resource.ComposeTestCheckFunc( + testCheckResourceAttrGreaterThanValue(dataSourceResourceName, "names.#", "0"), + ), + }, + }, + }) +} + +func testAccAWSEksClustersDataSourceConfig_Basic(rName string) string { + return composeConfig( + testAccAWSEksClusterConfig_Required(rName), ` +data "aws_eks_clusters" "test" { + depends_on = [aws_eks_cluster.test] +} +`) +} diff --git a/aws/data_source_aws_eks_node_group.go b/aws/data_source_aws_eks_node_group.go new file mode 100644 index 000000000000..07ba7fa7e5cf --- /dev/null +++ b/aws/data_source_aws_eks_node_group.go @@ -0,0 +1,193 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" +) + +func dataSourceAwsEksNodeGroup() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsEksNodeGroupRead, + + Schema: map[string]*schema.Schema{ + "ami_type": { + Type: schema.TypeString, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "disk_size": { + Type: schema.TypeInt, + Computed: true, + }, + "instance_types": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "labels": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "node_group_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "node_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "release_version": { + Type: schema.TypeString, + Computed: true, + }, + "remote_access": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ec2_ssh_key": { + Type: schema.TypeString, + Computed: true, + }, + "source_security_group_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "resources": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "autoscaling_groups": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "remote_access_security_group_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "scaling_config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "desired_size": { + Type: schema.TypeInt, + Computed: true, + }, + "max_size": { + Type: schema.TypeInt, + Computed: true, + }, + "min_size": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "tags": tagsSchemaComputed(), + "version": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsEksNodeGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).eksconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + clusterName := d.Get("cluster_name").(string) + nodeGroupName := d.Get("node_group_name").(string) + id := tfeks.NodeGroupCreateResourceID(clusterName, nodeGroupName) + nodeGroup, err := finder.NodegroupByClusterNameAndNodegroupName(conn, clusterName, nodeGroupName) + + if err != nil { + return diag.Errorf("error reading EKS Node Group (%s): %s", id, err) + } + + d.SetId(id) + + d.Set("ami_type", nodeGroup.AmiType) + d.Set("arn", nodeGroup.NodegroupArn) + d.Set("cluster_name", nodeGroup.ClusterName) + d.Set("disk_size", nodeGroup.DiskSize) + d.Set("instance_types", nodeGroup.InstanceTypes) + d.Set("labels", nodeGroup.Labels) + d.Set("node_group_name", nodeGroup.NodegroupName) + d.Set("node_role_arn", nodeGroup.NodeRole) + d.Set("release_version", nodeGroup.ReleaseVersion) + + if err := d.Set("remote_access", flattenEksRemoteAccessConfig(nodeGroup.RemoteAccess)); err != nil { + return diag.Errorf("error setting remote_access: %s", err) + } + + if err := d.Set("resources", flattenEksNodeGroupResources(nodeGroup.Resources)); err != nil { + return diag.Errorf("error setting resources: %s", err) + } + + if nodeGroup.ScalingConfig != nil { + if err := d.Set("scaling_config", []interface{}{flattenEksNodeGroupScalingConfig(nodeGroup.ScalingConfig)}); err != nil { + return diag.Errorf("error setting scaling_config: %s", err) + } + } else { + d.Set("scaling_config", nil) + } + + d.Set("status", nodeGroup.Status) + + if err := d.Set("subnet_ids", aws.StringValueSlice(nodeGroup.Subnets)); err != nil { + return diag.Errorf("error setting subnets: %s", err) + } + + if err := d.Set("tags", keyvaluetags.EksKeyValueTags(nodeGroup.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + + d.Set("version", nodeGroup.Version) + + return nil +} diff --git a/aws/data_source_aws_eks_node_group_test.go b/aws/data_source_aws_eks_node_group_test.go new file mode 100644 index 000000000000..e3bc1201b7a2 --- /dev/null +++ b/aws/data_source_aws_eks_node_group_test.go @@ -0,0 +1,65 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSEksNodegroupDataSource_basic(t *testing.T) { + var nodeGroup eks.Nodegroup + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceResourceName := "data.aws_eks_node_group.test" + resourceName := "aws_eks_node_group.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEksClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksNodeGroupConfigNodeGroupName(rName), + Check: resource.ComposeTestCheckFunc(), + }, + { + Config: testAccAWSEksNodeGroupDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup), + resource.TestCheckResourceAttrPair(resourceName, "ami_type", dataSourceResourceName, "ami_type"), + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "cluster_name", dataSourceResourceName, "cluster_name"), + resource.TestCheckResourceAttrPair(resourceName, "disk_size", dataSourceResourceName, "disk_size"), + resource.TestCheckResourceAttr(dataSourceResourceName, "instance_types.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "instance_type", dataSourceResourceName, "instance_type"), + resource.TestCheckResourceAttrPair(resourceName, "labels.%", dataSourceResourceName, "labels.%"), + resource.TestCheckResourceAttrPair(resourceName, "node_group_name", dataSourceResourceName, "node_group_name"), + resource.TestCheckResourceAttrPair(resourceName, "node_role_arn", dataSourceResourceName, "node_role_arn"), + resource.TestCheckResourceAttrPair(resourceName, "release_version", dataSourceResourceName, "release_version"), + resource.TestCheckResourceAttr(dataSourceResourceName, "remote_access.#", "0"), + resource.TestCheckResourceAttr(dataSourceResourceName, "resources.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "resources", dataSourceResourceName, "resources"), + resource.TestCheckResourceAttr(dataSourceResourceName, "scaling_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "scaling_config", dataSourceResourceName, "scaling_config"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceResourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "subnet_ids.#", dataSourceResourceName, "subnet_ids.#"), + resource.TestCheckResourceAttrPair(resourceName, "subnet_ids", dataSourceResourceName, "subnet_ids"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceResourceName, "tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "version", dataSourceResourceName, "version"), + ), + }, + }, + }) +} + +func testAccAWSEksNodeGroupDataSourceConfig(rName string) string { + return composeConfig(testAccAWSEksNodeGroupConfigNodeGroupName(rName), fmt.Sprintf(` +data "aws_eks_node_group" "test" { + cluster_name = aws_eks_cluster.test.name + node_group_name = %[1]q +} +`, rName)) +} diff --git a/aws/data_source_aws_eks_node_groups.go b/aws/data_source_aws_eks_node_groups.go new file mode 100644 index 000000000000..ae54f95d52f0 --- /dev/null +++ b/aws/data_source_aws_eks_node_groups.go @@ -0,0 +1,62 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceAwsEksNodeGroups() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEksNodeGroupsRead, + + Schema: map[string]*schema.Schema{ + "cluster_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "names": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsEksNodeGroupsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).eksconn + + clusterName := d.Get("cluster_name").(string) + + input := &eks.ListNodegroupsInput{ + ClusterName: aws.String(clusterName), + } + + var nodegroups []*string + + err := conn.ListNodegroupsPages(input, func(page *eks.ListNodegroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + nodegroups = append(nodegroups, page.Nodegroups...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing EKS Node Groups: %w", err) + } + + d.SetId(clusterName) + + d.Set("cluster_name", clusterName) + d.Set("names", aws.StringValueSlice(nodegroups)) + + return nil +} diff --git a/aws/data_source_aws_eks_node_groups_test.go b/aws/data_source_aws_eks_node_groups_test.go new file mode 100644 index 000000000000..303fdec61b3c --- /dev/null +++ b/aws/data_source_aws_eks_node_groups_test.go @@ -0,0 +1,87 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSEksNodegroupsDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceResourceName := "data.aws_eks_node_groups.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEksClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksNodeGroupNamesConfig(rName), + Check: resource.ComposeTestCheckFunc(), + }, + { + Config: testAccAWSEksNodeGroupNamesDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceResourceName, "cluster_name", rName), + resource.TestCheckResourceAttr(dataSourceResourceName, "names.#", "2"), + ), + }, + }, + }) +} + +func testAccAWSEksNodeGroupNamesDataSourceConfig(rName string) string { + return composeConfig(testAccAWSEksNodeGroupNamesConfig(rName), ` +data "aws_eks_node_groups" "test" { + cluster_name = aws_eks_cluster.test.name + + depends_on = [aws_eks_node_group.test_a, aws_eks_node_group.test_b] +} +`) +} + +func testAccAWSEksNodeGroupNamesConfig(rName string) string { + return composeConfig(testAccAWSEksNodeGroupConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_node_group" "test_a" { + cluster_name = aws_eks_cluster.test.name + node_group_name = "%[1]s-test-a" + node_role_arn = aws_iam_role.node.arn + subnet_ids = aws_subnet.test[*].id + + scaling_config { + desired_size = 1 + max_size = 1 + min_size = 1 + } + + depends_on = [ + "aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy", + "aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy", + "aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly", + ] +} + +resource "aws_eks_node_group" "test_b" { + cluster_name = aws_eks_cluster.test.name + node_group_name = "%[1]s-test-b" + node_role_arn = aws_iam_role.node.arn + subnet_ids = aws_subnet.test[*].id + + scaling_config { + desired_size = 1 + max_size = 1 + min_size = 1 + } + + depends_on = [ + "aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy", + "aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy", + "aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly", + ] +} +`, rName)) +} diff --git a/aws/data_source_aws_elastic_beanstalk_application_test.go b/aws/data_source_aws_elastic_beanstalk_application_test.go index bd1c689cce23..a4d19ccdd5d2 100644 --- a/aws/data_source_aws_elastic_beanstalk_application_test.go +++ b/aws/data_source_aws_elastic_beanstalk_application_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,6 +16,7 @@ func TestAccAwsElasticBeanstalkApplicationDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticbeanstalk.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSEksClusterDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_elastic_beanstalk_hosted_zone.go b/aws/data_source_aws_elastic_beanstalk_hosted_zone.go index 627ce4fc33a4..f9a2f7026e1b 100644 --- a/aws/data_source_aws_elastic_beanstalk_hosted_zone.go +++ b/aws/data_source_aws_elastic_beanstalk_hosted_zone.go @@ -15,7 +15,7 @@ var elasticBeanstalkHostedZoneIds = map[string]string{ endpoints.ApEast1RegionID: "ZPWYUBWRU171A", endpoints.ApNortheast1RegionID: "Z1R25G3KIG2GBW", endpoints.ApNortheast2RegionID: "Z3JE5OI70TWKCP", - "ap-northeast-3": "ZNE5GEY1TIAGY", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "ZNE5GEY1TIAGY", endpoints.ApSouth1RegionID: "Z18NTBI3Y7N9TZ", endpoints.CaCentral1RegionID: "ZJFCZL7SSZB5I", endpoints.EuCentral1RegionID: "Z1FRNW7UH4DEZJ", diff --git a/aws/data_source_aws_elastic_beanstalk_hosted_zone_test.go b/aws/data_source_aws_elastic_beanstalk_hosted_zone_test.go index c1c9b1d3c7f4..f19d07be0edc 100644 --- a/aws/data_source_aws_elastic_beanstalk_hosted_zone_test.go +++ b/aws/data_source_aws_elastic_beanstalk_hosted_zone_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -13,8 +14,9 @@ func TestAccAWSDataSourceElasticBeanstalkHostedZone_basic(t *testing.T) { dataSourceName := "data.aws_elastic_beanstalk_hosted_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticbeanstalk.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElasticBeanstalkHostedZoneDataSource_currentRegion, @@ -30,8 +32,9 @@ func TestAccAWSDataSourceElasticBeanstalkHostedZone_Region(t *testing.T) { dataSourceName := "data.aws_elastic_beanstalk_hosted_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticbeanstalk.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElasticBeanstalkHostedZoneDataSource_byRegion("ap-southeast-2"), //lintignore:AWSAT003 // passes in GovCloud diff --git a/aws/data_source_aws_elastic_beanstalk_solution_stack_test.go b/aws/data_source_aws_elastic_beanstalk_solution_stack_test.go index e3a2419a5ac7..e2f8a4ca150a 100644 --- a/aws/data_source_aws_elastic_beanstalk_solution_stack_test.go +++ b/aws/data_source_aws_elastic_beanstalk_solution_stack_test.go @@ -5,14 +5,16 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccAWSElasticBeanstalkSolutionStackDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticbeanstalk.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElasticBeanstalkSolutionStackDataSourceConfig, diff --git a/aws/data_source_aws_elasticache_cluster.go b/aws/data_source_aws_elasticache_cluster.go index 0ff5810fb174..1b9fddab184b 100644 --- a/aws/data_source_aws_elasticache_cluster.go +++ b/aws/data_source_aws_elasticache_cluster.go @@ -187,7 +187,7 @@ func dataSourceAwsElastiCacheClusterRead(d *schema.ResourceData, meta interface{ d.Set("availability_zone", cluster.PreferredAvailabilityZone) if cluster.NotificationConfiguration != nil { - if *cluster.NotificationConfiguration.TopicStatus == "active" { + if aws.StringValue(cluster.NotificationConfiguration.TopicStatus) == "active" { d.Set("notification_topic_arn", cluster.NotificationConfiguration.TopicArn) } } diff --git a/aws/data_source_aws_elasticache_cluster_test.go b/aws/data_source_aws_elasticache_cluster_test.go index 9891262b660c..df4ffdf95eb1 100644 --- a/aws/data_source_aws_elasticache_cluster_test.go +++ b/aws/data_source_aws_elasticache_cluster_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSDataElasticacheCluster_basic(t *testing.T) { dataSourceName := "data.aws_elasticache_cluster.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSElastiCacheClusterConfigWithDataSource(rName), diff --git a/aws/data_source_aws_elasticache_replication_group.go b/aws/data_source_aws_elasticache_replication_group.go index f748325ca575..a8a2d4a8fe01 100644 --- a/aws/data_source_aws_elasticache_replication_group.go +++ b/aws/data_source_aws_elasticache_replication_group.go @@ -16,8 +16,9 @@ func dataSourceAwsElasticacheReplicationGroup() *schema.Resource { Read: dataSourceAwsElasticacheReplicationGroupRead, Schema: map[string]*schema.Schema{ "replication_group_id": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validateReplicationGroupID, }, "replication_group_description": { Type: schema.TypeString, diff --git a/aws/data_source_aws_elasticache_replication_group_test.go b/aws/data_source_aws_elasticache_replication_group_test.go index ff4d5b9797e0..ae8c679dee1b 100644 --- a/aws/data_source_aws_elasticache_replication_group_test.go +++ b/aws/data_source_aws_elasticache_replication_group_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsElasticacheReplicationGroup_basic(t *testing.T) { dataSourceName := "data.aws_elasticache_replication_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsElasticacheReplicationGroupConfig_basic(rName), @@ -46,8 +48,9 @@ func TestAccDataSourceAwsElasticacheReplicationGroup_ClusterMode(t *testing.T) { dataSourceName := "data.aws_elasticache_replication_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsElasticacheReplicationGroupConfig_ClusterMode(rName), @@ -72,8 +75,9 @@ func TestAccDataSourceAwsElasticacheReplicationGroup_MultiAZ(t *testing.T) { dataSourceName := "data.aws_elasticache_replication_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsElasticacheReplicationGroupConfig_MultiAZ(rName), @@ -89,8 +93,9 @@ func TestAccDataSourceAwsElasticacheReplicationGroup_MultiAZ(t *testing.T) { func TestAccDataSourceAwsElasticacheReplicationGroup_NonExistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsElasticacheReplicationGroupConfig_NonExistent, diff --git a/aws/data_source_aws_elasticache_user.go b/aws/data_source_aws_elasticache_user.go new file mode 100644 index 000000000000..8cfa7b004ca2 --- /dev/null +++ b/aws/data_source_aws_elasticache_user.go @@ -0,0 +1,76 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsElastiCacheUser() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsElastiCacheUserRead, + + Schema: map[string]*schema.Schema{ + "access_string": { + Type: schema.TypeString, + Optional: true, + }, + "engine": { + Type: schema.TypeString, + Optional: true, + }, + "no_password_required": { + Type: schema.TypeBool, + Optional: true, + }, + "passwords": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Sensitive: true, + }, + "user_id": { + Type: schema.TypeString, + Required: true, + }, + "user_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsElastiCacheUserRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + params := &elasticache.DescribeUsersInput{ + UserId: aws.String(d.Get("user_id").(string)), + } + + log.Printf("[DEBUG] Reading ElastiCache User: %s", params) + response, err := conn.DescribeUsers(params) + if err != nil { + return err + } + + if len(response.Users) != 1 { + return fmt.Errorf("[ERROR] Query returned wrong number of results. Please change your search criteria and try again.") + } + + user := response.Users[0] + + d.SetId(aws.StringValue(user.UserId)) + + d.Set("access_string", user.AccessString) + d.Set("engine", user.Engine) + d.Set("user_id", user.UserId) + d.Set("user_name", user.UserName) + + return nil + +} diff --git a/aws/data_source_aws_elasticache_user_test.go b/aws/data_source_aws_elasticache_user_test.go new file mode 100644 index 000000000000..0fd26fe51dd0 --- /dev/null +++ b/aws/data_source_aws_elasticache_user_test.go @@ -0,0 +1,50 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAWSElasticacheUser_basic(t *testing.T) { + resourceName := "aws_elasticache_user.test-basic" + dataSourceName := "data.aws_elasticache_user.test-basic" + rName := acctest.RandomWithPrefix("tf-acc") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAWSElastiCacheUserConfigWithDataSource(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "engine", resourceName, "engine"), + resource.TestCheckResourceAttrPair(dataSourceName, "user_id", resourceName, "user_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "user_name", resourceName, "user_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "access_string", resourceName, "access_string"), + ), + }, + }, + }) +} + +// Basic Resource +func testAccAWSElastiCacheUserConfigWithDataSource(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_user" "test-basic" { + user_id = %[1]q + user_name = %[1]q + access_string = "on ~* +@all" + engine = "REDIS" + no_password_required = true +} + +data "aws_elasticache_user" "test-basic" { + user_id = aws_elasticache_user.test-basic.user_id +} +`, rName) +} diff --git a/aws/data_source_aws_elasticsearch_domain.go b/aws/data_source_aws_elasticsearch_domain.go index 578ff3352341..609e5f0e8f96 100644 --- a/aws/data_source_aws_elasticsearch_domain.go +++ b/aws/data_source_aws_elasticsearch_domain.go @@ -352,7 +352,7 @@ func dataSourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface } } else { if ds.Endpoint != nil { - d.Set("endpoint", aws.StringValue(ds.Endpoint)) + d.Set("endpoint", ds.Endpoint) d.Set("kibana_endpoint", getKibanaEndpoint(d)) } if ds.Endpoints != nil { diff --git a/aws/data_source_aws_elasticsearch_domain_test.go b/aws/data_source_aws_elasticsearch_domain_test.go index b5a602188f90..1ea735ffc1b3 100644 --- a/aws/data_source_aws_elasticsearch_domain_test.go +++ b/aws/data_source_aws_elasticsearch_domain_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elasticsearchservice" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSDataElasticsearchDomain_basic(t *testing.T) { resourceName := "aws_elasticsearch_domain.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckIamServiceLinkedRoleEs(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckIamServiceLinkedRoleEs(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearchservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSElasticsearchDomainConfigWithDataSource(rInt), @@ -46,8 +48,9 @@ func TestAccAWSDataElasticsearchDomain_advanced(t *testing.T) { resourceName := "aws_elasticsearch_domain.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckIamServiceLinkedRoleEs(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckIamServiceLinkedRoleEs(t) }, + ErrorCheck: testAccErrorCheck(t, elasticsearchservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSElasticsearchDomainConfigAdvancedWithDataSource(rInt), diff --git a/aws/data_source_aws_elb.go b/aws/data_source_aws_elb.go index 03b0b78281b5..23ccc7090722 100644 --- a/aws/data_source_aws_elb.go +++ b/aws/data_source_aws_elb.go @@ -8,6 +8,8 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" ) func dataSourceAwsElb() *schema.Resource { @@ -195,22 +197,22 @@ func dataSourceAwsElb() *schema.Resource { } func dataSourceAwsElbRead(d *schema.ResourceData, meta interface{}) error { - elbconn := meta.(*AWSClient).elbconn + conn := meta.(*AWSClient).elbconn ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig lbName := d.Get("name").(string) input := &elb.DescribeLoadBalancersInput{ - LoadBalancerNames: []*string{aws.String(lbName)}, + LoadBalancerNames: aws.StringSlice([]string{lbName}), } log.Printf("[DEBUG] Reading ELB: %s", input) - resp, err := elbconn.DescribeLoadBalancers(input) + resp, err := conn.DescribeLoadBalancers(input) if err != nil { - return fmt.Errorf("Error retrieving LB: %w", err) + return fmt.Errorf("error retrieving LB: %w", err) } if len(resp.LoadBalancerDescriptions) != 1 { - return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(resp.LoadBalancerDescriptions)) + return fmt.Errorf("search returned %d results, please revise so only one is returned", len(resp.LoadBalancerDescriptions)) } d.SetId(aws.StringValue(resp.LoadBalancerDescriptions[0].LoadBalancerName)) @@ -219,9 +221,102 @@ func dataSourceAwsElbRead(d *schema.ResourceData, meta interface{}) error { Region: meta.(*AWSClient).region, Service: "elasticloadbalancing", AccountID: meta.(*AWSClient).accountid, - Resource: fmt.Sprintf("loadbalancer/%s", *resp.LoadBalancerDescriptions[0].LoadBalancerName), + Resource: fmt.Sprintf("loadbalancer/%s", aws.StringValue(resp.LoadBalancerDescriptions[0].LoadBalancerName)), } d.Set("arn", arn.String()) - return flattenAwsELbResource(d, meta.(*AWSClient).ec2conn, elbconn, resp.LoadBalancerDescriptions[0], ignoreTagsConfig) + lb := resp.LoadBalancerDescriptions[0] + ec2conn := meta.(*AWSClient).ec2conn + + describeAttrsOpts := &elb.DescribeLoadBalancerAttributesInput{ + LoadBalancerName: aws.String(d.Id()), + } + describeAttrsResp, err := conn.DescribeLoadBalancerAttributes(describeAttrsOpts) + if err != nil { + return fmt.Errorf("error retrieving ELB: %w", err) + } + + lbAttrs := describeAttrsResp.LoadBalancerAttributes + + d.Set("name", lb.LoadBalancerName) + d.Set("dns_name", lb.DNSName) + d.Set("zone_id", lb.CanonicalHostedZoneNameID) + + var scheme bool + if lb.Scheme != nil { + scheme = aws.StringValue(lb.Scheme) == "internal" + } + d.Set("internal", scheme) + d.Set("availability_zones", flattenStringList(lb.AvailabilityZones)) + d.Set("instances", flattenInstances(lb.Instances)) + d.Set("listener", flattenListeners(lb.ListenerDescriptions)) + d.Set("security_groups", flattenStringList(lb.SecurityGroups)) + if lb.SourceSecurityGroup != nil { + group := lb.SourceSecurityGroup.GroupName + if lb.SourceSecurityGroup.OwnerAlias != nil && aws.StringValue(lb.SourceSecurityGroup.OwnerAlias) != "" { + group = aws.String(aws.StringValue(lb.SourceSecurityGroup.OwnerAlias) + "/" + aws.StringValue(lb.SourceSecurityGroup.GroupName)) + } + d.Set("source_security_group", group) + + // Manually look up the ELB Security Group ID, since it's not provided + var elbVpc string + if lb.VPCId != nil { + elbVpc = aws.StringValue(lb.VPCId) + sg, err := finder.SecurityGroupByNameAndVpcID(ec2conn, aws.StringValue(lb.SourceSecurityGroup.GroupName), elbVpc) + if err != nil { + return fmt.Errorf("error looking up ELB Security Group ID: %w", err) + } else { + d.Set("source_security_group_id", sg.GroupId) + } + } + } + d.Set("subnets", flattenStringList(lb.Subnets)) + if lbAttrs.ConnectionSettings != nil { + d.Set("idle_timeout", lbAttrs.ConnectionSettings.IdleTimeout) + } + d.Set("connection_draining", lbAttrs.ConnectionDraining.Enabled) + d.Set("connection_draining_timeout", lbAttrs.ConnectionDraining.Timeout) + d.Set("cross_zone_load_balancing", lbAttrs.CrossZoneLoadBalancing.Enabled) + if lbAttrs.AccessLog != nil { + // The AWS API does not allow users to remove access_logs, only disable them. + // During creation of the ELB, Terraform sets the access_logs to disabled, + // so there should not be a case where lbAttrs.AccessLog above is nil. + + // Here we do not record the remove value of access_log if: + // - there is no access_log block in the configuration + // - the remote access_logs are disabled + // + // This indicates there is no access_log in the configuration. + // - externally added access_logs will be enabled, so we'll detect the drift + // - locally added access_logs will be in the config, so we'll add to the + // API/state + // See https://github.com/hashicorp/terraform/issues/10138 + _, n := d.GetChange("access_logs") + elbal := lbAttrs.AccessLog + nl := n.([]interface{}) + if len(nl) == 0 && !aws.BoolValue(elbal.Enabled) { + elbal = nil + } + if err := d.Set("access_logs", flattenAccessLog(elbal)); err != nil { + return err + } + } + + tags, err := keyvaluetags.ElbListTags(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error listing tags for ELB (%s): %w", d.Id(), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + // There's only one health check, so save that to state as we + // currently can + if aws.StringValue(lb.HealthCheck.Target) != "" { + d.Set("health_check", flattenHealthCheck(lb.HealthCheck)) + } + + return nil } diff --git a/aws/data_source_aws_elb_hosted_zone_id.go b/aws/data_source_aws_elb_hosted_zone_id.go index e21767ee0434..f17ee59ab211 100644 --- a/aws/data_source_aws_elb_hosted_zone_id.go +++ b/aws/data_source_aws_elb_hosted_zone_id.go @@ -14,7 +14,7 @@ var elbHostedZoneIdPerRegionMap = map[string]string{ endpoints.ApEast1RegionID: "Z3DQVH9N71FHZ0", endpoints.ApNortheast1RegionID: "Z14GRHDCWA56QT", endpoints.ApNortheast2RegionID: "ZWKZPGTI48KDX", - "ap-northeast-3": "Z5LXEXXYW11ES", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "Z5LXEXXYW11ES", endpoints.ApSouth1RegionID: "ZP97RAFLXTNZK", endpoints.ApSoutheast1RegionID: "Z1LMS91P8CMLE5", endpoints.ApSoutheast2RegionID: "Z1GM3OXH4ZPM65", diff --git a/aws/data_source_aws_elb_hosted_zone_id_test.go b/aws/data_source_aws_elb_hosted_zone_id_test.go index 96c0ff98d919..0ac6299cb72f 100644 --- a/aws/data_source_aws_elb_hosted_zone_id_test.go +++ b/aws/data_source_aws_elb_hosted_zone_id_test.go @@ -3,13 +3,15 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccAWSElbHostedZoneId_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElbHostedZoneIdConfig, diff --git a/aws/data_source_aws_elb_service_account.go b/aws/data_source_aws_elb_service_account.go index a8328697dc65..1251ab0bd5d1 100644 --- a/aws/data_source_aws_elb_service_account.go +++ b/aws/data_source_aws_elb_service_account.go @@ -14,7 +14,7 @@ var elbAccountIdPerRegionMap = map[string]string{ endpoints.ApEast1RegionID: "754344448648", endpoints.ApNortheast1RegionID: "582318560864", endpoints.ApNortheast2RegionID: "600734575887", - "ap-northeast-3": "383597477331", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "383597477331", endpoints.ApSouth1RegionID: "718504428378", endpoints.ApSoutheast1RegionID: "114774131450", endpoints.ApSoutheast2RegionID: "783225319266", diff --git a/aws/data_source_aws_elb_service_account_test.go b/aws/data_source_aws_elb_service_account_test.go index d4ba325e34a5..48df06c60717 100644 --- a/aws/data_source_aws_elb_service_account_test.go +++ b/aws/data_source_aws_elb_service_account_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSElbServiceAccount_basic(t *testing.T) { dataSourceName := "data.aws_elb_service_account.main" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElbServiceAccountConfig, @@ -32,8 +34,9 @@ func TestAccAWSElbServiceAccount_Region(t *testing.T) { dataSourceName := "data.aws_elb_service_account.regional" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsElbServiceAccountExplicitRegionConfig, diff --git a/aws/data_source_aws_elb_test.go b/aws/data_source_aws_elb_test.go index 813eaeb44bcd..4340ecc5ffe1 100644 --- a/aws/data_source_aws_elb_test.go +++ b/aws/data_source_aws_elb_test.go @@ -4,32 +4,36 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAWSELB_basic(t *testing.T) { // Must be less than 32 characters for ELB name - rName := fmt.Sprintf("TestAccDataSourceAWSELB-%s", acctest.RandString(5)) + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_elb.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSELBConfigBasic(rName, t.Name()), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "name", rName), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "cross_zone_load_balancing", "true"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "idle_timeout", "30"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "internal", "true"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "subnets.#", "2"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "security_groups.#", "1"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "tags.%", "1"), - resource.TestCheckResourceAttr("data.aws_elb.elb_test", "tags.TestName", t.Name()), - resource.TestCheckResourceAttrSet("data.aws_elb.elb_test", "dns_name"), - resource.TestCheckResourceAttrSet("data.aws_elb.elb_test", "zone_id"), - resource.TestCheckResourceAttrPair("data.aws_elb.elb_test", "arn", "aws_elb.elb_test", "arn"), + resource.TestCheckResourceAttr(dataSourceName, "name", rName), + resource.TestCheckResourceAttr(dataSourceName, "cross_zone_load_balancing", "true"), + resource.TestCheckResourceAttr(dataSourceName, "idle_timeout", "30"), + resource.TestCheckResourceAttr(dataSourceName, "internal", "true"), + resource.TestCheckResourceAttr(dataSourceName, "subnets.#", "2"), + resource.TestCheckResourceAttr(dataSourceName, "security_groups.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(dataSourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(dataSourceName, "tags.TestName", t.Name()), + resource.TestCheckResourceAttrSet(dataSourceName, "dns_name"), + resource.TestCheckResourceAttrSet(dataSourceName, "zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", "aws_elb.test", "arn"), ), }, }, @@ -37,12 +41,12 @@ func TestAccDataSourceAWSELB_basic(t *testing.T) { } func testAccDataSourceAWSELBConfigBasic(rName, testName string) string { - return testAccAvailableAZsNoOptInConfig() + fmt.Sprintf(` -resource "aws_elb" "elb_test" { - name = "%[1]s" + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_elb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.elb_test.id] - subnets = aws_subnet.elb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 @@ -54,39 +58,37 @@ resource "aws_elb" "elb_test" { } tags = { - TestName = "%[2]s" + Name = %[1]q + TestName = %[2]q } } -variable "subnets" { - default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -resource "aws_vpc" "elb_test" { +resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-elb-data-source" + Name = %[1]q } } -resource "aws_subnet" "elb_test" { - count = 2 - vpc_id = aws_vpc.elb_test.id - cidr_block = element(var.subnets, count.index) +resource "aws_subnet" "test" { + count = 2 + + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + vpc_id = aws_vpc.test.id + map_public_ip_on_launch = true - availability_zone = element(data.aws_availability_zones.available.names, count.index) tags = { - Name = "tf-acc-elb-data-source" + Name = %[1]q } } -resource "aws_security_group" "elb_test" { +resource "aws_security_group" "test" { name = "%[1]s" description = "%[2]s" - vpc_id = aws_vpc.elb_test.id + vpc_id = aws_vpc.test.id ingress { from_port = 0 @@ -103,12 +105,13 @@ resource "aws_security_group" "elb_test" { } tags = { - TestName = "%[2]s" + Name = %[1]q + TestName = %[2]q } } -data "aws_elb" "elb_test" { - name = aws_elb.elb_test.name +data "aws_elb" "test" { + name = aws_elb.test.name } -`, rName, testName) +`, rName, testName)) } diff --git a/aws/data_source_aws_globalaccelerator_accelerator.go b/aws/data_source_aws_globalaccelerator_accelerator.go new file mode 100644 index 000000000000..3353d9e1adab --- /dev/null +++ b/aws/data_source_aws_globalaccelerator_accelerator.go @@ -0,0 +1,153 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/globalaccelerator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/globalaccelerator/finder" +) + +func dataSourceAwsGlobalAcceleratorAccelerator() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsGlobalAcceleratorAcceleratorRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "ip_address_type": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "dns_name": { + Type: schema.TypeString, + Computed: true, + }, + "hosted_zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "ip_sets": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_addresses": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "ip_family": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "attributes": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "flow_logs_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "flow_logs_s3_bucket": { + Type: schema.TypeString, + Computed: true, + }, + "flow_logs_s3_prefix": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsGlobalAcceleratorAcceleratorRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).globalacceleratorconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + var results []*globalaccelerator.Accelerator + + err := conn.ListAcceleratorsPages(&globalaccelerator.ListAcceleratorsInput{}, func(page *globalaccelerator.ListAcceleratorsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, l := range page.Accelerators { + if l == nil { + continue + } + + if v, ok := d.GetOk("arn"); ok && v.(string) != aws.StringValue(l.AcceleratorArn) { + continue + } + + if v, ok := d.GetOk("name"); ok && v.(string) != aws.StringValue(l.Name) { + continue + } + + results = append(results, l) + } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error reading AWS Global Accelerator: %w", err) + } + + if len(results) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) + } + + accelerator := results[0] + d.SetId(aws.StringValue(accelerator.AcceleratorArn)) + d.Set("arn", accelerator.AcceleratorArn) + d.Set("enabled", accelerator.Enabled) + d.Set("dns_name", accelerator.DnsName) + d.Set("hosted_zone_id", globalAcceleratorRoute53ZoneID) + d.Set("name", accelerator.Name) + d.Set("ip_address_type", accelerator.IpAddressType) + d.Set("ip_sets", flattenGlobalAcceleratorIpSets(accelerator.IpSets)) + + acceleratorAttributes, err := finder.AcceleratorAttributesByARN(conn, d.Id()) + if err != nil { + return fmt.Errorf("error reading Global Accelerator Accelerator (%s) attributes: %w", d.Id(), err) + } + + if err := d.Set("attributes", []interface{}{flattenGlobalAcceleratorAcceleratorAttributes(acceleratorAttributes)}); err != nil { + return fmt.Errorf("error setting attributes: %w", err) + } + + tags, err := keyvaluetags.GlobalacceleratorListTags(conn, d.Id()) + if err != nil { + return fmt.Errorf("error listing tags for Global Accelerator Accelerator (%s): %w", d.Id(), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + return nil +} diff --git a/aws/data_source_aws_globalaccelerator_accelerator_test.go b/aws/data_source_aws_globalaccelerator_accelerator_test.go new file mode 100644 index 000000000000..b9aa18277ab9 --- /dev/null +++ b/aws/data_source_aws_globalaccelerator_accelerator_test.go @@ -0,0 +1,79 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/globalaccelerator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSDataGlobalAcceleratorAccelerator_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_globalaccelerator_accelerator.test" + dataSourceName := "data.aws_globalaccelerator_accelerator.test_by_arn" + dataSourceName2 := "data.aws_globalaccelerator_accelerator.test_by_name" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckGlobalAccelerator(t) }, + ErrorCheck: testAccErrorCheck(t, globalaccelerator.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSGlobalAcceleratorAcceleratorConfigWithDataSource(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "attributes.#", resourceName, "attributes.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "attributes.0.flow_logs_enabled", resourceName, "attributes.0.flow_logs_enabled"), + resource.TestCheckResourceAttrPair(dataSourceName, "attributes.0.flow_logs_s3_bucket", resourceName, "attributes.0.flow_logs_s3_bucket"), + resource.TestCheckResourceAttrPair(dataSourceName, "attributes.0.flow_logs_s3_prefix", resourceName, "attributes.0.flow_logs_s3_prefix"), + resource.TestCheckResourceAttrPair(dataSourceName, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "enabled", resourceName, "enabled"), + resource.TestCheckResourceAttrPair(dataSourceName, "hosted_zone_id", resourceName, "hosted_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_address_type", resourceName, "ip_address_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_sets.#", resourceName, "ip_sets.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_sets.0.ip_addresses.#", resourceName, "ip_sets.0.ip_addresses.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_sets.0.ip_addresses.0", resourceName, "ip_sets.0.ip_addresses.0"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_sets.0.ip_addresses.1", resourceName, "ip_sets.0.ip_addresses.1"), + resource.TestCheckResourceAttrPair(dataSourceName, "ip_sets.0.ip_family", resourceName, "ip_sets.0.ip_family"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName2, "attributes.#", resourceName, "attributes.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "attributes.0.flow_logs_enabled", resourceName, "attributes.0.flow_logs_enabled"), + resource.TestCheckResourceAttrPair(dataSourceName2, "attributes.0.flow_logs_s3_bucket", resourceName, "attributes.0.flow_logs_s3_bucket"), + resource.TestCheckResourceAttrPair(dataSourceName2, "attributes.0.flow_logs_s3_prefix", resourceName, "attributes.0.flow_logs_s3_prefix"), + resource.TestCheckResourceAttrPair(dataSourceName2, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName2, "enabled", resourceName, "enabled"), + resource.TestCheckResourceAttrPair(dataSourceName2, "hosted_zone_id", resourceName, "hosted_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_address_type", resourceName, "ip_address_type"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_sets.#", resourceName, "ip_sets.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_sets.0.ip_addresses.#", resourceName, "ip_sets.0.ip_addresses.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_sets.0.ip_addresses.0", resourceName, "ip_sets.0.ip_addresses.0"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_sets.0.ip_addresses.1", resourceName, "ip_sets.0.ip_addresses.1"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ip_sets.0.ip_family", resourceName, "ip_sets.0.ip_family"), + resource.TestCheckResourceAttrPair(dataSourceName2, "name", resourceName, "name"), + ), + }, + }, + }) +} + +func testAccAWSGlobalAcceleratorAcceleratorConfigWithDataSource(rName string) string { + return fmt.Sprintf(` +resource "aws_globalaccelerator_accelerator" "test" { + name = %[1]q + attributes { + flow_logs_enabled = false + flow_logs_s3_bucket = "" + flow_logs_s3_prefix = "flow-logs/globalaccelerator/" + } +} + +data "aws_globalaccelerator_accelerator" "test_by_arn" { + arn = aws_globalaccelerator_accelerator.test.id +} + +data "aws_globalaccelerator_accelerator" "test_by_name" { + name = aws_globalaccelerator_accelerator.test.name +} +`, rName) +} diff --git a/aws/data_source_aws_glue_connection.go b/aws/data_source_aws_glue_connection.go new file mode 100644 index 000000000000..18fa47a04b11 --- /dev/null +++ b/aws/data_source_aws_glue_connection.go @@ -0,0 +1,130 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/glue" + "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" +) + +func dataSourceAwsGlueConnection() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsGlueConnectionRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "catalog_id": { + Type: schema.TypeString, + Computed: true, + }, + "connection_properties": { + Type: schema.TypeMap, + Computed: true, + Sensitive: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "connection_type": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "match_criteria": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "physical_connection_requirements": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "availability_zone": { + Type: schema.TypeString, + Computed: true, + }, + "security_group_id_list": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceAwsGlueConnectionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).glueconn + id := d.Get("id").(string) + catalogID, connectionName, err := decodeGlueConnectionID(id) + if err != nil { + return diag.Errorf("error decoding Glue Connection %s: %s", id, err) + } + input := &glue.GetConnectionInput{ + CatalogId: aws.String(catalogID), + Name: aws.String(connectionName), + } + output, err := conn.GetConnection(input) + if err != nil { + if isAWSErr(err, glue.ErrCodeEntityNotFoundException, "") { + return diag.Errorf("error Glue Connection (%s) not found", id) + } + return diag.Errorf("error reading Glue Connection (%s): %s", id, err) + } + + connection := output.Connection + d.SetId(id) + d.Set("catalog_id", catalogID) + d.Set("connection_type", connection.ConnectionType) + d.Set("name", connection.Name) + d.Set("description", connection.Description) + + connectionArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "glue", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("connection/%s", connectionName), + }.String() + d.Set("arn", connectionArn) + + if err := d.Set("connection_properties", aws.StringValueMap(connection.ConnectionProperties)); err != nil { + return diag.Errorf("error setting connection_properties: %s", err) + } + + if err := d.Set("physical_connection_requirements", flattenGluePhysicalConnectionRequirements(connection.PhysicalConnectionRequirements)); err != nil { + return diag.Errorf("error setting physical_connection_requirements: %s", err) + } + + if err := d.Set("match_criteria", flattenStringList(connection.MatchCriteria)); err != nil { + return diag.Errorf("error setting match_criteria: %s", err) + } + + return nil +} diff --git a/aws/data_source_aws_glue_connection_test.go b/aws/data_source_aws_glue_connection_test.go new file mode 100644 index 000000000000..a62255c814cb --- /dev/null +++ b/aws/data_source_aws_glue_connection_test.go @@ -0,0 +1,71 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/glue" + "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" +) + +func TestAccDataSourceAwsGlueConnection_basic(t *testing.T) { + resourceName := "aws_glue_connection.test" + datasourceName := "data.aws_glue_connection.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + jdbcConnectionUrl := fmt.Sprintf("jdbc:mysql://%s/testdatabase", testAccRandomDomainName()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, glue.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsGlueConnectionConfig(rName, jdbcConnectionUrl), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsGlueConnectionCheck(datasourceName), + resource.TestCheckResourceAttrPair(datasourceName, "catalog_id", resourceName, "catalog_id"), + resource.TestCheckResourceAttrPair(datasourceName, "connection_type", resourceName, "connection_type"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "connection_properties", resourceName, "connection_properties"), + resource.TestCheckResourceAttrPair(datasourceName, "physical_connection_requirements", resourceName, "physical_connection_requirements"), + resource.TestCheckResourceAttrPair(datasourceName, "match_criteria", resourceName, "match_criteria"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsGlueConnectionCheck(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("root module has no resource called %s", name) + } + + return nil + } +} + +func testAccDataSourceAwsGlueConnectionConfig(rName, jdbcConnectionUrl string) string { + return fmt.Sprintf(` +resource "aws_glue_connection" "test" { + name = %[1]q + + connection_properties = { + JDBC_CONNECTION_URL = %[2]q + PASSWORD = "testpassword" + USERNAME = "testusername" + } + +} + +data "aws_glue_connection" "test" { + id = aws_glue_connection.test.id +} +`, rName, jdbcConnectionUrl) +} diff --git a/aws/data_source_aws_glue_data_catalog_encryption_settings.go b/aws/data_source_aws_glue_data_catalog_encryption_settings.go new file mode 100644 index 000000000000..2fa57400fb11 --- /dev/null +++ b/aws/data_source_aws_glue_data_catalog_encryption_settings.go @@ -0,0 +1,81 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/glue" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsGlueDataCatalogEncryptionSettings() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAwsGlueDataCatalogEncryptionSettingsRead, + Schema: map[string]*schema.Schema{ + "catalog_id": { + Type: schema.TypeString, + Required: true, + }, + "data_catalog_encryption_settings": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "connection_password_encryption": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "aws_kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "return_connection_password_encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "encryption_at_rest": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "catalog_encryption_mode": { + Type: schema.TypeString, + Computed: true, + }, + "sse_aws_kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceAwsGlueDataCatalogEncryptionSettingsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).glueconn + id := d.Get("catalog_id").(string) + input := &glue.GetDataCatalogEncryptionSettingsInput{ + CatalogId: aws.String(id), + } + out, err := conn.GetDataCatalogEncryptionSettings(input) + if err != nil { + return diag.Errorf("Error reading Glue Data Catalog Encryption Settings: %s", err) + } + d.SetId(id) + d.Set("catalog_id", d.Id()) + + if err := d.Set("data_catalog_encryption_settings", flattenGlueDataCatalogEncryptionSettings(out.DataCatalogEncryptionSettings)); err != nil { + return diag.Errorf("error setting data_catalog_encryption_settings: %s", err) + } + return nil +} diff --git a/aws/data_source_aws_glue_data_catalog_encryption_settings_test.go b/aws/data_source_aws_glue_data_catalog_encryption_settings_test.go new file mode 100644 index 000000000000..6dee030639e5 --- /dev/null +++ b/aws/data_source_aws_glue_data_catalog_encryption_settings_test.go @@ -0,0 +1,62 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/glue" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataSourceAwsGlueDataCatalogEncryptionSettings_basic(t *testing.T) { + resourceName := "aws_glue_data_catalog_encryption_settings.test" + datasourceName := "data.aws_glue_data_catalog_encryption_settings.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, glue.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsGlueDataCatalogEncryptionSettingsConfig(), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsGlueDataCatalogEncryptionSettingsCheck(datasourceName), + resource.TestCheckResourceAttrPair(datasourceName, "catalog_id", resourceName, "catalog_id"), + resource.TestCheckResourceAttrPair(datasourceName, "data_catalog_encryption_settings", resourceName, "data_catalog_encryption_settings"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsGlueDataCatalogEncryptionSettingsCheck(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("root module has no resource called %s", name) + } + + return nil + } +} + +func testAccDataSourceAwsGlueDataCatalogEncryptionSettingsConfig() string { + return ` +resource "aws_glue_data_catalog_encryption_settings" "test" { + data_catalog_encryption_settings { + connection_password_encryption { + return_connection_password_encrypted = false + } + + encryption_at_rest { + catalog_encryption_mode = "DISABLED" + } + } +} + +data "aws_glue_data_catalog_encryption_settings" "test" { + catalog_id = aws_glue_data_catalog_encryption_settings.test.id +} +` +} diff --git a/aws/data_source_aws_glue_script_test.go b/aws/data_source_aws_glue_script_test.go index d369141d38db..f43fa461e6db 100644 --- a/aws/data_source_aws_glue_script_test.go +++ b/aws/data_source_aws_glue_script_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/glue" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ func TestAccDataSourceAWSGlueScript_Language_Python(t *testing.T) { dataSourceName := "data.aws_glue_script.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, glue.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSGlueScriptConfigPython(), @@ -28,8 +30,9 @@ func TestAccDataSourceAWSGlueScript_Language_Scala(t *testing.T) { dataSourceName := "data.aws_glue_script.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, glue.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSGlueScriptConfigScala(), diff --git a/aws/data_source_aws_guardduty_detector_test.go b/aws/data_source_aws_guardduty_detector_test.go index bccba21cce0d..79dec12b641b 100644 --- a/aws/data_source_aws_guardduty_detector_test.go +++ b/aws/data_source_aws_guardduty_detector_test.go @@ -3,12 +3,14 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/guardduty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func testAccAWSGuarddutyDetectorDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, guardduty.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -31,8 +33,9 @@ func testAccAWSGuarddutyDetectorDataSource_basic(t *testing.T) { func testAccAWSGuarddutyDetectorDataSource_Id(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, guardduty.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsGuarddutyDetectorExplicitConfig(), diff --git a/aws/data_source_aws_iam_account_alias_test.go b/aws/data_source_aws_iam_account_alias_test.go new file mode 100644 index 000000000000..5d116ec14b8e --- /dev/null +++ b/aws/data_source_aws_iam_account_alias_test.go @@ -0,0 +1,44 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func testAccAWSIAMAccountAliasDataSource_basic(t *testing.T) { + dataSourceName := "data.aws_iam_account_alias.test" + resourceName := "aws_iam_account_alias.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIAMAccountAliasDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIAMAccountAliasDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "account_alias", resourceName, "account_alias"), + ), + }, + }, + }) +} + +func testAccAWSIAMAccountAliasDataSourceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_account_alias" "test" { + account_alias = %[1]q +} + +data "aws_iam_account_alias" "test" { + depends_on = [aws_iam_account_alias.test] +} +`, rName) +} diff --git a/aws/data_source_aws_iam_group_test.go b/aws/data_source_aws_iam_group_test.go index cd85d7ea53a3..6fb013ccbb07 100644 --- a/aws/data_source_aws_iam_group_test.go +++ b/aws/data_source_aws_iam_group_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,11 +13,12 @@ func TestAccAWSDataSourceIAMGroup_basic(t *testing.T) { groupName := fmt.Sprintf("test-datasource-user-%d", acctest.RandInt()) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMGroupConfig(groupName), + Config: testAccAwsIAMGroupDataSourceConfig(groupName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("data.aws_iam_group.test", "group_id"), resource.TestCheckResourceAttr("data.aws_iam_group.test", "path", "/"), @@ -35,11 +37,12 @@ func TestAccAWSDataSourceIAMGroup_users(t *testing.T) { userCount := 101 resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMGroupConfigWithUser(groupName, userName, groupMemberShipName, userCount), + Config: testAccAwsIAMGroupDataSourceConfigWithUser(groupName, userName, groupMemberShipName, userCount), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("data.aws_iam_group.test", "group_id"), resource.TestCheckResourceAttr("data.aws_iam_group.test", "path", "/"), @@ -56,7 +59,7 @@ func TestAccAWSDataSourceIAMGroup_users(t *testing.T) { }) } -func testAccAwsIAMGroupConfig(name string) string { +func testAccAwsIAMGroupDataSourceConfig(name string) string { return fmt.Sprintf(` resource "aws_iam_group" "group" { name = "%s" @@ -69,7 +72,7 @@ data "aws_iam_group" "test" { `, name) } -func testAccAwsIAMGroupConfigWithUser(groupName, userName, membershipName string, userCount int) string { +func testAccAwsIAMGroupDataSourceConfigWithUser(groupName, userName, membershipName string, userCount int) string { return fmt.Sprintf(` resource "aws_iam_group" "group" { name = "%s" diff --git a/aws/data_source_aws_iam_instance_profile_test.go b/aws/data_source_aws_iam_instance_profile_test.go index 078f47347a75..be2f36b2b26a 100644 --- a/aws/data_source_aws_iam_instance_profile_test.go +++ b/aws/data_source_aws_iam_instance_profile_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccAWSDataSourceIAMInstanceProfile_basic(t *testing.T) { profileName := fmt.Sprintf("tf-acc-ds-instance-profile-%d", acctest.RandInt()) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDatasourceAwsIamInstanceProfileConfig(roleName, profileName), diff --git a/aws/data_source_aws_iam_policy.go b/aws/data_source_aws_iam_policy.go index 8e2d3fdd161b..b4177d16c919 100644 --- a/aws/data_source_aws_iam_policy.go +++ b/aws/data_source_aws_iam_policy.go @@ -1,7 +1,19 @@ package aws import ( + "fmt" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func dataSourceAwsIAMPolicy() *schema.Resource { @@ -10,31 +22,176 @@ func dataSourceAwsIAMPolicy() *schema.Resource { Schema: map[string]*schema.Schema{ "arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, + }, + "description": { Type: schema.TypeString, - Required: true, + Computed: true, }, "name": { Type: schema.TypeString, + Optional: true, Computed: true, }, - "policy": { + "path": { Type: schema.TypeString, Computed: true, }, - "path": { + "path_prefix": { + Type: schema.TypeString, + Optional: true, + }, + "policy": { Type: schema.TypeString, Computed: true, }, - - "description": { + "policy_id": { Type: schema.TypeString, Computed: true, }, + "tags": tagsSchemaComputed(), }, } } func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error { - d.SetId(d.Get("arn").(string)) - return resourceAwsIamPolicyRead(d, meta) + conn := meta.(*AWSClient).iamconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + arn := d.Get("arn").(string) + name := d.Get("name").(string) + pathPrefix := d.Get("path_prefix").(string) + + var results []*iam.Policy + + // Handle IAM eventual consistency + err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + results, err = finder.Policies(conn, arn, name, pathPrefix) + + if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + results, err = finder.Policies(conn, arn, name, pathPrefix) + } + + if err != nil { + return fmt.Errorf("error reading IAM policy (%s): %w", PolicySearchDetails(arn, name, pathPrefix), err) + } + + if len(results) == 0 { + return fmt.Errorf("no IAM policy found matching criteria (%s); try different search", PolicySearchDetails(arn, name, pathPrefix)) + } + + if len(results) > 1 { + return fmt.Errorf("multiple IAM policies found matching criteria (%s); try different search", PolicySearchDetails(arn, name, pathPrefix)) + } + + policy := results[0] + policyArn := aws.StringValue(policy.Arn) + + d.SetId(policyArn) + + d.Set("arn", policyArn) + d.Set("name", policy.PolicyName) + d.Set("path", policy.Path) + d.Set("policy_id", policy.PolicyId) + + if err := d.Set("tags", keyvaluetags.IamKeyValueTags(policy.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + // Retrieve policy description + policyInput := &iam.GetPolicyInput{ + PolicyArn: policy.Arn, + } + + policyOutput, err := conn.GetPolicy(policyInput) + + if err != nil { + return fmt.Errorf("error reading IAM policy (%s): %w", policyArn, err) + } + + if policyOutput == nil || policyOutput.Policy == nil { + return fmt.Errorf("error reading IAM policy (%s): empty output", policyArn) + } + + d.Set("description", policyOutput.Policy.Description) + + // Retrieve policy + policyVersionInput := &iam.GetPolicyVersionInput{ + PolicyArn: policy.Arn, + VersionId: policy.DefaultVersionId, + } + + // Handle IAM eventual consistency + var policyVersionOutput *iam.GetPolicyVersionOutput + err = resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + policyVersionOutput, err = conn.GetPolicyVersion(policyVersionInput) + + if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + policyVersionOutput, err = conn.GetPolicyVersion(policyVersionInput) + } + + if err != nil { + return fmt.Errorf("error reading IAM Policy (%s) version: %w", policyArn, err) + } + + if policyVersionOutput == nil || policyVersionOutput.PolicyVersion == nil { + return fmt.Errorf("error reading IAM Policy (%s) version: empty output", policyArn) + } + + policyVersion := policyVersionOutput.PolicyVersion + + var policyDocument string + if policyVersion != nil { + policyDocument, err = url.QueryUnescape(aws.StringValue(policyVersion.Document)) + if err != nil { + return fmt.Errorf("error parsing IAM Policy (%s) document: %w", policyArn, err) + } + } + + d.Set("policy", policyDocument) + + return nil +} + +// PolicySearchDetails returns the configured search criteria as a printable string +func PolicySearchDetails(arn, name, pathPrefix string) string { + var policyDetails []string + if arn != "" { + policyDetails = append(policyDetails, fmt.Sprintf("ARN: %s", arn)) + } + if name != "" { + policyDetails = append(policyDetails, fmt.Sprintf("Name: %s", name)) + } + if pathPrefix != "" { + policyDetails = append(policyDetails, fmt.Sprintf("PathPrefix: %s", pathPrefix)) + } + + return strings.Join(policyDetails, ", ") } diff --git a/aws/data_source_aws_iam_policy_document.go b/aws/data_source_aws_iam_policy_document.go index f8521c480fe7..04b7fc14dcdd 100644 --- a/aws/data_source_aws_iam_policy_document.go +++ b/aws/data_source_aws_iam_policy_document.go @@ -311,7 +311,7 @@ func dataSourceAwsIamPolicyDocumentMakeConditions(in []interface{}, version stri Variable: item["variable"].(string), } out[i].Values, err = dataSourceAwsIamPolicyDocumentReplaceVarsInList( - aws.StringValueSlice(expandStringList(item["values"].([]interface{}))), + aws.StringValueSlice(expandStringListKeepEmpty(item["values"].([]interface{}))), version, ) if err != nil { diff --git a/aws/data_source_aws_iam_policy_document_test.go b/aws/data_source_aws_iam_policy_document_test.go index 8ac7a72c49dd..7927f5845468 100644 --- a/aws/data_source_aws_iam_policy_document_test.go +++ b/aws/data_source_aws_iam_policy_document_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_basic(t *testing.T) { // acceptance test, but just instantiating the AWS provider requires // some AWS API calls, and so this needs valid AWS credentials to work. resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentConfig, @@ -34,8 +36,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_source(t *testing.T) { // acceptance test, but just instantiating the AWS provider requires // some AWS API calls, and so this needs valid AWS credentials to work. resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentSourceConfig, @@ -59,8 +62,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_source(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_sourceList(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentSourceListConfig, @@ -76,8 +80,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_sourceList(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_sourceConflicting(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentSourceConflictingConfig, @@ -93,8 +98,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_sourceConflicting(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_sourceListConflicting(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentSourceListConflictingConfig, @@ -106,8 +112,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_sourceListConflicting(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_override(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentOverrideConfig, @@ -123,8 +130,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_override(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_overrideList(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentOverrideListConfig, @@ -140,8 +148,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_overrideList(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_noStatementMerge(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentNoStatementMergeConfig, @@ -157,8 +166,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_noStatementMerge(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_noStatementOverride(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentNoStatementOverrideConfig, @@ -174,8 +184,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_noStatementOverride(t *testing.T) { func TestAccAWSDataSourceIAMPolicyDocument_duplicateSid(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentDuplicateSidConfig, @@ -198,8 +209,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_statementPrincipalIdentifiers_stringA dataSourceName := "data.aws_iam_policy_document.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentConfigStatementPrincipalIdentifiersStringAndSlice, @@ -216,8 +228,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_statementPrincipalIdentifiers_multipl dataSourceName := "data.aws_iam_policy_document.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionPreCheck(endpoints.AwsPartitionID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionPreCheck(endpoints.AwsPartitionID, t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentConfigStatementPrincipalIdentifiersMultiplePrincipals, @@ -233,8 +246,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_statementPrincipalIdentifiers_multipl dataSourceName := "data.aws_iam_policy_document.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionPreCheck(endpoints.AwsUsGovPartitionID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionPreCheck(endpoints.AwsUsGovPartitionID, t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentConfigStatementPrincipalIdentifiersMultiplePrincipals, @@ -248,8 +262,9 @@ func TestAccAWSDataSourceIAMPolicyDocument_statementPrincipalIdentifiers_multipl func TestAccAWSDataSourceIAMPolicyDocument_version20081017(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIAMPolicyDocumentDataSourceConfigVersion20081017ConversionCondition, @@ -312,6 +327,7 @@ data "aws_iam_policy_document" "test" { variable = "s3:prefix" values = [ "home/", + "", "home/&{aws:username}/", ] } @@ -394,6 +410,7 @@ func testAccAWSIAMPolicyDocumentExpectedJSON() string { "StringLike": { "s3:prefix": [ "home/", + "", "home/${aws:username}/" ] } diff --git a/aws/data_source_aws_iam_policy_test.go b/aws/data_source_aws_iam_policy_test.go index c78712bb513f..f14a6b469ebb 100644 --- a/aws/data_source_aws_iam_policy_test.go +++ b/aws/data_source_aws_iam_policy_test.go @@ -2,40 +2,184 @@ package aws import ( "fmt" + "regexp" "testing" + "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { - resourceName := "data.aws_iam_policy.test" +func TestPolicySearchDetails(t *testing.T) { + testCases := []struct { + Arn string + Name string + PathPrefix string + Expected string + }{ + { + Arn: "", + Name: "", + PathPrefix: "", + Expected: "", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "", + PathPrefix: "", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + }, + { + Arn: "", + Name: "tf-acc-test-policy", + PathPrefix: "", + Expected: "Name: tf-acc-test-policy", + }, + { + Arn: "", + Name: "", + PathPrefix: "/test-prefix/", + Expected: "PathPrefix: /test-prefix/", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "tf-acc-test-policy", + PathPrefix: "", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, Name: tf-acc-test-policy", //lintignore:AWSAT005 + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "", + PathPrefix: "/test-prefix/", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, PathPrefix: /test-prefix/", //lintignore:AWSAT005 + }, + { + Arn: "", + Name: "tf-acc-test-policy", + PathPrefix: "/test-prefix/", + Expected: "Name: tf-acc-test-policy, PathPrefix: /test-prefix/", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "tf-acc-test-policy", + PathPrefix: "/test-prefix/", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, Name: tf-acc-test-policy, PathPrefix: /test-prefix/", //lintignore:AWSAT005 + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := PolicySearchDetails(testCase.Arn, testCase.Name, testCase.PathPrefix) + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} + +func TestAccAWSDataSourceIAMPolicy_Arn(t *testing.T) { + datasourceName := "data.aws_iam_policy.test" + resourceName := "aws_iam_policy.test" + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_Arn(policyName, "/"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "path", resourceName, "path"), + resource.TestCheckResourceAttrPair(datasourceName, "policy", resourceName, "policy"), + resource.TestCheckResourceAttrPair(datasourceName, "policy_id", resourceName, "policy_id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "tags", resourceName, "tags"), + ), + }, + }, + }) +} + +func TestAccAWSDataSourceIAMPolicy_Name(t *testing.T) { + datasourceName := "data.aws_iam_policy.test" + resourceName := "aws_iam_policy.test" + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_Name(policyName, "/"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "path", resourceName, "path"), + resource.TestCheckResourceAttrPair(datasourceName, "policy", resourceName, "policy"), + resource.TestCheckResourceAttrPair(datasourceName, "policy_id", resourceName, "policy_id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "tags", resourceName, "tags"), + ), + }, + }, + }) +} + +func TestAccAWSDataSourceIAMPolicy_NameAndPathPrefix(t *testing.T) { + datasourceName := "data.aws_iam_policy.test" + resourceName := "aws_iam_policy.test" + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + policyPath := "/test-path/" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsDataSourceIamPolicyConfig(policyName), + Config: testAccAwsDataSourceIamPolicyConfig_PathPrefix(policyName, policyPath), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", policyName), - resource.TestCheckResourceAttr(resourceName, "description", "My test policy"), - resource.TestCheckResourceAttr(resourceName, "path", "/"), - resource.TestCheckResourceAttrSet(resourceName, "policy"), - resource.TestCheckResourceAttrPair(resourceName, "arn", "aws_iam_policy.test_policy", "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "path", resourceName, "path"), + resource.TestCheckResourceAttrPair(datasourceName, "policy", resourceName, "policy"), + resource.TestCheckResourceAttrPair(datasourceName, "policy_id", resourceName, "policy_id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "tags", resourceName, "tags"), ), }, }, }) +} + +func TestAccAWSDataSourceIAMPolicy_NonExistent(t *testing.T) { + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + policyPath := "/test-path/" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_NonExistent(policyName, policyPath), + ExpectError: regexp.MustCompile(`no IAM policy found matching criteria`), + }, + }, + }) } -func testAccAwsDataSourceIamPolicyConfig(policyName string) string { +func testAccAwsDataSourceIamPolicyBaseConfig(policyName, policyPath string) string { return fmt.Sprintf(` -resource "aws_iam_policy" "test_policy" { - name = "%s" - path = "/" +resource "aws_iam_policy" "test" { + name = %q + path = %q description = "My test policy" policy = < 0 { for _, ni := range instance.NetworkInterfaces { - if *ni.Attachment.DeviceIndex == 0 { + if aws.Int64Value(ni.Attachment.DeviceIndex) == 0 { d.Set("subnet_id", ni.SubnetId) d.Set("network_interface_id", ni.NetworkInterfaceId) d.Set("associate_public_ip_address", ni.Association != nil) @@ -477,6 +494,14 @@ func instanceDescriptionAttributes(d *schema.ResourceData, instance *ec2.Instanc if err := d.Set("secondary_private_ips", secondaryIPs); err != nil { return fmt.Errorf("error setting secondary_private_ips: %w", err) } + + ipV6Addresses := make([]string, 0, len(ni.Ipv6Addresses)) + for _, ip := range ni.Ipv6Addresses { + ipV6Addresses = append(ipV6Addresses, aws.StringValue(ip.Ipv6Address)) + } + if err := d.Set("ipv6_addresses", ipV6Addresses); err != nil { + return fmt.Errorf("error setting ipv6_addresses: %w", err) + } } } } else { @@ -485,12 +510,12 @@ func instanceDescriptionAttributes(d *schema.ResourceData, instance *ec2.Instanc } d.Set("ebs_optimized", instance.EbsOptimized) - if instance.SubnetId != nil && *instance.SubnetId != "" { + if aws.StringValue(instance.SubnetId) != "" { d.Set("source_dest_check", instance.SourceDestCheck) } - if instance.Monitoring != nil && instance.Monitoring.State != nil { - monitoringState := *instance.Monitoring.State + if instance.Monitoring != nil { + monitoringState := aws.StringValue(instance.Monitoring.State) d.Set("monitoring", monitoringState == "enabled" || monitoringState == "pending") } @@ -533,7 +558,7 @@ func instanceDescriptionAttributes(d *schema.ResourceData, instance *ec2.Instanc if attr != nil && attr.UserData != nil && attr.UserData.Value != nil { d.Set("user_data", userDataHashSum(aws.StringValue(attr.UserData.Value))) if d.Get("get_user_data").(bool) { - d.Set("user_data_base64", aws.StringValue(attr.UserData.Value)) + d.Set("user_data_base64", attr.UserData.Value) } } } diff --git a/aws/data_source_aws_instance_test.go b/aws/data_source_aws_instance_test.go index ae5831ebbce7..73d6deab63eb 100644 --- a/aws/data_source_aws_instance_test.go +++ b/aws/data_source_aws_instance_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccAWSInstanceDataSource_basic(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig, @@ -37,8 +39,9 @@ func TestAccAWSInstanceDataSource_tags(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_Tags(rInt), @@ -57,8 +60,9 @@ func TestAccAWSInstanceDataSource_AzUserData(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_AzUserData, @@ -79,8 +83,9 @@ func TestAccAWSInstanceDataSource_gp2IopsDevice(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_gp2IopsDevice, @@ -103,8 +108,9 @@ func TestAccAWSInstanceDataSource_gp3ThroughputDevice(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_gp3ThroughputDevice, @@ -127,8 +133,9 @@ func TestAccAWSInstanceDataSource_blockDevices(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_blockDevices, @@ -151,8 +158,9 @@ func TestAccAWSInstanceDataSource_blockDevices(t *testing.T) { // Test to verify that ebs_block_device kms_key_id does not elicit a panic func TestAccAWSInstanceDataSource_EbsBlockDevice_KmsKeyId(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_EbsBlockDevice_KmsKeyId, @@ -164,8 +172,9 @@ func TestAccAWSInstanceDataSource_EbsBlockDevice_KmsKeyId(t *testing.T) { // Test to verify that root_block_device kms_key_id does not elicit a panic func TestAccAWSInstanceDataSource_RootBlockDevice_KmsKeyId(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_RootBlockDevice_KmsKeyId, @@ -179,8 +188,9 @@ func TestAccAWSInstanceDataSource_rootInstanceStore(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_rootInstanceStore, @@ -202,8 +212,9 @@ func TestAccAWSInstanceDataSource_privateIP(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_privateIP(rName), @@ -223,8 +234,9 @@ func TestAccAWSInstanceDataSource_secondaryPrivateIPs(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_secondaryPrivateIPs(rName), @@ -238,17 +250,45 @@ func TestAccAWSInstanceDataSource_secondaryPrivateIPs(t *testing.T) { }) } +func TestAccAWSInstanceDataSource_ipv6Addresses(t *testing.T) { + resourceName := "aws_instance.test" + datasourceName := "data.aws_instance.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccInstanceDataSourceConfig_ipv6Addresses(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "ami", resourceName, "ami"), + resource.TestCheckResourceAttrPair(datasourceName, "instance_type", resourceName, "instance_type"), + resource.TestCheckResourceAttrPair(datasourceName, "ipv6_addresses.#", resourceName, "ipv6_address_count"), + ), + }, + }, + }) +} + func TestAccAWSInstanceDataSource_keyPair(t *testing.T) { resourceName := "aws_instance.test" datasourceName := "data.aws_instance.test" rName := fmt.Sprintf("tf-test-key-%d", acctest.RandInt()) + publicKey, _, err := acctest.RandSSHKeyPair(testAccDefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } + resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccInstanceDataSourceConfig_keyPair(rName), + Config: testAccInstanceDataSourceConfig_keyPair(rName, publicKey), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "ami", resourceName, "ami"), resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), @@ -266,8 +306,9 @@ func TestAccAWSInstanceDataSource_VPC(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_VPC(rName), @@ -289,8 +330,9 @@ func TestAccAWSInstanceDataSource_PlacementGroup(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_PlacementGroup(rName), @@ -308,8 +350,9 @@ func TestAccAWSInstanceDataSource_SecurityGroups(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_SecurityGroups(rInt), @@ -331,8 +374,9 @@ func TestAccAWSInstanceDataSource_VPCSecurityGroups(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_VPCSecurityGroups(rName), @@ -349,21 +393,27 @@ func TestAccAWSInstanceDataSource_VPCSecurityGroups(t *testing.T) { func TestAccAWSInstanceDataSource_getPasswordData_trueToFalse(t *testing.T) { datasourceName := "data.aws_instance.test" + rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) + publicKey, _, err := acctest.RandSSHKeyPair(testAccDefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccInstanceDataSourceConfig_getPasswordData(rName, true), + Config: testAccInstanceDataSourceConfig_getPasswordData(rName, publicKey, true), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(datasourceName, "get_password_data", "true"), resource.TestCheckResourceAttrSet(datasourceName, "password_data"), ), }, { - Config: testAccInstanceDataSourceConfig_getPasswordData(rName, false), + Config: testAccInstanceDataSourceConfig_getPasswordData(rName, publicKey, false), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(datasourceName, "get_password_data", "false"), resource.TestCheckNoResourceAttr(datasourceName, "password_data"), @@ -375,21 +425,27 @@ func TestAccAWSInstanceDataSource_getPasswordData_trueToFalse(t *testing.T) { func TestAccAWSInstanceDataSource_getPasswordData_falseToTrue(t *testing.T) { datasourceName := "data.aws_instance.test" + rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) + publicKey, _, err := acctest.RandSSHKeyPair(testAccDefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccInstanceDataSourceConfig_getPasswordData(rName, false), + Config: testAccInstanceDataSourceConfig_getPasswordData(rName, publicKey, false), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(datasourceName, "get_password_data", "false"), resource.TestCheckNoResourceAttr(datasourceName, "password_data"), ), }, { - Config: testAccInstanceDataSourceConfig_getPasswordData(rName, true), + Config: testAccInstanceDataSourceConfig_getPasswordData(rName, publicKey, true), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(datasourceName, "get_password_data", "true"), resource.TestCheckResourceAttrSet(datasourceName, "password_data"), @@ -404,8 +460,9 @@ func TestAccAWSInstanceDataSource_GetUserData(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfigGetUserData(rName, true), @@ -438,8 +495,9 @@ func TestAccAWSInstanceDataSource_GetUserData_NoUserData(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfigGetUserDataNoUserData(rName, true), @@ -475,8 +533,9 @@ func TestAccAWSInstanceDataSource_creditSpecification(t *testing.T) { rName := fmt.Sprintf("tf-testacc-instance-%s", acctest.RandString(12)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { @@ -497,8 +556,9 @@ func TestAccAWSInstanceDataSource_metadataOptions(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_metadataOptions(rName), @@ -519,8 +579,9 @@ func TestAccAWSInstanceDataSource_enclaveOptions(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_enclaveOptions(rName), @@ -539,8 +600,9 @@ func TestAccAWSInstanceDataSource_blockDeviceTags(t *testing.T) { datasourceName := "data.aws_instance.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstanceDataSourceConfig_blockDeviceTags(rName), @@ -786,11 +848,30 @@ data "aws_instance" "test" { `) } -func testAccInstanceDataSourceConfig_keyPair(rName string) string { - return testAccLatestAmazonLinuxHvmEbsAmiConfig() + fmt.Sprintf(` +func testAccInstanceDataSourceConfig_ipv6Addresses(rName string) string { + return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), testAccAwsInstanceVpcIpv6Config(rName), fmt.Sprintf(` +resource "aws_instance" "test" { + ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = "t2.micro" + subnet_id = aws_subnet.test.id + ipv6_address_count = 1 + + tags = { + Name = %[1]q + } +} + +data "aws_instance" "test" { + instance_id = aws_instance.test.id +} +`, rName)) +} + +func testAccInstanceDataSourceConfig_keyPair(rName, publicKey string) string { + return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), fmt.Sprintf(` resource "aws_key_pair" "test" { key_name = %[1]q - public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com" + public_key = %[2]q } resource "aws_instance" "test" { @@ -814,7 +895,7 @@ data "aws_instance" "test" { values = [aws_instance.test.key_name] } } -`, rName) +`, rName, publicKey)) } func testAccInstanceDataSourceConfig_VPC(rName string) string { @@ -906,11 +987,13 @@ data "aws_instance" "test" { `) } -func testAccInstanceDataSourceConfig_getPasswordData(rName string, val bool) string { - return testAccLatestWindowsServer2016CoreAmiConfig() + fmt.Sprintf(` +func testAccInstanceDataSourceConfig_getPasswordData(rName, publicKey string, val bool) string { + return composeConfig( + testAccLatestWindowsServer2016CoreAmiConfig(), + fmt.Sprintf(` resource "aws_key_pair" "test" { key_name = %[1]q - public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAq6U3HQYC4g8WzU147gZZ7CKQH8TgYn3chZGRPxaGmHW1RUwsyEs0nmombmIhwxudhJ4ehjqXsDLoQpd6+c7BuLgTMvbv8LgE9LX53vnljFe1dsObsr/fYLvpU9LTlo8HgHAqO5ibNdrAUvV31ronzCZhms/Gyfdaue88Fd0/YnsZVGeOZPayRkdOHSpqme2CBrpa8myBeL1CWl0LkDG4+YCURjbaelfyZlIApLYKy3FcCan9XQFKaL32MJZwCgzfOvWIMtYcU8QtXMgnA3/I3gXk8YDUJv5P4lj0s/PJXuTM8DygVAUtebNwPuinS7wwonm5FXcWMuVGsVpG5K7FGQ== tf-acc-winpasswordtest" + public_key = %[2]q } resource "aws_instance" "test" { @@ -922,9 +1005,9 @@ resource "aws_instance" "test" { data "aws_instance" "test" { instance_id = aws_instance.test.id - get_password_data = %[2]t + get_password_data = %[3]t } -`, rName, val) +`, rName, publicKey, val)) } func testAccInstanceDataSourceConfigGetUserData(rName string, getUserData bool) string { diff --git a/aws/data_source_aws_instances.go b/aws/data_source_aws_instances.go index 60f1c1a8dbad..d0e96a885cc4 100644 --- a/aws/data_source_aws_instances.go +++ b/aws/data_source_aws_instances.go @@ -89,7 +89,7 @@ func dataSourceAwsInstancesRead(d *schema.ResourceData, meta interface{}) error log.Printf("[DEBUG] Reading EC2 instances: %s", params) var instanceIds, privateIps, publicIps []string - err := conn.DescribeInstancesPages(params, func(resp *ec2.DescribeInstancesOutput, isLast bool) bool { + err := conn.DescribeInstancesPages(params, func(resp *ec2.DescribeInstancesOutput, lastPage bool) bool { for _, res := range resp.Reservations { for _, instance := range res.Instances { instanceIds = append(instanceIds, *instance.InstanceId) @@ -101,7 +101,7 @@ func dataSourceAwsInstancesRead(d *schema.ResourceData, meta interface{}) error } } } - return !isLast + return !lastPage }) if err != nil { return err diff --git a/aws/data_source_aws_instances_test.go b/aws/data_source_aws_instances_test.go index ac7940e27d80..6f0197eae64c 100644 --- a/aws/data_source_aws_instances_test.go +++ b/aws/data_source_aws_instances_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSInstancesDataSource_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstancesDataSourceConfig_ids(rName), @@ -32,8 +34,9 @@ func TestAccAWSInstancesDataSource_tags(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstancesDataSourceConfig_tags(rName), @@ -49,8 +52,9 @@ func TestAccAWSInstancesDataSource_instanceStateNames(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccInstancesDataSourceConfig_instanceStateNames(rName), diff --git a/aws/data_source_aws_internet_gateway.go b/aws/data_source_aws_internet_gateway.go index 3f0f5b3fc7e5..92237dc9f331 100644 --- a/aws/data_source_aws_internet_gateway.go +++ b/aws/data_source_aws_internet_gateway.go @@ -98,9 +98,9 @@ func dataSourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, - AccountID: meta.(*AWSClient).accountid, + AccountID: aws.StringValue(igw.OwnerId), Resource: fmt.Sprintf("internet-gateway/%s", d.Id()), }.String() diff --git a/aws/data_source_aws_internet_gateway_test.go b/aws/data_source_aws_internet_gateway_test.go index ad115a7e970c..ff909b6caaf6 100644 --- a/aws/data_source_aws_internet_gateway_test.go +++ b/aws/data_source_aws_internet_gateway_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsInternetGateway_typical(t *testing.T) { ds3ResourceName := "data.aws_internet_gateway.by_tags" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsInternetGatewayConfig, diff --git a/aws/data_source_aws_iot_endpoint_test.go b/aws/data_source_aws_iot_endpoint_test.go index e28dd814f514..d19d9ed8b52a 100644 --- a/aws/data_source_aws_iot_endpoint_test.go +++ b/aws/data_source_aws_iot_endpoint_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/iot" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSIotEndpointDataSource_basic(t *testing.T) { dataSourceName := "data.aws_iot_endpoint.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iot.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIotEndpointConfig, @@ -29,8 +31,9 @@ func TestAccAWSIotEndpointDataSource_EndpointType_IOTCredentialProvider(t *testi dataSourceName := "data.aws_iot_endpoint.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iot.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIotEndpointConfigEndpointType("iot:CredentialProvider"), @@ -46,8 +49,9 @@ func TestAccAWSIotEndpointDataSource_EndpointType_IOTData(t *testing.T) { dataSourceName := "data.aws_iot_endpoint.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iot.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIotEndpointConfigEndpointType("iot:Data"), @@ -63,8 +67,9 @@ func TestAccAWSIotEndpointDataSource_EndpointType_IOTDataATS(t *testing.T) { dataSourceName := "data.aws_iot_endpoint.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iot.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIotEndpointConfigEndpointType("iot:Data-ATS"), @@ -80,8 +85,9 @@ func TestAccAWSIotEndpointDataSource_EndpointType_IOTJobs(t *testing.T) { dataSourceName := "data.aws_iot_endpoint.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iot.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIotEndpointConfigEndpointType("iot:Jobs"), diff --git a/aws/data_source_aws_ip_ranges.go b/aws/data_source_aws_ip_ranges.go index 07ef06083c7d..1f64e50e8dd1 100644 --- a/aws/data_source_aws_ip_ranges.go +++ b/aws/data_source_aws_ip_ranges.go @@ -3,7 +3,7 @@ package aws import ( "encoding/json" "fmt" - "io/ioutil" + "io" "log" "sort" "strconv" @@ -89,7 +89,7 @@ func dataSourceAwsIPRangesRead(d *schema.ResourceData, meta interface{}) error { defer res.Body.Close() - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("Error reading response body from (%s): %w", url, err) diff --git a/aws/data_source_aws_ip_ranges_test.go b/aws/data_source_aws_ip_ranges_test.go index c8de3a0d9157..9cc095ec9e5c 100644 --- a/aws/data_source_aws_ip_ranges_test.go +++ b/aws/data_source_aws_ip_ranges_test.go @@ -15,8 +15,9 @@ import ( func TestAccAWSIPRanges_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIPRangesConfig, @@ -32,8 +33,9 @@ func TestAccAWSIPRanges_basic(t *testing.T) { func TestAccAWSIPRanges_Url(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSIPRangesConfigUrl, diff --git a/aws/data_source_aws_kinesis_firehose_delivery_stream.go b/aws/data_source_aws_kinesis_firehose_delivery_stream.go new file mode 100644 index 000000000000..ec7e675840fa --- /dev/null +++ b/aws/data_source_aws_kinesis_firehose_delivery_stream.go @@ -0,0 +1,42 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/firehose/finder" +) + +func dataSourceAwsKinesisFirehoseDeliveryStream() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsKinesisFirehoseDeliveryStreamRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func dataSourceAwsKinesisFirehoseDeliveryStreamRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).firehoseconn + + sn := d.Get("name").(string) + output, err := finder.DeliveryStreamByName(conn, sn) + + if err != nil { + return fmt.Errorf("error reading Kinesis Firehose Delivery Stream (%s): %w", sn, err) + } + + d.SetId(aws.StringValue(output.DeliveryStreamARN)) + d.Set("arn", output.DeliveryStreamARN) + d.Set("name", output.DeliveryStreamName) + + return nil +} diff --git a/aws/data_source_aws_kinesis_firehose_delivery_stream_test.go b/aws/data_source_aws_kinesis_firehose_delivery_stream_test.go new file mode 100644 index 000000000000..be40fd884f29 --- /dev/null +++ b/aws/data_source_aws_kinesis_firehose_delivery_stream_test.go @@ -0,0 +1,115 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/firehose" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsKinesisFirehoseDeliveryStream_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_kinesis_firehose_delivery_stream.test" + resourceName := "aws_kinesis_firehose_delivery_stream.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, firehose.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckKinesisFirehoseDeliveryStreamDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsKinesisFirehoseDeliveryStreamConfigBasic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttr(dataSourceName, "name", rName), + ), + }, + }, + }) +} + +func testAccDataSourceAwsKinesisFirehoseDeliveryStreamConfigBasic(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "firehose" { + name = %[1]q + + assume_role_policy = < 1 { + return fmt.Errorf("multiple Kinesis Stream Consumers found matching criteria; try different search") + } + + consumer := results[0] + + d.SetId(aws.StringValue(consumer.ConsumerARN)) + d.Set("arn", consumer.ConsumerARN) + d.Set("name", consumer.ConsumerName) + d.Set("status", consumer.ConsumerStatus) + d.Set("stream_arn", streamArn) + d.Set("creation_timestamp", aws.TimeValue(consumer.ConsumerCreationTimestamp).Format(time.RFC3339)) + + return nil +} diff --git a/aws/data_source_aws_kinesis_stream_consumer_test.go b/aws/data_source_aws_kinesis_stream_consumer_test.go new file mode 100644 index 000000000000..6a4f18f88a44 --- /dev/null +++ b/aws/data_source_aws_kinesis_stream_consumer_test.go @@ -0,0 +1,144 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSKinesisStreamConsumerDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_kinesis_stream_consumer.test" + resourceName := "aws_kinesis_stream_consumer.test" + streamName := "aws_kinesis_stream.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kinesis.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSKinesisStreamConsumerDataSourceConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "stream_arn", streamName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "creation_timestamp"), + resource.TestCheckResourceAttrSet(dataSourceName, "status"), + ), + }, + }, + }) +} + +func TestAccAWSKinesisStreamConsumerDataSource_Name(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_kinesis_stream_consumer.test" + resourceName := "aws_kinesis_stream_consumer.test" + streamName := "aws_kinesis_stream.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kinesis.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSKinesisStreamConsumerDataSourceConfigName(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "stream_arn", streamName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "creation_timestamp"), + resource.TestCheckResourceAttrSet(dataSourceName, "status"), + ), + }, + }, + }) +} + +func TestAccAWSKinesisStreamConsumerDataSource_Arn(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_kinesis_stream_consumer.test" + resourceName := "aws_kinesis_stream_consumer.test" + streamName := "aws_kinesis_stream.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kinesis.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSKinesisStreamConsumerDataSourceConfigArn(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "stream_arn", streamName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "creation_timestamp"), + resource.TestCheckResourceAttrSet(dataSourceName, "status"), + ), + }, + }, + }) +} + +func testAccAWSKinesisStreamConsumerDataSourceBaseConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_kinesis_stream" "test" { + name = %q + shard_count = 2 +} +`, rName) +} + +func testAccAWSKinesisStreamConsumerDataSourceConfig(rName string) string { + return composeConfig( + testAccAWSKinesisStreamConsumerDataSourceBaseConfig(rName), + fmt.Sprintf(` +data "aws_kinesis_stream_consumer" "test" { + stream_arn = aws_kinesis_stream_consumer.test.stream_arn +} + +resource "aws_kinesis_stream_consumer" "test" { + name = %q + stream_arn = aws_kinesis_stream.test.arn +} +`, rName)) +} + +func testAccAWSKinesisStreamConsumerDataSourceConfigName(rName string) string { + return composeConfig( + testAccAWSKinesisStreamConsumerDataSourceBaseConfig(rName), + fmt.Sprintf(` +data "aws_kinesis_stream_consumer" "test" { + name = aws_kinesis_stream_consumer.test.name + stream_arn = aws_kinesis_stream_consumer.test.stream_arn +} + +resource "aws_kinesis_stream_consumer" "test" { + name = %q + stream_arn = aws_kinesis_stream.test.arn +} +`, rName)) +} + +func testAccAWSKinesisStreamConsumerDataSourceConfigArn(rName string) string { + return composeConfig( + testAccAWSKinesisStreamConsumerDataSourceBaseConfig(rName), + fmt.Sprintf(` +data "aws_kinesis_stream_consumer" "test" { + arn = aws_kinesis_stream_consumer.test.arn + stream_arn = aws_kinesis_stream_consumer.test.stream_arn +} + +resource "aws_kinesis_stream_consumer" "test" { + name = %q + stream_arn = aws_kinesis_stream.test.arn +} +`, rName)) +} diff --git a/aws/data_source_aws_kinesis_stream_test.go b/aws/data_source_aws_kinesis_stream_test.go index cf4e22a71696..11123d959d5f 100644 --- a/aws/data_source_aws_kinesis_stream_test.go +++ b/aws/data_source_aws_kinesis_stream_test.go @@ -34,6 +34,7 @@ func TestAccAWSKinesisStreamDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kinesis.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckKinesisStreamDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_kms_alias.go b/aws/data_source_aws_kms_alias.go index 1c5c84e56879..21ee79abd004 100644 --- a/aws/data_source_aws_kms_alias.go +++ b/aws/data_source_aws_kms_alias.go @@ -2,26 +2,25 @@ package aws import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/kms" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kms/finder" ) func dataSourceAwsKmsAlias() *schema.Resource { return &schema.Resource{ Read: dataSourceAwsKmsAliasRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "name": { Type: schema.TypeString, Required: true, ValidateFunc: validateAwsKmsName, }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, "target_key_arn": { Type: schema.TypeString, Computed: true, @@ -36,27 +35,13 @@ func dataSourceAwsKmsAlias() *schema.Resource { func dataSourceAwsKmsAliasRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).kmsconn - params := &kms.ListAliasesInput{} - target := d.Get("name") - var alias *kms.AliasListEntry - log.Printf("[DEBUG] Reading KMS Alias: %s", params) - err := conn.ListAliasesPages(params, func(page *kms.ListAliasesOutput, lastPage bool) bool { - for _, entity := range page.Aliases { - if *entity.AliasName == target { - alias = entity - return false - } - } + target := d.Get("name").(string) - return true - }) - if err != nil { - return fmt.Errorf("Error fetch KMS alias list: %w", err) - } + alias, err := finder.AliasByName(conn, target) - if alias == nil { - return fmt.Errorf("No alias with name %q found in this region.", target) + if err != nil { + return fmt.Errorf("error reading KMS Alias (%s): %w", target, err) } d.SetId(aws.StringValue(alias.AliasArn)) @@ -73,16 +58,14 @@ func dataSourceAwsKmsAliasRead(d *schema.ResourceData, meta interface{}) error { // // https://docs.aws.amazon.com/kms/latest/APIReference/API_ListAliases.html - req := &kms.DescribeKeyInput{ - KeyId: aws.String(target.(string)), - } - resp, err := conn.DescribeKey(req) + keyMetadata, err := finder.KeyByID(conn, target) + if err != nil { - return fmt.Errorf("Error calling KMS DescribeKey: %w", err) + return fmt.Errorf("error reading KMS Key (%s): %w", target, err) } - d.Set("target_key_arn", resp.KeyMetadata.Arn) - d.Set("target_key_id", resp.KeyMetadata.KeyId) + d.Set("target_key_arn", keyMetadata.Arn) + d.Set("target_key_id", keyMetadata.KeyId) return nil } diff --git a/aws/data_source_aws_kms_alias_test.go b/aws/data_source_aws_kms_alias_test.go index 0964dfe5e643..d35745474e96 100644 --- a/aws/data_source_aws_kms_alias_test.go +++ b/aws/data_source_aws_kms_alias_test.go @@ -5,25 +5,25 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/kms" "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" ) func TestAccDataSourceAwsKmsAlias_AwsService(t *testing.T) { - name := "alias/aws/s3" + rName := "alias/aws/s3" resourceName := "data.aws_kms_alias.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsKmsAlias_name(name), + Config: testAccDataSourceAwsKmsAliasConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccDataSourceAwsKmsAliasCheckExists(resourceName), - testAccCheckResourceAttrRegionalARN(resourceName, "arn", "kms", name), - resource.TestCheckResourceAttr(resourceName, "name", name), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "kms", rName), + resource.TestCheckResourceAttr(resourceName, "name", rName), testAccMatchResourceAttrRegionalARN(resourceName, "target_key_arn", "kms", regexp.MustCompile(`key/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}`)), resource.TestMatchResourceAttr(resourceName, "target_key_id", regexp.MustCompile("^[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}$")), ), @@ -33,18 +33,18 @@ func TestAccDataSourceAwsKmsAlias_AwsService(t *testing.T) { } func TestAccDataSourceAwsKmsAlias_CMK(t *testing.T) { - rInt := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") aliasResourceName := "aws_kms_alias.test" datasourceAliasResourceName := "data.aws_kms_alias.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsKmsAlias_CMK(rInt), + Config: testAccDataSourceAwsKmsAliasConfigCMK(rName), Check: resource.ComposeTestCheckFunc( - testAccDataSourceAwsKmsAliasCheckExists(datasourceAliasResourceName), resource.TestCheckResourceAttrPair(datasourceAliasResourceName, "arn", aliasResourceName, "arn"), resource.TestCheckResourceAttrPair(datasourceAliasResourceName, "target_key_arn", aliasResourceName, "target_key_arn"), resource.TestCheckResourceAttrPair(datasourceAliasResourceName, "target_key_id", aliasResourceName, "target_key_id"), @@ -54,37 +54,28 @@ func TestAccDataSourceAwsKmsAlias_CMK(t *testing.T) { }) } -func testAccDataSourceAwsKmsAliasCheckExists(name string) resource.TestCheckFunc { - return func(s *terraform.State) error { - _, ok := s.RootModule().Resources[name] - if !ok { - return fmt.Errorf("root module has no resource called %s", name) - } - - return nil - } -} - -func testAccDataSourceAwsKmsAlias_name(name string) string { +func testAccDataSourceAwsKmsAliasConfig(rName string) string { return fmt.Sprintf(` data "aws_kms_alias" "test" { - name = "%s" + name = %[1]q } -`, name) +`, rName) } -func testAccDataSourceAwsKmsAlias_CMK(rInt int) string { +func testAccDataSourceAwsKmsAliasConfigCMK(rName string) string { return fmt.Sprintf(` resource "aws_kms_key" "test" { - description = "Terraform acc test" + description = %[1]q deletion_window_in_days = 7 } resource "aws_kms_alias" "test" { - name = "alias/tf-acc-key-alias-%d" + name = "alias/%[1]s" target_key_id = aws_kms_key.test.key_id } -%s -`, rInt, testAccDataSourceAwsKmsAlias_name("${aws_kms_alias.test.name}")) +data "aws_kms_alias" "test" { + name = aws_kms_alias.test.name +} +`, rName) } diff --git a/aws/data_source_aws_kms_ciphertext.go b/aws/data_source_aws_kms_ciphertext.go index 19fef6cb310f..7919a2a6a2ba 100644 --- a/aws/data_source_aws_kms_ciphertext.go +++ b/aws/data_source_aws_kms_ciphertext.go @@ -48,7 +48,7 @@ func dataSourceAwsKmsCiphertextRead(d *schema.ResourceData, meta interface{}) er } if ec := d.Get("context"); ec != nil { - req.EncryptionContext = stringMapToPointers(ec.(map[string]interface{})) + req.EncryptionContext = expandStringMap(ec.(map[string]interface{})) } log.Printf("[DEBUG] KMS encrypt for key: %s", d.Get("key_id").(string)) diff --git a/aws/data_source_aws_kms_ciphertext_test.go b/aws/data_source_aws_kms_ciphertext_test.go index b29aea85ac48..66395e475910 100644 --- a/aws/data_source_aws_kms_ciphertext_test.go +++ b/aws/data_source_aws_kms_ciphertext_test.go @@ -3,13 +3,15 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/kms" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsKmsCiphertext_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsKmsCiphertextConfig_basic, @@ -24,8 +26,9 @@ func TestAccDataSourceAwsKmsCiphertext_basic(t *testing.T) { func TestAccDataSourceAwsKmsCiphertext_validate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsKmsCiphertextConfig_validate, @@ -40,8 +43,9 @@ func TestAccDataSourceAwsKmsCiphertext_validate(t *testing.T) { func TestAccDataSourceAwsKmsCiphertext_validate_withContext(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsKmsCiphertextConfig_validate_withContext, diff --git a/aws/data_source_aws_kms_key_test.go b/aws/data_source_aws_kms_key_test.go index 4f374ed25a74..63249d6d5f28 100644 --- a/aws/data_source_aws_kms_key_test.go +++ b/aws/data_source_aws_kms_key_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/kms" "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" @@ -15,8 +16,9 @@ func TestAccDataSourceAwsKmsKey_basic(t *testing.T) { rName := fmt.Sprintf("tf-testacc-kms-key-%s", acctest.RandString(13)) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsKmsKeyConfig(rName), diff --git a/aws/data_source_aws_kms_public_key.go b/aws/data_source_aws_kms_public_key.go new file mode 100644 index 000000000000..13d5391df68f --- /dev/null +++ b/aws/data_source_aws_kms_public_key.go @@ -0,0 +1,90 @@ +package aws + +import ( + "encoding/base64" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsKmsPublicKey() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsKmsPublicKeyRead, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "customer_master_key_spec": { + Type: schema.TypeString, + Computed: true, + }, + "encryption_algorithms": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "grant_tokens": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "key_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateKmsKey, + }, + "key_usage": { + Type: schema.TypeString, + Computed: true, + }, + "public_key": { + Type: schema.TypeString, + Computed: true, + }, + "signing_algorithms": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsKmsPublicKeyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kmsconn + keyId := d.Get("key_id").(string) + + input := &kms.GetPublicKeyInput{ + KeyId: aws.String(keyId), + } + + if v, ok := d.GetOk("grant_tokens"); ok { + input.GrantTokens = aws.StringSlice(v.([]string)) + } + + output, err := conn.GetPublicKey(input) + + if err != nil { + return fmt.Errorf("error while describing KMS public key (%s): %w", keyId, err) + } + + d.SetId(aws.StringValue(output.KeyId)) + + d.Set("arn", output.KeyId) + d.Set("customer_master_key_spec", output.CustomerMasterKeySpec) + d.Set("key_usage", output.KeyUsage) + d.Set("public_key", base64.StdEncoding.EncodeToString(output.PublicKey)) + + if err := d.Set("encryption_algorithms", flattenStringList(output.EncryptionAlgorithms)); err != nil { + return fmt.Errorf("error setting encryption_algorithms: %w", err) + } + + if err := d.Set("signing_algorithms", flattenStringList(output.SigningAlgorithms)); err != nil { + return fmt.Errorf("error setting signing_algorithms: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_kms_public_key_test.go b/aws/data_source_aws_kms_public_key_test.go new file mode 100644 index 000000000000..8a5879c575a1 --- /dev/null +++ b/aws/data_source_aws_kms_public_key_test.go @@ -0,0 +1,102 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/kms" + "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" +) + +func TestAccDataSourceAwsKmsPublicKey_basic(t *testing.T) { + resourceName := "aws_kms_key.test" + datasourceName := "data.aws_kms_public_key.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsKmsPublicKeyConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsKmsPublicKeyCheck(datasourceName), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "customer_master_key_spec", resourceName, "customer_master_key_spec"), + resource.TestCheckResourceAttrPair(datasourceName, "key_id", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "key_usage", resourceName, "key_usage"), + resource.TestCheckResourceAttrSet(datasourceName, "public_key"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsKmsPublicKey_encrypt(t *testing.T) { + resourceName := "aws_kms_key.test" + datasourceName := "data.aws_kms_public_key.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsKmsPublicKeyEncryptConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsKmsPublicKeyCheck(datasourceName), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "key_id", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "customer_master_key_spec", resourceName, "customer_master_key_spec"), + resource.TestCheckResourceAttrPair(datasourceName, "key_usage", resourceName, "key_usage"), + resource.TestCheckResourceAttrSet(datasourceName, "public_key"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsKmsPublicKeyCheck(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("root module has no resource called %s", name) + } + + return nil + } +} + +func testAccDataSourceAwsKmsPublicKeyConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 + customer_master_key_spec = "RSA_2048" + key_usage = "SIGN_VERIFY" +} + +data "aws_kms_public_key" "test" { + key_id = aws_kms_key.test.arn +} +`, rName) +} + +func testAccDataSourceAwsKmsPublicKeyEncryptConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 + customer_master_key_spec = "RSA_2048" + key_usage = "ENCRYPT_DECRYPT" +} + +data "aws_kms_public_key" "test" { + key_id = aws_kms_key.test.arn +} +`, rName) +} diff --git a/aws/data_source_aws_kms_secret_test.go b/aws/data_source_aws_kms_secret_test.go index 7b4d37ef0ebe..99fae3e31016 100644 --- a/aws/data_source_aws_kms_secret_test.go +++ b/aws/data_source_aws_kms_secret_test.go @@ -4,13 +4,15 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/kms" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccAWSKmsSecretDataSource_removed(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsKmsSecretDataSourceConfig, diff --git a/aws/data_source_aws_kms_secrets_test.go b/aws/data_source_aws_kms_secrets_test.go index c6e6cae242d1..e74d54179315 100644 --- a/aws/data_source_aws_kms_secrets_test.go +++ b/aws/data_source_aws_kms_secrets_test.go @@ -20,8 +20,9 @@ func TestAccAWSKmsSecretsDataSource_basic(t *testing.T) { // Run a resource test to setup our KMS key resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsKmsSecretsDataSourceKey, @@ -64,8 +65,9 @@ func testAccDataSourceAwsKmsSecretsDecrypt(t *testing.T, plaintext string, encry dataSourceName := "data.aws_kms_secrets.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kms.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsKmsSecretsDataSourceSecret(*encryptedPayload), diff --git a/aws/data_source_aws_lakeformation_data_lake_settings_test.go b/aws/data_source_aws_lakeformation_data_lake_settings_test.go index e0ae83a99b49..09b424fe346a 100644 --- a/aws/data_source_aws_lakeformation_data_lake_settings_test.go +++ b/aws/data_source_aws_lakeformation_data_lake_settings_test.go @@ -8,20 +8,20 @@ import ( ) func testAccAWSLakeFormationDataLakeSettingsDataSource_basic(t *testing.T) { - callerIdentityName := "data.aws_caller_identity.current" resourceName := "data.aws_lakeformation_data_lake_settings.test" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationDataLakeSettingsDestroy, Steps: []resource.TestStep{ { Config: testAccAWSLakeFormationDataLakeSettingsDataSourceConfig_basic, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair(resourceName, "catalog_id", callerIdentityName, "account_id"), + resource.TestCheckResourceAttrPair(resourceName, "catalog_id", "data.aws_caller_identity.current", "account_id"), resource.TestCheckResourceAttr(resourceName, "admins.#", "1"), - resource.TestCheckResourceAttrPair(resourceName, "admins.0", callerIdentityName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "admins.0", "data.aws_iam_session_context.current", "issuer_arn"), ), }, }, @@ -31,9 +31,13 @@ func testAccAWSLakeFormationDataLakeSettingsDataSource_basic(t *testing.T) { const testAccAWSLakeFormationDataLakeSettingsDataSourceConfig_basic = ` data "aws_caller_identity" "current" {} +data "aws_iam_session_context" "current" { + arn = data.aws_caller_identity.current.arn +} + resource "aws_lakeformation_data_lake_settings" "test" { catalog_id = data.aws_caller_identity.current.account_id - admins = [data.aws_caller_identity.current.arn] + admins = [data.aws_iam_session_context.current.issuer_arn] } data "aws_lakeformation_data_lake_settings" "test" { diff --git a/aws/data_source_aws_lakeformation_permissions.go b/aws/data_source_aws_lakeformation_permissions.go index 71fb02d87215..97963b217158 100644 --- a/aws/data_source_aws_lakeformation_permissions.go +++ b/aws/data_source_aws_lakeformation_permissions.go @@ -3,15 +3,14 @@ package aws import ( "fmt" "log" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lakeformation" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "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/terraform-providers/terraform-provider-aws/aws/internal/hashcode" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation/waiter" ) func dataSourceAwsLakeFormationPermissions() *schema.Resource { @@ -133,8 +132,9 @@ func dataSourceAwsLakeFormationPermissions() *schema.Resource { ValidateFunc: validateAwsAccountId, }, "column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -145,8 +145,9 @@ func dataSourceAwsLakeFormationPermissions() *schema.Resource { Required: true, }, "excluded_column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -156,6 +157,10 @@ func dataSourceAwsLakeFormationPermissions() *schema.Resource { Type: schema.TypeString, Required: true, }, + "wildcard": { + Type: schema.TypeBool, + Optional: true, + }, }, }, }, @@ -170,102 +175,152 @@ func dataSourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta inte Principal: &lakeformation.DataLakePrincipal{ DataLakePrincipalIdentifier: aws.String(d.Get("principal").(string)), }, + Resource: &lakeformation.Resource{}, } if v, ok := d.GetOk("catalog_id"); ok { input.CatalogId = aws.String(v.(string)) } - input.Resource = expandLakeFormationResource(d, true) - matchResource := expandLakeFormationResource(d, false) + if _, ok := d.GetOk("catalog_resource"); ok { + input.Resource.Catalog = expandLakeFormationCatalogResource() + } - log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) - var principalResourcePermissions []*lakeformation.PrincipalResourcePermissions + if v, ok := d.GetOk("data_location"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Resource.DataLocation = expandLakeFormationDataLocationResource(v.([]interface{})[0].(map[string]interface{})) + } - err := resource.Retry(2*time.Minute, func() *resource.RetryError { - err := conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } + if v, ok := d.GetOk("database"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Resource.Database = expandLakeFormationDatabaseResource(v.([]interface{})[0].(map[string]interface{})) + } - if resourceAwsLakeFormationPermissionsCompareResource(*matchResource, *permission.Resource) { - principalResourcePermissions = append(principalResourcePermissions, permission) - } - } - return !lastPage - }) + tableType := "" + + if v, ok := d.GetOk("table"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Resource.Table = expandLakeFormationTableResource(v.([]interface{})[0].(map[string]interface{})) + tableType = tflakeformation.TableTypeTable + } + + if v, ok := d.GetOk("table_with_columns"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + // can't ListPermissions for TableWithColumns, so use Table instead + input.Resource.Table = expandLakeFormationTableWithColumnsResourceAsTable(v.([]interface{})[0].(map[string]interface{})) + tableType = tflakeformation.TableTypeTableWithColumns + } + + columnNames := make([]*string, 0) + excludedColumnNames := make([]*string, 0) + columnWildcard := false + + if tableType == tflakeformation.TableTypeTableWithColumns { + if v, ok := d.GetOk("table_with_columns.0.wildcard"); ok { + columnWildcard = v.(bool) + } - if err != nil { - if isAWSErr(err, lakeformation.ErrCodeInvalidInputException, "Invalid principal") { - return resource.RetryableError(err) + if v, ok := d.GetOk("table_with_columns.0.column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + columnNames = expandStringSet(v) } - return resource.NonRetryableError(fmt.Errorf("error reading Lake Formation Permissions: %w", err)) } - return nil - }) - - if isResourceTimeoutError(err) { - err = conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } - if resourceAwsLakeFormationPermissionsCompareResource(*matchResource, *permission.Resource) { - principalResourcePermissions = append(principalResourcePermissions, permission) - } + if v, ok := d.GetOk("table_with_columns.0.excluded_column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + excludedColumnNames = expandStringSet(v) } - return !lastPage - }) + } } - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, lakeformation.ErrCodeEntityNotFoundException) { - log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } + log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) + + allPermissions, err := waiter.PermissionsReady(conn, input, tableType, columnNames, excludedColumnNames, columnWildcard) + + d.SetId(fmt.Sprintf("%d", hashcode.String(input.String()))) if err != nil { return fmt.Errorf("error reading Lake Formation permissions: %w", err) } - if len(principalResourcePermissions) > 1 { - return fmt.Errorf("error reading Lake Formation permissions: %s", "multiple permissions found") + // clean permissions = filter out permissions that do not pertain to this specific resource + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, allPermissions) + + if len(cleanPermissions) != len(allPermissions) { + log.Printf("[INFO] Resource Lake Formation clean permissions (%d) and all permissions (%d) have different lengths (this is not necessarily a problem): %s", len(cleanPermissions), len(allPermissions), d.Id()) } - d.SetId(fmt.Sprintf("%d", hashcode.String(input.String()))) - for _, permissions := range principalResourcePermissions { - d.Set("principal", permissions.Principal.DataLakePrincipalIdentifier) - d.Set("permissions", permissions.Permissions) - d.Set("permissions_with_grant_option", permissions.PermissionsWithGrantOption) + d.Set("principal", cleanPermissions[0].Principal.DataLakePrincipalIdentifier) + d.Set("permissions", flattenLakeFormationPermissions(cleanPermissions)) + d.Set("permissions_with_grant_option", flattenLakeFormationGrantPermissions(cleanPermissions)) + + if cleanPermissions[0].Resource.Catalog != nil { + d.Set("catalog_resource", true) + } else { + d.Set("catalog_resource", false) + } - if permissions.Resource.Catalog != nil { - d.Set("catalog_resource", true) + if cleanPermissions[0].Resource.DataLocation != nil { + if err := d.Set("data_location", []interface{}{flattenLakeFormationDataLocationResource(cleanPermissions[0].Resource.DataLocation)}); err != nil { + return fmt.Errorf("error setting data_location: %w", err) } + } else { + d.Set("data_location", nil) + } - if permissions.Resource.DataLocation != nil { - d.Set("data_location", []interface{}{flattenLakeFormationDataLocationResource(permissions.Resource.DataLocation)}) - } else { - d.Set("data_location", nil) + if cleanPermissions[0].Resource.Database != nil { + if err := d.Set("database", []interface{}{flattenLakeFormationDatabaseResource(cleanPermissions[0].Resource.Database)}); err != nil { + return fmt.Errorf("error setting database: %w", err) } + } else { + d.Set("database", nil) + } + + tableSet := false + + if v, ok := d.GetOk("table"); ok && len(v.([]interface{})) > 0 { + // since perm list could include TableWithColumns, get the right one + for _, perm := range cleanPermissions { + if perm.Resource == nil { + continue + } + + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil { + if err := d.Set("table", []interface{}{flattenLakeFormationTableWithColumnsResourceAsTable(perm.Resource.TableWithColumns)}); err != nil { + return fmt.Errorf("error setting table: %w", err) + } + tableSet = true + break + } - if permissions.Resource.Database != nil { - d.Set("database", []interface{}{flattenLakeFormationDatabaseResource(permissions.Resource.Database)}) - } else { - d.Set("database", nil) + if perm.Resource.Table != nil { + if err := d.Set("table", []interface{}{flattenLakeFormationTableResource(perm.Resource.Table)}); err != nil { + return fmt.Errorf("error setting table: %w", err) + } + tableSet = true + break + } } + } + + if !tableSet { + d.Set("table", nil) + } + + twcSet := false - // table with columns permissions will include the table and table with columns - if permissions.Resource.TableWithColumns != nil { - d.Set("table_with_columns", []interface{}{flattenLakeFormationTableWithColumnsResource(permissions.Resource.TableWithColumns)}) - } else if permissions.Resource.Table != nil { - d.Set("table_with_columns", nil) - d.Set("table", []interface{}{flattenLakeFormationTableResource(permissions.Resource.Table)}) - } else { - d.Set("table", nil) + if v, ok := d.GetOk("table_with_columns"); ok && len(v.([]interface{})) > 0 { + // since perm list could include Table, get the right one + for _, perm := range cleanPermissions { + if perm.Resource.TableWithColumns != nil { + if err := d.Set("table_with_columns", []interface{}{flattenLakeFormationTableWithColumnsResource(perm.Resource.TableWithColumns)}); err != nil { + return fmt.Errorf("error setting table_with_columns: %w", err) + } + twcSet = true + break + } } } + if !twcSet { + d.Set("table_with_columns", nil) + } + return nil } diff --git a/aws/data_source_aws_lakeformation_permissions_test.go b/aws/data_source_aws_lakeformation_permissions_test.go index c6299c985c36..47d2a77ca5af 100644 --- a/aws/data_source_aws_lakeformation_permissions_test.go +++ b/aws/data_source_aws_lakeformation_permissions_test.go @@ -16,6 +16,7 @@ func testAccAWSLakeFormationPermissionsDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ @@ -39,6 +40,7 @@ func testAccAWSLakeFormationPermissionsDataSource_dataLocation(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ @@ -63,6 +65,7 @@ func testAccAWSLakeFormationPermissionsDataSource_database(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ @@ -91,6 +94,7 @@ func testAccAWSLakeFormationPermissionsDataSource_table(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ @@ -116,6 +120,7 @@ func testAccAWSLakeFormationPermissionsDataSource_tableWithColumns(t *testing.T) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ @@ -143,34 +148,37 @@ data "aws_partition" "current" {} resource "aws_iam_role" "test" { name = %[1]q + path = "/" - assume_role_policy = < 0 { + var loadBalancers []*elbv2.LoadBalancer + + for _, loadBalancer := range results { + arn := aws.StringValue(loadBalancer.LoadBalancerArn) + tags, err := keyvaluetags.Elbv2ListTags(conn, arn) + + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeLoadBalancerNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("error listing tags for (%s): %w", arn, err) + } + + if !tags.ContainsAll(tagsToMatch) { + continue + } + + loadBalancers = append(loadBalancers, loadBalancer) + } + + results = loadBalancers + } + + if len(results) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) + } + + lb := results[0] + + d.SetId(aws.StringValue(lb.LoadBalancerArn)) + + d.Set("arn", lb.LoadBalancerArn) + d.Set("arn_suffix", lbSuffixFromARN(lb.LoadBalancerArn)) + d.Set("name", lb.LoadBalancerName) + d.Set("internal", lb.Scheme != nil && aws.StringValue(lb.Scheme) == "internal") + d.Set("security_groups", flattenStringList(lb.SecurityGroups)) + d.Set("vpc_id", lb.VpcId) + d.Set("zone_id", lb.CanonicalHostedZoneId) + d.Set("dns_name", lb.DNSName) + d.Set("ip_address_type", lb.IpAddressType) + d.Set("load_balancer_type", lb.Type) + d.Set("customer_owned_ipv4_pool", lb.CustomerOwnedIpv4Pool) + + if err := d.Set("subnets", flattenSubnetsFromAvailabilityZones(lb.AvailabilityZones)); err != nil { + return fmt.Errorf("error setting subnets: %w", err) + } + + if err := d.Set("subnet_mapping", flattenSubnetMappingsFromAvailabilityZones(lb.AvailabilityZones)); err != nil { + return fmt.Errorf("error setting subnet_mapping: %w", err) } - log.Printf("[DEBUG] Reading Load Balancer: %s", describeLbOpts) - describeResp, err := conn.DescribeLoadBalancers(describeLbOpts) + tags, err := keyvaluetags.Elbv2ListTags(conn, d.Id()) + if err != nil { - return fmt.Errorf("Error retrieving LB: %w", err) + return fmt.Errorf("error listing tags for (%s): %w", d.Id(), err) } - if len(describeResp.LoadBalancers) != 1 { - return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(describeResp.LoadBalancers)) + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + attributesResp, err := conn.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{ + LoadBalancerArn: aws.String(d.Id()), + }) + if err != nil { + return fmt.Errorf("error retrieving LB Attributes: %w", err) + } + + accessLogMap := map[string]interface{}{ + "bucket": "", + "enabled": false, + "prefix": "", + } + + for _, attr := range attributesResp.Attributes { + switch aws.StringValue(attr.Key) { + case "access_logs.s3.enabled": + accessLogMap["enabled"] = aws.StringValue(attr.Value) == "true" + case "access_logs.s3.bucket": + accessLogMap["bucket"] = aws.StringValue(attr.Value) + case "access_logs.s3.prefix": + accessLogMap["prefix"] = aws.StringValue(attr.Value) + case "idle_timeout.timeout_seconds": + timeout, err := strconv.Atoi(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error parsing ALB timeout: %w", err) + } + d.Set("idle_timeout", timeout) + case "routing.http.drop_invalid_header_fields.enabled": + dropInvalidHeaderFieldsEnabled := aws.StringValue(attr.Value) == "true" + d.Set("drop_invalid_header_fields", dropInvalidHeaderFieldsEnabled) + case "deletion_protection.enabled": + protectionEnabled := aws.StringValue(attr.Value) == "true" + d.Set("enable_deletion_protection", protectionEnabled) + case "routing.http2.enabled": + http2Enabled := aws.StringValue(attr.Value) == "true" + d.Set("enable_http2", http2Enabled) + case "load_balancing.cross_zone.enabled": + crossZoneLbEnabled := aws.StringValue(attr.Value) == "true" + d.Set("enable_cross_zone_load_balancing", crossZoneLbEnabled) + } + } + + if err := d.Set("access_logs", []interface{}{accessLogMap}); err != nil { + return fmt.Errorf("error setting access_logs: %w", err) } - d.SetId(aws.StringValue(describeResp.LoadBalancers[0].LoadBalancerArn)) - return flattenAwsLbResource(d, meta, describeResp.LoadBalancers[0]) + return nil } diff --git a/aws/data_source_aws_lb_listener.go b/aws/data_source_aws_lb_listener.go index dd594b6d5b38..239834b43b77 100644 --- a/aws/data_source_aws_lb_listener.go +++ b/aws/data_source_aws_lb_listener.go @@ -3,10 +3,12 @@ package aws import ( "errors" "fmt" + "sort" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func dataSourceAwsLbListener() *schema.Resource { @@ -14,41 +16,20 @@ func dataSourceAwsLbListener() *schema.Resource { Read: dataSourceAwsLbListenerRead, Schema: map[string]*schema.Schema{ + "alpn_policy": { + Type: schema.TypeString, + Computed: true, + }, "arn": { Type: schema.TypeString, Optional: true, Computed: true, ConflictsWith: []string{"load_balancer_arn", "port"}, }, - - "load_balancer_arn": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"arn"}, - }, - "port": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - ConflictsWith: []string{"arn"}, - }, - - "protocol": { - Type: schema.TypeString, - Computed: true, - }, - - "ssl_policy": { - Type: schema.TypeString, - Computed: true, - }, - "certificate_arn": { Type: schema.TypeString, Computed: true, }, - "default_action": { Type: schema.TypeList, Computed: true, @@ -169,6 +150,46 @@ func dataSourceAwsLbListener() *schema.Resource { }, }, }, + "forward": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "stickiness": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "duration": { + Type: schema.TypeInt, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "target_group": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "weight": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + }, + }, + }, "order": { Type: schema.TypeInt, Computed: true, @@ -213,82 +234,117 @@ func dataSourceAwsLbListener() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "forward": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "target_group": { - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "arn": { - Type: schema.TypeString, - Computed: true, - }, - "weight": { - Type: schema.TypeInt, - Computed: true, - }, - }, - }, - }, - "stickiness": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, - Computed: true, - }, - "duration": { - Type: schema.TypeInt, - Computed: true, - }, - }, - }, - }, - }, - }, - }, }, }, }, + "load_balancer_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"arn"}, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"arn"}, + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + }, + "ssl_policy": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), }, } } func dataSourceAwsLbListenerRead(d *schema.ResourceData, meta interface{}) error { - if _, ok := d.GetOk("arn"); ok { - d.SetId(d.Get("arn").(string)) - //log.Printf("[DEBUG] read listener %s", d.Get("arn").(string)) - return resourceAwsLbListenerRead(d, meta) - } - conn := meta.(*AWSClient).elbv2conn - lbArn, lbOk := d.GetOk("load_balancer_arn") - port, portOk := d.GetOk("port") - if !lbOk || !portOk { - return errors.New("both load_balancer_arn and port must be set") + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &elbv2.DescribeListenersInput{} + + if v, ok := d.GetOk("arn"); ok { + input.ListenerArns = aws.StringSlice([]string{v.(string)}) + } else { + lbArn, lbOk := d.GetOk("load_balancer_arn") + _, portOk := d.GetOk("port") + + if !lbOk || !portOk { + return errors.New("both load_balancer_arn and port must be set") + } + + input.LoadBalancerArn = aws.String(lbArn.(string)) } - resp, err := conn.DescribeListeners(&elbv2.DescribeListenersInput{ - LoadBalancerArn: aws.String(lbArn.(string)), + + var results []*elbv2.Listener + + err := conn.DescribeListenersPages(input, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, l := range page.Listeners { + if l == nil { + continue + } + + if v, ok := d.GetOk("port"); ok && v.(int) != int(aws.Int64Value(l.Port)) { + continue + } + + results = append(results, l) + } + + return !lastPage }) + if err != nil { - return err + return fmt.Errorf("error reading Listener: %w", err) } - if len(resp.Listeners) == 0 { - return fmt.Errorf("no listener exists for load balancer: %s", lbArn) + + if len(results) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) } - for _, listener := range resp.Listeners { - if *listener.Port == int64(port.(int)) { - //log.Printf("[DEBUG] get listener arn for %s:%s: %s", lbArn, port, *listener.Port) - d.SetId(aws.StringValue(listener.ListenerArn)) - return resourceAwsLbListenerRead(d, meta) - } + + listener := results[0] + + d.SetId(aws.StringValue(listener.ListenerArn)) + d.Set("arn", listener.ListenerArn) + d.Set("load_balancer_arn", listener.LoadBalancerArn) + d.Set("port", listener.Port) + d.Set("protocol", listener.Protocol) + d.Set("ssl_policy", listener.SslPolicy) + + if listener.Certificates != nil && len(listener.Certificates) == 1 && listener.Certificates[0] != nil { + d.Set("certificate_arn", listener.Certificates[0].CertificateArn) + } + + if listener.AlpnPolicy != nil && len(listener.AlpnPolicy) == 1 && listener.AlpnPolicy[0] != nil { + d.Set("alpn_policy", listener.AlpnPolicy[0]) + } + + sort.Slice(listener.DefaultActions, func(i, j int) bool { + return aws.Int64Value(listener.DefaultActions[i].Order) < aws.Int64Value(listener.DefaultActions[j].Order) + }) + + if err := d.Set("default_action", flattenLbListenerActions(d, listener.DefaultActions)); err != nil { + return fmt.Errorf("error setting default_action: %w", err) + } + + tags, err := keyvaluetags.Elbv2ListTags(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error listing tags for (%s): %w", d.Id(), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - return errors.New("failed to get listener arn with given arguments") + return nil } diff --git a/aws/data_source_aws_lb_listener_test.go b/aws/data_source_aws_lb_listener_test.go index 8c717c4f4ded..0ac4641253df 100644 --- a/aws/data_source_aws_lb_listener_test.go +++ b/aws/data_source_aws_lb_listener_test.go @@ -4,35 +4,40 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAWSLBListener_basic(t *testing.T) { - lbName := fmt.Sprintf("testlistener-basic-%s", acctest.RandString(13)) - targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_lb_listener.test" + dataSourceName2 := "data.aws_lb_listener.from_lb_and_port" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBListenerConfigBasic(lbName, targetGroupName), + Config: testAccDataSourceAWSLBListenerConfigBasic(rName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "protocol", "HTTP"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "port", "80"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "default_action.0.type", "forward"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "protocol", "HTTP"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "port", "80"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "default_action.0.type", "forward"), + resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTP"), + resource.TestCheckResourceAttr(dataSourceName, "port", "80"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), + resource.TestCheckResourceAttr(dataSourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTP"), + resource.TestCheckResourceAttr(dataSourceName2, "port", "80"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), + resource.TestCheckResourceAttr(dataSourceName2, "tags.%", "0"), ), }, }, @@ -40,30 +45,32 @@ func TestAccDataSourceAWSLBListener_basic(t *testing.T) { } func TestAccDataSourceAWSLBListener_BackwardsCompatibility(t *testing.T) { - lbName := fmt.Sprintf("testlistener-basic-%s", acctest.RandString(13)) - targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_alb_listener.test" + dataSourceName2 := "data.aws_alb_listener.from_lb_and_port" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBListenerConfigBackwardsCompatibility(lbName, targetGroupName), + Config: testAccDataSourceAWSLBListenerConfigBackwardsCompatibility(rName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.aws_alb_listener.front_end", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_alb_listener.front_end", "arn"), - resource.TestCheckResourceAttrSet("data.aws_alb_listener.front_end", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr("data.aws_alb_listener.front_end", "protocol", "HTTP"), - resource.TestCheckResourceAttr("data.aws_alb_listener.front_end", "port", "80"), - resource.TestCheckResourceAttr("data.aws_alb_listener.front_end", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_alb_listener.front_end", "default_action.0.type", "forward"), - resource.TestCheckResourceAttrSet("data.aws_alb_listener.from_lb_and_port", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_alb_listener.from_lb_and_port", "arn"), - resource.TestCheckResourceAttrSet("data.aws_alb_listener.from_lb_and_port", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr("data.aws_alb_listener.from_lb_and_port", "protocol", "HTTP"), - resource.TestCheckResourceAttr("data.aws_alb_listener.from_lb_and_port", "port", "80"), - resource.TestCheckResourceAttr("data.aws_alb_listener.from_lb_and_port", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_alb_listener.from_lb_and_port", "default_action.0.type", "forward"), + resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTP"), + resource.TestCheckResourceAttr(dataSourceName, "port", "80"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), + resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTP"), + resource.TestCheckResourceAttr(dataSourceName2, "port", "80"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), ), }, }, @@ -71,36 +78,38 @@ func TestAccDataSourceAWSLBListener_BackwardsCompatibility(t *testing.T) { } func TestAccDataSourceAWSLBListener_https(t *testing.T) { - lbName := fmt.Sprintf("testlistener-https-%s", acctest.RandString(13)) - targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, "example.com") + dataSourceName := "data.aws_lb_listener.test" + dataSourceName2 := "data.aws_lb_listener.from_lb_and_port" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBListenerConfigHTTPS(lbName, targetGroupName, tlsPemEscapeNewlines(certificate), tlsPemEscapeNewlines(key)), + Config: testAccDataSourceAWSLBListenerConfigHTTPS(rName, tlsPemEscapeNewlines(certificate), tlsPemEscapeNewlines(key)), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.front_end", "certificate_arn"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "protocol", "HTTPS"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "port", "443"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "default_action.0.type", "forward"), - resource.TestCheckResourceAttr("data.aws_lb_listener.front_end", "ssl_policy", "ELBSecurityPolicy-2016-08"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "load_balancer_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "default_action.0.target_group_arn"), - resource.TestCheckResourceAttrSet("data.aws_lb_listener.from_lb_and_port", "certificate_arn"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "protocol", "HTTPS"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "port", "443"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "default_action.#", "1"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "default_action.0.type", "forward"), - resource.TestCheckResourceAttr("data.aws_lb_listener.from_lb_and_port", "ssl_policy", "ELBSecurityPolicy-2016-08"), + resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttrSet(dataSourceName, "certificate_arn"), + resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttr(dataSourceName, "port", "443"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), + resource.TestCheckResourceAttr(dataSourceName, "ssl_policy", "ELBSecurityPolicy-2016-08"), + resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), + resource.TestCheckResourceAttrSet(dataSourceName2, "certificate_arn"), + resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTPS"), + resource.TestCheckResourceAttr(dataSourceName2, "port", "443"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), + resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), + resource.TestCheckResourceAttr(dataSourceName2, "ssl_policy", "ELBSecurityPolicy-2016-08"), ), }, }, @@ -113,8 +122,9 @@ func TestAccDataSourceAWSLBListener_DefaultAction_Forward(t *testing.T) { resourceName := "aws_lb_listener.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSLBListenerConfigDefaultActionForward(rName), @@ -127,10 +137,10 @@ func TestAccDataSourceAWSLBListener_DefaultAction_Forward(t *testing.T) { }) } -func testAccDataSourceAWSLBListenerConfigBasic(lbName, targetGroupName string) string { - return fmt.Sprintf(` -resource "aws_lb_listener" "front_end" { - load_balancer_arn = aws_lb.alb_test.id +func testAccDataSourceAWSLBListenerConfigBasic(rName string) string { + return composeConfig(testAccAWSLBListenerConfigBase(rName), fmt.Sprintf(` +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id protocol = "HTTP" port = "80" @@ -140,11 +150,11 @@ resource "aws_lb_listener" "front_end" { } } -resource "aws_lb" "alb_test" { - name = "%s" +resource "aws_lb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.alb_test.id] - subnets = aws_subnet.alb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false @@ -155,10 +165,10 @@ resource "aws_lb" "alb_test" { } resource "aws_lb_target_group" "test" { - name = "%s" + name = %[1]q port = 8080 protocol = "HTTP" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id health_check { path = "/health" @@ -172,91 +182,21 @@ resource "aws_lb_target_group" "test" { } } -variable "subnets" { - default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_vpc" "alb_test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-lb-listener-data-source-basic" - } -} - -resource "aws_subnet" "alb_test" { - count = 2 - vpc_id = aws_vpc.alb_test.id - cidr_block = element(var.subnets, count.index) - map_public_ip_on_launch = true - availability_zone = element(data.aws_availability_zones.available.names, count.index) - - tags = { - Name = "tf-acc-lb-listener-data-source-basic" - } -} - -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" - description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id - - ingress { - from_port = 0 - to_port = 0 - protocol = "-1" - self = true - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - TestName = "TestAccAWSALB_basic" - } -} - -data "aws_lb_listener" "front_end" { - arn = aws_lb_listener.front_end.arn +data "aws_lb_listener" "test" { + arn = aws_lb_listener.test.arn } data "aws_lb_listener" "from_lb_and_port" { - load_balancer_arn = aws_lb.alb_test.arn - port = aws_lb_listener.front_end.port + load_balancer_arn = aws_lb.test.arn + port = aws_lb_listener.test.port } - -output "front_end_load_balancer_arn" { - value = data.aws_lb_listener.front_end.load_balancer_arn -} - -output "front_end_port" { - value = data.aws_lb_listener.front_end.port -} - -output "from_lb_and_port_arn" { - value = data.aws_lb_listener.from_lb_and_port.arn -} -`, lbName, targetGroupName) +`, rName)) } -func testAccDataSourceAWSLBListenerConfigBackwardsCompatibility(lbName, targetGroupName string) string { - return fmt.Sprintf(` -resource "aws_alb_listener" "front_end" { - load_balancer_arn = aws_alb.alb_test.id +func testAccDataSourceAWSLBListenerConfigBackwardsCompatibility(rName string) string { + return composeConfig(testAccAWSLBListenerConfigBase(rName), fmt.Sprintf(` +resource "aws_alb_listener" "test" { + load_balancer_arn = aws_alb.test.id protocol = "HTTP" port = "80" @@ -266,11 +206,11 @@ resource "aws_alb_listener" "front_end" { } } -resource "aws_alb" "alb_test" { - name = "%s" +resource "aws_alb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.alb_test.id] - subnets = aws_subnet.alb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false @@ -281,10 +221,10 @@ resource "aws_alb" "alb_test" { } resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 8080 protocol = "HTTP" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id health_check { path = "/health" @@ -298,83 +238,25 @@ resource "aws_alb_target_group" "test" { } } -variable "subnets" { - default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_vpc" "alb_test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-lb-listener-data-source-bc" - } -} - -resource "aws_subnet" "alb_test" { - count = 2 - vpc_id = aws_vpc.alb_test.id - cidr_block = element(var.subnets, count.index) - map_public_ip_on_launch = true - availability_zone = element(data.aws_availability_zones.available.names, count.index) - - tags = { - Name = "tf-acc-lb-listener-data-source-bc" - } -} - -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" - description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id - - ingress { - from_port = 0 - to_port = 0 - protocol = "-1" - self = true - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - TestName = "TestAccAWSALB_basic" - } -} - -data "aws_alb_listener" "front_end" { - arn = aws_alb_listener.front_end.arn +data "aws_alb_listener" "test" { + arn = aws_alb_listener.test.arn } data "aws_alb_listener" "from_lb_and_port" { - load_balancer_arn = aws_alb.alb_test.arn - port = aws_alb_listener.front_end.port + load_balancer_arn = aws_alb.test.arn + port = aws_alb_listener.test.port } -`, lbName, targetGroupName) +`, rName)) } -func testAccDataSourceAWSLBListenerConfigHTTPS(lbName, targetGroupName, certificate, key string) string { - return fmt.Sprintf(` -resource "aws_lb_listener" "front_end" { - load_balancer_arn = aws_lb.alb_test.id +func testAccDataSourceAWSLBListenerConfigHTTPS(rName, certificate, key string) string { + return composeConfig(testAccAWSLBListenerConfigBase(rName), fmt.Sprintf(` +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id protocol = "HTTPS" port = "443" ssl_policy = "ELBSecurityPolicy-2016-08" - certificate_arn = aws_iam_server_certificate.test_cert.arn + certificate_arn = aws_iam_server_certificate.test.arn default_action { target_group_arn = aws_lb_target_group.test.id @@ -382,11 +264,11 @@ resource "aws_lb_listener" "front_end" { } } -resource "aws_lb" "alb_test" { - name = "%[1]s" +resource "aws_lb" "test" { + name = %[1]q internal = false - security_groups = [aws_security_group.alb_test.id] - subnets = aws_subnet.alb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false @@ -399,10 +281,10 @@ resource "aws_lb" "alb_test" { } resource "aws_lb_target_group" "test" { - name = "%[2]s" + name = %[1]q port = 8080 protocol = "HTTP" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id health_check { path = "/health" @@ -416,88 +298,30 @@ resource "aws_lb_target_group" "test" { } } -variable "subnets" { - default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_vpc" "alb_test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-lb-listener-data-source-https" - } -} - resource "aws_internet_gateway" "gw" { - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id tags = { - Name = "terraform-testacc-lb-listener-data-source-https" + Name = %[1]q TestName = "TestAccAWSALB_basic" } } -resource "aws_subnet" "alb_test" { - count = 2 - vpc_id = aws_vpc.alb_test.id - cidr_block = element(var.subnets, count.index) - map_public_ip_on_launch = true - availability_zone = element(data.aws_availability_zones.available.names, count.index) - - tags = { - Name = "tf-acc-lb-listener-data-source-https" - } +resource "aws_iam_server_certificate" "test" { + name = %[1]q + certificate_body = "%[2]s" + private_key = "%[3]s" } -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" - description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id - - ingress { - from_port = 0 - to_port = 0 - protocol = "-1" - self = true - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - TestName = "TestAccAWSALB_basic" - } -} - -resource "aws_iam_server_certificate" "test_cert" { - name = "terraform-test-cert-%[3]d" - certificate_body = "%[4]s" - private_key = "%[5]s" -} - -data "aws_lb_listener" "front_end" { - arn = aws_lb_listener.front_end.arn +data "aws_lb_listener" "test" { + arn = aws_lb_listener.test.arn } data "aws_lb_listener" "from_lb_and_port" { - load_balancer_arn = aws_lb.alb_test.arn - port = aws_lb_listener.front_end.port + load_balancer_arn = aws_lb.test.arn + port = aws_lb_listener.test.port } -`, lbName, targetGroupName, acctest.RandInt(), certificate, key) +`, rName, certificate, key)) } func testAccDataSourceAWSLBListenerConfigDefaultActionForward(rName string) string { @@ -508,7 +332,7 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "tf-acc-test-load-balancer" + Name = %[1]q } } @@ -520,7 +344,7 @@ resource "aws_subnet" "test" { vpc_id = aws_vpc.test.id tags = { - Name = "tf-acc-test-load-balancer" + Name = %[1]q } } diff --git a/aws/data_source_aws_lb_target_group.go b/aws/data_source_aws_lb_target_group.go index ca765be15258..1f19006f5541 100644 --- a/aws/data_source_aws_lb_target_group.go +++ b/aws/data_source_aws_lb_target_group.go @@ -2,11 +2,12 @@ package aws import ( "fmt" - "log" + "strconv" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func dataSourceAwsLbTargetGroup() *schema.Resource { @@ -18,69 +19,15 @@ func dataSourceAwsLbTargetGroup() *schema.Resource { Optional: true, Computed: true, }, - "arn_suffix": { Type: schema.TypeString, Computed: true, }, - - "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - - "port": { - Type: schema.TypeInt, - Computed: true, - }, - - "protocol": { - Type: schema.TypeString, - Computed: true, - }, - - "protocol_version": { - Type: schema.TypeString, - Computed: true, - }, - - "vpc_id": { - Type: schema.TypeString, - Computed: true, - }, - "deregistration_delay": { Type: schema.TypeInt, Computed: true, }, - - "slow_start": { - Type: schema.TypeInt, - Computed: true, - }, - - "proxy_protocol_v2": { - Type: schema.TypeBool, - Computed: true, - }, - - "lambda_multi_value_headers_enabled": { - Type: schema.TypeBool, - Computed: true, - }, - - "load_balancing_algorithm_type": { - Type: schema.TypeString, - Computed: true, - }, - - "target_type": { - Type: schema.TypeString, - Computed: true, - }, - - "stickiness": { + "health_check": { Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ @@ -89,100 +36,232 @@ func dataSourceAwsLbTargetGroup() *schema.Resource { Type: schema.TypeBool, Computed: true, }, - "type": { - Type: schema.TypeString, - Computed: true, - }, - "cookie_duration": { + "healthy_threshold": { Type: schema.TypeInt, Computed: true, }, - }, - }, - }, - - "health_check": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, - Computed: true, - }, - "interval": { Type: schema.TypeInt, Computed: true, }, - + "matcher": { + Type: schema.TypeString, + Computed: true, + }, "path": { Type: schema.TypeString, Computed: true, }, - "port": { Type: schema.TypeString, Computed: true, }, - "protocol": { Type: schema.TypeString, Computed: true, }, - "timeout": { Type: schema.TypeInt, Computed: true, }, - - "healthy_threshold": { + "unhealthy_threshold": { Type: schema.TypeInt, Computed: true, }, - - "matcher": { + }, + }, + }, + "lambda_multi_value_headers_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "load_balancing_algorithm_type": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "port": { + Type: schema.TypeInt, + Computed: true, + }, + "preserve_client_ip": { + Type: schema.TypeString, + Computed: true, + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + }, + "protocol_version": { + Type: schema.TypeString, + Computed: true, + }, + "proxy_protocol_v2": { + Type: schema.TypeBool, + Computed: true, + }, + "slow_start": { + Type: schema.TypeInt, + Computed: true, + }, + "stickiness": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cookie_duration": { + Type: schema.TypeInt, + Computed: true, + }, + "cookie_name": { Type: schema.TypeString, Computed: true, }, - - "unhealthy_threshold": { - Type: schema.TypeInt, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "type": { + Type: schema.TypeString, Computed: true, }, }, }, }, - + "target_type": { + Type: schema.TypeString, + Computed: true, + }, "tags": tagsSchemaComputed(), + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, }, } } func dataSourceAwsLbTargetGroupRead(d *schema.ResourceData, meta interface{}) error { - elbconn := meta.(*AWSClient).elbv2conn - tgArn := d.Get("arn").(string) - tgName := d.Get("name").(string) - - describeTgOpts := &elbv2.DescribeTargetGroupsInput{} - switch { - case tgArn != "": - describeTgOpts.TargetGroupArns = []*string{aws.String(tgArn)} - case tgName != "": - describeTgOpts.Names = []*string{aws.String(tgName)} + conn := meta.(*AWSClient).elbv2conn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &elbv2.DescribeTargetGroupsInput{} + + if v, ok := d.GetOk("arn"); ok { + input.TargetGroupArns = aws.StringSlice([]string{v.(string)}) + } else if v, ok := d.GetOk("name"); ok { + input.Names = aws.StringSlice([]string{v.(string)}) } - log.Printf("[DEBUG] Reading Load Balancer Target Group: %s", describeTgOpts) - describeResp, err := elbconn.DescribeTargetGroups(describeTgOpts) + var results []*elbv2.TargetGroup + + err := conn.DescribeTargetGroupsPages(input, func(page *elbv2.DescribeTargetGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + results = append(results, page.TargetGroups...) + + return !lastPage + }) + if err != nil { - return fmt.Errorf("Error retrieving LB Target Group: %w", err) + return fmt.Errorf("error retrieving LB Target Group: %w", err) } - if len(describeResp.TargetGroups) != 1 { - return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(describeResp.TargetGroups)) + if len(results) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) } - targetGroup := describeResp.TargetGroups[0] + targetGroup := results[0] d.SetId(aws.StringValue(targetGroup.TargetGroupArn)) - return flattenAwsLbTargetGroupResource(d, meta, targetGroup) + + d.Set("arn", targetGroup.TargetGroupArn) + d.Set("arn_suffix", lbTargetGroupSuffixFromARN(targetGroup.TargetGroupArn)) + d.Set("name", targetGroup.TargetGroupName) + d.Set("target_type", targetGroup.TargetType) + + if err := d.Set("health_check", flattenLbTargetGroupHealthCheck(targetGroup)); err != nil { + return fmt.Errorf("error setting health_check: %w", err) + } + + if v, _ := d.Get("target_type").(string); v != elbv2.TargetTypeEnumLambda { + d.Set("vpc_id", targetGroup.VpcId) + d.Set("port", targetGroup.Port) + d.Set("protocol", targetGroup.Protocol) + } + switch d.Get("protocol").(string) { + case elbv2.ProtocolEnumHttp, elbv2.ProtocolEnumHttps: + d.Set("protocol_version", targetGroup.ProtocolVersion) + } + + attrResp, err := conn.DescribeTargetGroupAttributes(&elbv2.DescribeTargetGroupAttributesInput{ + TargetGroupArn: aws.String(d.Id()), + }) + if err != nil { + return fmt.Errorf("error retrieving Target Group Attributes: %w", err) + } + + for _, attr := range attrResp.Attributes { + switch aws.StringValue(attr.Key) { + case "deregistration_delay.timeout_seconds": + timeout, err := strconv.Atoi(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error converting deregistration_delay.timeout_seconds to int: %s", aws.StringValue(attr.Value)) + } + d.Set("deregistration_delay", timeout) + case "lambda.multi_value_headers.enabled": + enabled, err := strconv.ParseBool(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error converting lambda.multi_value_headers.enabled to bool: %s", aws.StringValue(attr.Value)) + } + d.Set("lambda_multi_value_headers_enabled", enabled) + case "proxy_protocol_v2.enabled": + enabled, err := strconv.ParseBool(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error converting proxy_protocol_v2.enabled to bool: %s", aws.StringValue(attr.Value)) + } + d.Set("proxy_protocol_v2", enabled) + case "slow_start.duration_seconds": + slowStart, err := strconv.Atoi(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error converting slow_start.duration_seconds to int: %s", aws.StringValue(attr.Value)) + } + d.Set("slow_start", slowStart) + case "load_balancing.algorithm.type": + loadBalancingAlgorithm := aws.StringValue(attr.Value) + d.Set("load_balancing_algorithm_type", loadBalancingAlgorithm) + case "preserve_client_ip.enabled": + _, err := strconv.ParseBool(aws.StringValue(attr.Value)) + if err != nil { + return fmt.Errorf("error converting preserve_client_ip.enabled to bool: %s", aws.StringValue(attr.Value)) + } + d.Set("preserve_client_ip", attr.Value) + } + } + + stickinessAttr, err := flattenAwsLbTargetGroupStickiness(attrResp.Attributes) + if err != nil { + return fmt.Errorf("error flattening stickiness: %w", err) + } + + if err := d.Set("stickiness", stickinessAttr); err != nil { + return fmt.Errorf("error setting stickiness: %w", err) + } + + tags, err := keyvaluetags.Elbv2ListTags(conn, d.Id()) + + if err != nil { + return fmt.Errorf("error listing tags for LB Target Group (%s): %w", d.Id(), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil } diff --git a/aws/data_source_aws_lb_target_group_test.go b/aws/data_source_aws_lb_target_group_test.go index 56f00d7b7c62..8c1eeabd0c38 100644 --- a/aws/data_source_aws_lb_target_group_test.go +++ b/aws/data_source_aws_lb_target_group_test.go @@ -4,24 +4,25 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccDataSourceAWSALBTargetGroup_basic(t *testing.T) { - lbName := fmt.Sprintf("testlb-%s", acctest.RandString(13)) - targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandString(10)) +func TestAccDataSourceAWSLBTargetGroup_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") resourceNameArn := "data.aws_lb_target_group.alb_tg_test_with_arn" resourceName := "data.aws_lb_target_group.alb_tg_test_with_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBTargetGroupConfigBasic(lbName, targetGroupName), + Config: testAccDataSourceAWSLBTargetGroupConfigBasic(rName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resourceNameArn, "name", targetGroupName), + resource.TestCheckResourceAttr(resourceNameArn, "name", rName), resource.TestCheckResourceAttrSet(resourceNameArn, "arn"), resource.TestCheckResourceAttrSet(resourceNameArn, "arn_suffix"), resource.TestCheckResourceAttr(resourceNameArn, "port", "8080"), @@ -32,7 +33,7 @@ func TestAccDataSourceAWSALBTargetGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceNameArn, "deregistration_delay", "300"), resource.TestCheckResourceAttr(resourceNameArn, "slow_start", "0"), resource.TestCheckResourceAttr(resourceNameArn, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceNameArn, "tags.TestName", "TestAccDataSourceAWSALBTargetGroup_basic"), + resource.TestCheckResourceAttr(resourceNameArn, "tags.TestName", rName), resource.TestCheckResourceAttr(resourceNameArn, "stickiness.#", "1"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.#", "1"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.path", "/health"), @@ -43,7 +44,7 @@ func TestAccDataSourceAWSALBTargetGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.unhealthy_threshold", "3"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.matcher", "200-299"), - resource.TestCheckResourceAttr(resourceName, "name", targetGroupName), + resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttrSet(resourceName, "arn"), resource.TestCheckResourceAttrSet(resourceName, "arn_suffix"), resource.TestCheckResourceAttr(resourceName, "port", "8080"), @@ -53,7 +54,7 @@ func TestAccDataSourceAWSALBTargetGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "300"), resource.TestCheckResourceAttr(resourceName, "slow_start", "0"), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.TestName", "TestAccDataSourceAWSALBTargetGroup_basic"), + resource.TestCheckResourceAttr(resourceName, "tags.TestName", rName), resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health"), @@ -69,20 +70,61 @@ func TestAccDataSourceAWSALBTargetGroup_basic(t *testing.T) { }) } +func TestAccDataSourceAWSLBTargetGroup_appCookie(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceNameArn := "data.aws_lb_target_group.alb_tg_test_with_arn" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAWSLBTargetGroupConfigAppCookie(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceNameArn, "name", rName), + resource.TestCheckResourceAttrSet(resourceNameArn, "arn"), + resource.TestCheckResourceAttrSet(resourceNameArn, "arn_suffix"), + resource.TestCheckResourceAttr(resourceNameArn, "port", "8080"), + resource.TestCheckResourceAttr(resourceNameArn, "protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceNameArn, "protocol_version", "HTTP1"), + resource.TestCheckResourceAttrSet(resourceNameArn, "vpc_id"), + resource.TestCheckResourceAttrSet(resourceNameArn, "load_balancing_algorithm_type"), + resource.TestCheckResourceAttr(resourceNameArn, "deregistration_delay", "300"), + resource.TestCheckResourceAttr(resourceNameArn, "slow_start", "0"), + resource.TestCheckResourceAttr(resourceNameArn, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceNameArn, "tags.TestName", rName), + resource.TestCheckResourceAttr(resourceNameArn, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceNameArn, "stickiness.0.cookie_duration", "600"), + resource.TestCheckResourceAttr(resourceNameArn, "stickiness.0.cookie_name", "cookieName"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.path", "/health"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.port", "8081"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.timeout", "3"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.healthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.unhealthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.matcher", "200-299"), + ), + }, + }, + }) +} + func TestAccDataSourceAWSLBTargetGroup_BackwardsCompatibility(t *testing.T) { - lbName := fmt.Sprintf("testlb-%s", acctest.RandString(13)) - targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") resourceNameArn := "data.aws_alb_target_group.alb_tg_test_with_arn" resourceName := "data.aws_alb_target_group.alb_tg_test_with_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBTargetGroupConfigBackwardsCompatibility(lbName, targetGroupName), + Config: testAccDataSourceAWSLBTargetGroupConfigBackwardsCompatibility(rName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resourceNameArn, "name", targetGroupName), + resource.TestCheckResourceAttr(resourceNameArn, "name", rName), resource.TestCheckResourceAttrSet(resourceNameArn, "arn"), resource.TestCheckResourceAttrSet(resourceNameArn, "arn_suffix"), resource.TestCheckResourceAttr(resourceNameArn, "port", "8080"), @@ -92,7 +134,7 @@ func TestAccDataSourceAWSLBTargetGroup_BackwardsCompatibility(t *testing.T) { resource.TestCheckResourceAttr(resourceNameArn, "deregistration_delay", "300"), resource.TestCheckResourceAttr(resourceNameArn, "slow_start", "0"), resource.TestCheckResourceAttr(resourceNameArn, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceNameArn, "tags.TestName", "TestAccDataSourceAWSALBTargetGroup_basic"), + resource.TestCheckResourceAttr(resourceNameArn, "tags.TestName", rName), resource.TestCheckResourceAttr(resourceNameArn, "stickiness.#", "1"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.#", "1"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.path", "/health"), @@ -103,7 +145,7 @@ func TestAccDataSourceAWSLBTargetGroup_BackwardsCompatibility(t *testing.T) { resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.unhealthy_threshold", "3"), resource.TestCheckResourceAttr(resourceNameArn, "health_check.0.matcher", "200-299"), - resource.TestCheckResourceAttr(resourceName, "name", targetGroupName), + resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttrSet(resourceName, "arn"), resource.TestCheckResourceAttrSet(resourceName, "arn_suffix"), resource.TestCheckResourceAttr(resourceName, "port", "8080"), @@ -112,7 +154,7 @@ func TestAccDataSourceAWSLBTargetGroup_BackwardsCompatibility(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "300"), resource.TestCheckResourceAttr(resourceName, "slow_start", "0"), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.TestName", "TestAccDataSourceAWSALBTargetGroup_basic"), + resource.TestCheckResourceAttr(resourceName, "tags.TestName", rName), resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health"), @@ -128,7 +170,7 @@ func TestAccDataSourceAWSLBTargetGroup_BackwardsCompatibility(t *testing.T) { }) } -func testAccDataSourceAWSLBTargetGroupConfigBasic(lbName string, targetGroupName string) string { +func testAccDataSourceAWSLBTargetGroupConfigBasic(rName string) string { return fmt.Sprintf(` resource "aws_lb_listener" "front_end" { load_balancer_arn = aws_lb.alb_test.id @@ -142,7 +184,7 @@ resource "aws_lb_listener" "front_end" { } resource "aws_lb" "alb_test" { - name = "%s" + name = %[1]q internal = true security_groups = [aws_security_group.alb_test.id] subnets = aws_subnet.alb_test[*].id @@ -151,12 +193,12 @@ resource "aws_lb" "alb_test" { enable_deletion_protection = false tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } resource "aws_lb_target_group" "test" { - name = "%s" + name = %[1]q port = 8080 protocol = "HTTP" vpc_id = aws_vpc.alb_test.id @@ -173,13 +215,13 @@ resource "aws_lb_target_group" "test" { } tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } variable "subnets" { default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" + type = list(string) } data "aws_availability_zones" "available" { @@ -195,7 +237,7 @@ resource "aws_vpc" "alb_test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-lb-data-source-target-group-basic" + Name = %[1]q } } @@ -207,7 +249,7 @@ resource "aws_subnet" "alb_test" { availability_zone = element(data.aws_availability_zones.available.names, count.index) tags = { - Name = "tf-acc-lb-data-source-target-group-basic" + Name = %[1]q } } @@ -231,7 +273,7 @@ resource "aws_security_group" "alb_test" { } tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -242,10 +284,129 @@ data "aws_lb_target_group" "alb_tg_test_with_arn" { data "aws_lb_target_group" "alb_tg_test_with_name" { name = aws_lb_target_group.test.name } -`, lbName, targetGroupName) +`, rName) +} + +func testAccDataSourceAWSLBTargetGroupConfigAppCookie(rName string) string { + return fmt.Sprintf(` +resource "aws_lb_listener" "front_end" { + load_balancer_arn = aws_lb.alb_test.id + protocol = "HTTP" + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.id + type = "forward" + } +} + +resource "aws_lb" "alb_test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.alb_test.id] + subnets = aws_subnet.alb_test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + TestName = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.alb_test.id + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + stickiness { + type = "app_cookie" + cookie_name = "cookieName" + cookie_duration = 600 + } + + tags = { + TestName = %[1]q + } +} + +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = list(string) +} + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "alb_test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "alb_test" { + count = 2 + vpc_id = aws_vpc.alb_test.id + cidr_block = element(var.subnets, count.index) + map_public_ip_on_launch = true + availability_zone = element(data.aws_availability_zones.available.names, count.index) + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "alb_test" { + name = "allow_all_alb_test" + description = "Used for ALB Testing" + vpc_id = aws_vpc.alb_test.id + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + TestName = %[1]q + } +} + +data "aws_lb_target_group" "alb_tg_test_with_arn" { + arn = aws_lb_target_group.test.arn +} +`, rName) } -func testAccDataSourceAWSLBTargetGroupConfigBackwardsCompatibility(lbName string, targetGroupName string) string { +func testAccDataSourceAWSLBTargetGroupConfigBackwardsCompatibility(rName string) string { return fmt.Sprintf(` resource "aws_alb_listener" "front_end" { load_balancer_arn = aws_alb.alb_test.id @@ -259,7 +420,7 @@ resource "aws_alb_listener" "front_end" { } resource "aws_alb" "alb_test" { - name = "%s" + name = %[1]q internal = true security_groups = [aws_security_group.alb_test.id] subnets = aws_subnet.alb_test[*].id @@ -268,12 +429,12 @@ resource "aws_alb" "alb_test" { enable_deletion_protection = false tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 8080 protocol = "HTTP" vpc_id = aws_vpc.alb_test.id @@ -290,13 +451,13 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } variable "subnets" { default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" + type = list(string) } data "aws_availability_zones" "available" { @@ -312,7 +473,7 @@ resource "aws_vpc" "alb_test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-lb-data-source-target-group-bc" + Name = %[1]q } } @@ -324,7 +485,7 @@ resource "aws_subnet" "alb_test" { availability_zone = element(data.aws_availability_zones.available.names, count.index) tags = { - Name = "tf-acc-lb-data-source-target-group-bc" + Name = %[1]q } } @@ -348,7 +509,7 @@ resource "aws_security_group" "alb_test" { } tags = { - TestName = "TestAccDataSourceAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -359,5 +520,5 @@ data "aws_alb_target_group" "alb_tg_test_with_arn" { data "aws_alb_target_group" "alb_tg_test_with_name" { name = aws_alb_target_group.test.name } -`, lbName, targetGroupName) +`, rName) } diff --git a/aws/data_source_aws_lb_test.go b/aws/data_source_aws_lb_test.go index 766db4b0bc9b..b276144acd6b 100644 --- a/aws/data_source_aws_lb_test.go +++ b/aws/data_source_aws_lb_test.go @@ -4,29 +4,33 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAWSLB_basic(t *testing.T) { - lbName := fmt.Sprintf("testaccawslb-basic-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_lb.alb_test_with_arn" dataSourceName2 := "data.aws_lb.alb_test_with_name" - resourceName := "aws_lb.alb_test" + dataSourceName3 := "data.aws_lb.alb_test_with_tags" + resourceName := "aws_lb.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBConfigBasic(lbName), + Config: testAccDataSourceAWSLBConfigBasic(rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), resource.TestCheckResourceAttrPair(dataSourceName, "internal", resourceName, "internal"), resource.TestCheckResourceAttrPair(dataSourceName, "subnets.#", resourceName, "subnets.#"), resource.TestCheckResourceAttrPair(dataSourceName, "security_groups.#", resourceName, "security_groups.#"), resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), - resource.TestCheckResourceAttrPair(dataSourceName, "tags.TestName", resourceName, "tags.TestName"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Config", resourceName, "tags.Config"), resource.TestCheckResourceAttrPair(dataSourceName, "enable_deletion_protection", resourceName, "enable_deletion_protection"), resource.TestCheckResourceAttrPair(dataSourceName, "idle_timeout", resourceName, "idle_timeout"), resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", resourceName, "vpc_id"), @@ -40,7 +44,8 @@ func TestAccDataSourceAWSLB_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName2, "subnets.#", resourceName, "subnets.#"), resource.TestCheckResourceAttrPair(dataSourceName2, "security_groups.#", resourceName, "security_groups.#"), resource.TestCheckResourceAttrPair(dataSourceName2, "tags.%", resourceName, "tags.%"), - resource.TestCheckResourceAttrPair(dataSourceName2, "tags.TestName", resourceName, "tags.TestName"), + resource.TestCheckResourceAttrPair(dataSourceName2, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName2, "tags.Config", resourceName, "tags.Config"), resource.TestCheckResourceAttrPair(dataSourceName2, "enable_deletion_protection", resourceName, "enable_deletion_protection"), resource.TestCheckResourceAttrPair(dataSourceName2, "idle_timeout", resourceName, "idle_timeout"), resource.TestCheckResourceAttrPair(dataSourceName2, "vpc_id", resourceName, "vpc_id"), @@ -49,6 +54,21 @@ func TestAccDataSourceAWSLB_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName2, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceName2, "ip_address_type", resourceName, "ip_address_type"), resource.TestCheckResourceAttrPair(dataSourceName2, "subnet_mapping.#", resourceName, "subnet_mapping.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "internal", resourceName, "internal"), + resource.TestCheckResourceAttrPair(dataSourceName3, "subnets.#", resourceName, "subnets.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "security_groups.#", resourceName, "security_groups.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.Config", resourceName, "tags.Config"), + resource.TestCheckResourceAttrPair(dataSourceName3, "enable_deletion_protection", resourceName, "enable_deletion_protection"), + resource.TestCheckResourceAttrPair(dataSourceName3, "idle_timeout", resourceName, "idle_timeout"), + resource.TestCheckResourceAttrPair(dataSourceName3, "vpc_id", resourceName, "vpc_id"), + resource.TestCheckResourceAttrPair(dataSourceName3, "zone_id", resourceName, "zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName3, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName3, "ip_address_type", resourceName, "ip_address_type"), + resource.TestCheckResourceAttrPair(dataSourceName3, "subnet_mapping.#", resourceName, "subnet_mapping.#"), ), }, }, @@ -56,23 +76,25 @@ func TestAccDataSourceAWSLB_basic(t *testing.T) { } func TestAccDataSourceAWSLB_outpost(t *testing.T) { - lbName := fmt.Sprintf("testaccawslb-outpost-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_lb.alb_test_with_arn" - resourceName := "aws_lb.alb_test" + resourceName := "aws_lb.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBConfigOutpost(lbName), + Config: testAccDataSourceAWSLBConfigOutpost(rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), resource.TestCheckResourceAttrPair(dataSourceName, "internal", resourceName, "internal"), resource.TestCheckResourceAttrPair(dataSourceName, "subnets.#", resourceName, "subnets.#"), resource.TestCheckResourceAttrPair(dataSourceName, "security_groups.#", resourceName, "security_groups.#"), resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), - resource.TestCheckResourceAttrPair(dataSourceName, "tags.TestName", resourceName, "tags.TestName"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.Config", resourceName, "tags.Config"), resource.TestCheckResourceAttrPair(dataSourceName, "enable_deletion_protection", resourceName, "enable_deletion_protection"), resource.TestCheckResourceAttrPair(dataSourceName, "idle_timeout", resourceName, "idle_timeout"), resource.TestCheckResourceAttrPair(dataSourceName, "vpc_id", resourceName, "vpc_id"), @@ -89,24 +111,27 @@ func TestAccDataSourceAWSLB_outpost(t *testing.T) { } func TestAccDataSourceAWSLB_BackwardsCompatibility(t *testing.T) { - lbName := fmt.Sprintf("testaccawsalb-basic-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName1 := "data.aws_alb.alb_test_with_arn" dataSourceName2 := "data.aws_alb.alb_test_with_name" - resourceName := "aws_alb.alb_test" + dataSourceName3 := "data.aws_alb.alb_test_with_tags" + resourceName := "aws_alb.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAWSLBConfigBackardsCompatibility(lbName), + Config: testAccDataSourceAWSLBConfigBackwardsCompatibility(rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName1, "name", resourceName, "name"), resource.TestCheckResourceAttrPair(dataSourceName1, "internal", resourceName, "internal"), resource.TestCheckResourceAttrPair(dataSourceName1, "subnets.#", resourceName, "subnets.#"), resource.TestCheckResourceAttrPair(dataSourceName1, "security_groups.#", resourceName, "security_groups.#"), resource.TestCheckResourceAttrPair(dataSourceName1, "tags.%", resourceName, "tags.%"), - resource.TestCheckResourceAttrPair(dataSourceName1, "tags.TestName", resourceName, "tags.TestName"), + resource.TestCheckResourceAttrPair(dataSourceName1, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName1, "tags.Config", resourceName, "tags.Config"), resource.TestCheckResourceAttrPair(dataSourceName1, "enable_deletion_protection", resourceName, "enable_deletion_protection"), resource.TestCheckResourceAttrPair(dataSourceName1, "idle_timeout", resourceName, "idle_timeout"), resource.TestCheckResourceAttrPair(dataSourceName1, "vpc_id", resourceName, "vpc_id"), @@ -123,7 +148,8 @@ func TestAccDataSourceAWSLB_BackwardsCompatibility(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName2, "subnets.#", resourceName, "subnets.#"), resource.TestCheckResourceAttrPair(dataSourceName2, "security_groups.#", resourceName, "security_groups.#"), resource.TestCheckResourceAttrPair(dataSourceName2, "tags.%", resourceName, "tags.%"), - resource.TestCheckResourceAttrPair(dataSourceName2, "tags.TestName", resourceName, "tags.TestName"), + resource.TestCheckResourceAttrPair(dataSourceName2, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName2, "tags.Config", resourceName, "tags.Config"), resource.TestCheckResourceAttrPair(dataSourceName2, "enable_deletion_protection", resourceName, "enable_deletion_protection"), resource.TestCheckResourceAttrPair(dataSourceName2, "idle_timeout", resourceName, "idle_timeout"), resource.TestCheckResourceAttrPair(dataSourceName2, "vpc_id", resourceName, "vpc_id"), @@ -132,69 +158,79 @@ func TestAccDataSourceAWSLB_BackwardsCompatibility(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName2, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceName2, "ip_address_type", resourceName, "ip_address_type"), resource.TestCheckResourceAttrPair(dataSourceName2, "subnet_mapping.#", resourceName, "subnet_mapping.#"), - resource.TestCheckResourceAttrPair(dataSourceName1, "drop_invalid_header_fields", resourceName, "drop_invalid_header_fields"), - resource.TestCheckResourceAttrPair(dataSourceName1, "enable_http2", resourceName, "enable_http2"), - resource.TestCheckResourceAttrPair(dataSourceName1, "access_logs.#", resourceName, "access_logs.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "drop_invalid_header_fields", resourceName, "drop_invalid_header_fields"), + resource.TestCheckResourceAttrPair(dataSourceName2, "enable_http2", resourceName, "enable_http2"), + resource.TestCheckResourceAttrPair(dataSourceName2, "access_logs.#", resourceName, "access_logs.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "internal", resourceName, "internal"), + resource.TestCheckResourceAttrPair(dataSourceName3, "subnets.#", resourceName, "subnets.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "security_groups.#", resourceName, "security_groups.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.Name", resourceName, "tags.Name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "tags.Config", resourceName, "tags.Config"), + resource.TestCheckResourceAttrPair(dataSourceName3, "enable_deletion_protection", resourceName, "enable_deletion_protection"), + resource.TestCheckResourceAttrPair(dataSourceName3, "idle_timeout", resourceName, "idle_timeout"), + resource.TestCheckResourceAttrPair(dataSourceName3, "vpc_id", resourceName, "vpc_id"), + resource.TestCheckResourceAttrPair(dataSourceName3, "zone_id", resourceName, "zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName3, "dns_name", resourceName, "dns_name"), + resource.TestCheckResourceAttrPair(dataSourceName3, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName3, "ip_address_type", resourceName, "ip_address_type"), + resource.TestCheckResourceAttrPair(dataSourceName3, "subnet_mapping.#", resourceName, "subnet_mapping.#"), + resource.TestCheckResourceAttrPair(dataSourceName3, "drop_invalid_header_fields", resourceName, "drop_invalid_header_fields"), + resource.TestCheckResourceAttrPair(dataSourceName3, "enable_http2", resourceName, "enable_http2"), + resource.TestCheckResourceAttrPair(dataSourceName3, "access_logs.#", resourceName, "access_logs.#"), ), }, }, }) } -func testAccDataSourceAWSLBConfigBasic(lbName string) string { - return fmt.Sprintf(` -resource "aws_lb" "alb_test" { - name = "%s" +func testAccDataSourceAWSLBConfigBasic(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_lb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.alb_test.id] - subnets = aws_subnet.alb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false tags = { - TestName = "TestAccAWSALB_basic" + Name = %[1]q + Config = "Basic" } } variable "subnets" { default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" + type = list(string) } -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_vpc" "alb_test" { +resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-lb-data-source-basic" + Name = %[1]q } } -resource "aws_subnet" "alb_test" { +resource "aws_subnet" "test" { count = 2 - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id cidr_block = element(var.subnets, count.index) map_public_ip_on_launch = true availability_zone = element(data.aws_availability_zones.available.names, count.index) tags = { - Name = "tf-acc-lb-data-source-basic" + Name = %[1]q } } -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" +resource "aws_security_group" "test" { + name = %[1]q description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id ingress { from_port = 0 @@ -211,21 +247,25 @@ resource "aws_security_group" "alb_test" { } tags = { - TestName = "TestAccAWSALB_basic" + Name = %[1]q } } data "aws_lb" "alb_test_with_arn" { - arn = aws_lb.alb_test.arn + arn = aws_lb.test.arn } data "aws_lb" "alb_test_with_name" { - name = aws_lb.alb_test.name + name = aws_lb.test.name } -`, lbName) + +data "aws_lb" "alb_test_with_tags" { + tags = aws_lb.test.tags +} +`, rName)) } -func testAccDataSourceAWSLBConfigOutpost(lbName string) string { +func testAccDataSourceAWSLBConfigOutpost(rName string) string { return fmt.Sprintf(` data "aws_outposts_outposts" "test" {} @@ -233,43 +273,44 @@ data "aws_outposts_outpost" "test" { id = tolist(data.aws_outposts_outposts.test.ids)[0] } -resource "aws_lb" "alb_test" { - name = "%s" +resource "aws_lb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.alb_test.id] - subnets = [aws_subnet.alb_test.id] + security_groups = [aws_security_group.test.id] + subnets = [aws_subnet.test.id] idle_timeout = 30 enable_deletion_protection = false tags = { - TestName = "TestAccAWSALB_outpost" + Name = %[1]q + Config = "Outposts" } } -resource "aws_vpc" "alb_test" { +resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-lb-data-source-outpost" + Name = %[1]q } } -resource "aws_subnet" "alb_test" { - vpc_id = aws_vpc.alb_test.id +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id cidr_block = "10.0.0.0/24" availability_zone = data.aws_outposts_outpost.test.availability_zone outpost_arn = data.aws_outposts_outpost.test.arn tags = { - Name = "tf-acc-lb-data-source-outpost" + Name = %[1]q } } -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" +resource "aws_security_group" "test" { + name = %[1]q description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id ingress { from_port = 0 @@ -286,70 +327,62 @@ resource "aws_security_group" "alb_test" { } tags = { - TestName = "TestAccAWSALB_outpost" + Name = %[1]q } } data "aws_lb" "alb_test_with_arn" { - arn = aws_lb.alb_test.arn + arn = aws_lb.test.arn } -`, lbName) +`, rName) } -func testAccDataSourceAWSLBConfigBackardsCompatibility(albName string) string { - return fmt.Sprintf(` -resource "aws_alb" "alb_test" { - name = "%s" +func testAccDataSourceAWSLBConfigBackwardsCompatibility(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_alb" "test" { + name = %[1]q internal = true - security_groups = [aws_security_group.alb_test.id] - subnets = aws_subnet.alb_test[*].id + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false tags = { - TestName = "TestAccAWSALB_basic" + Name = %[1]q + Config = "BackwardsCompatibility" } } variable "subnets" { default = ["10.0.1.0/24", "10.0.2.0/24"] - type = "list" -} - -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } + type = list(string) } -resource "aws_vpc" "alb_test" { +resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-lb-data-source-bc" + Name = %[1]q } } -resource "aws_subnet" "alb_test" { +resource "aws_subnet" "test" { count = 2 - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id cidr_block = element(var.subnets, count.index) map_public_ip_on_launch = true availability_zone = element(data.aws_availability_zones.available.names, count.index) tags = { - Name = "tf-acc-lb-data-source-bc" + Name = %[1]q } } -resource "aws_security_group" "alb_test" { - name = "allow_all_alb_test" +resource "aws_security_group" "test" { + name = %[1]q description = "Used for ALB Testing" - vpc_id = aws_vpc.alb_test.id + vpc_id = aws_vpc.test.id ingress { from_port = 0 @@ -366,16 +399,20 @@ resource "aws_security_group" "alb_test" { } tags = { - TestName = "TestAccAWSALB_basic" + Name = %[1]q } } data "aws_alb" "alb_test_with_arn" { - arn = aws_alb.alb_test.arn + arn = aws_alb.test.arn } data "aws_alb" "alb_test_with_name" { - name = aws_alb.alb_test.name + name = aws_alb.test.name +} + +data "aws_alb" "alb_test_with_tags" { + tags = aws_alb.test.tags } -`, albName) +`, rName)) } diff --git a/aws/data_source_aws_lex_bot_alias_test.go b/aws/data_source_aws_lex_bot_alias_test.go index 4498da3a41f4..622649097fa0 100644 --- a/aws/data_source_aws_lex_bot_alias_test.go +++ b/aws/data_source_aws_lex_bot_alias_test.go @@ -15,8 +15,9 @@ func testAccDataSourceAwsLexBotAlias_basic(t *testing.T) { // If this test runs in parallel with other Lex Bot tests, it loses its description resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( diff --git a/aws/data_source_aws_lex_bot_test.go b/aws/data_source_aws_lex_bot_test.go index 2d13bc19621a..a89236f27d2a 100644 --- a/aws/data_source_aws_lex_bot_test.go +++ b/aws/data_source_aws_lex_bot_test.go @@ -14,8 +14,9 @@ func TestAccDataSourceAwsLexBot_basic(t *testing.T) { resourceName := "aws_lex_bot.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( @@ -52,8 +53,9 @@ func testAccDataSourceAwsLexBot_withVersion(t *testing.T) { // If this test runs in parallel with other Lex Bot tests, it loses its description resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( diff --git a/aws/data_source_aws_lex_intent_test.go b/aws/data_source_aws_lex_intent_test.go index 2cca31c93e6b..e8a1e64e9173 100644 --- a/aws/data_source_aws_lex_intent_test.go +++ b/aws/data_source_aws_lex_intent_test.go @@ -14,8 +14,9 @@ func TestAccDataSourceAwsLexIntent_basic(t *testing.T) { resourceName := "aws_lex_intent.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( @@ -42,8 +43,9 @@ func TestAccDataSourceAwsLexIntent_withVersion(t *testing.T) { resourceName := "aws_lex_intent.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( diff --git a/aws/data_source_aws_lex_slot_type_test.go b/aws/data_source_aws_lex_slot_type_test.go index 0d2f7cffcd14..ae8a3e223d98 100644 --- a/aws/data_source_aws_lex_slot_type_test.go +++ b/aws/data_source_aws_lex_slot_type_test.go @@ -14,8 +14,9 @@ func TestAccDataSourceAwsLexSlotType_basic(t *testing.T) { resourceName := "aws_lex_slot_type.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( @@ -43,8 +44,9 @@ func TestAccDataSourceAwsLexSlotType_withVersion(t *testing.T) { resourceName := "aws_lex_slot_type.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lexmodelbuildingservice.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lexmodelbuildingservice.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: composeConfig( diff --git a/aws/data_source_aws_mq_broker.go b/aws/data_source_aws_mq_broker.go index 5edbe7bce309..d7d8b9c4c600 100644 --- a/aws/data_source_aws_mq_broker.go +++ b/aws/data_source_aws_mq_broker.go @@ -1,12 +1,13 @@ package aws import ( - "errors" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/mq" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func dataSourceAwsMqBroker() *schema.Resource { @@ -14,6 +15,18 @@ func dataSourceAwsMqBroker() *schema.Resource { Read: dataSourceAwsmQBrokerRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "authentication_strategy": { + Type: schema.TypeString, + Computed: true, + }, + "auto_minor_version_upgrade": { + Type: schema.TypeBool, + Computed: true, + }, "broker_id": { Type: schema.TypeString, Optional: true, @@ -26,14 +39,6 @@ func dataSourceAwsMqBroker() *schema.Resource { Computed: true, ConflictsWith: []string{"broker_id"}, }, - "auto_minor_version_upgrade": { - Type: schema.TypeBool, - Computed: true, - }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, "configuration": { Type: schema.TypeList, Computed: true, @@ -91,29 +96,74 @@ func dataSourceAwsMqBroker() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "endpoints": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "ip_address": { Type: schema.TypeString, Computed: true, }, - "endpoints": { + }, + }, + }, + "ldap_server_metadata": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hosts": { Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "role_base": { + Type: schema.TypeString, + Computed: true, + }, + "role_name": { + Type: schema.TypeString, + Computed: true, + }, + "role_search_matching": { + Type: schema.TypeString, + Computed: true, + }, + "role_search_subtree": { + Type: schema.TypeBool, + Computed: true, + }, + "service_account_password": { + Type: schema.TypeString, + Computed: true, + }, + "service_account_username": { + Type: schema.TypeString, + Computed: true, + }, + "user_base": { + Type: schema.TypeString, + Computed: true, + }, + "user_role_name": { + Type: schema.TypeString, + Computed: true, + }, + "user_search_matching": { + Type: schema.TypeString, + Computed: true, + }, + "user_search_subtree": { + Type: schema.TypeBool, + Computed: true, + }, }, }, }, "logs": { Type: schema.TypeList, - Optional: true, - MaxItems: 1, - // Ignore missing configuration block - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if old == "1" && new == "0" { - return true - } - return false - }, + Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "general": { @@ -121,7 +171,7 @@ func dataSourceAwsMqBroker() *schema.Resource { Computed: true, }, "audit": { - Type: schema.TypeBool, + Type: nullable.TypeNullableBool, Computed: true, }, }, @@ -156,6 +206,10 @@ func dataSourceAwsMqBroker() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, }, + "storage_type": { + Type: schema.TypeString, + Computed: true, + }, "subnet_ids": { Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, @@ -190,34 +244,114 @@ func dataSourceAwsMqBroker() *schema.Resource { } func dataSourceAwsmQBrokerRead(d *schema.ResourceData, meta interface{}) error { - if brokerId, ok := d.GetOk("broker_id"); ok { - d.SetId(brokerId.(string)) - } else { - conn := meta.(*AWSClient).mqconn - brokerName := d.Get("broker_name").(string) - var nextToken string - for { - out, err := conn.ListBrokers(&mq.ListBrokersInput{NextToken: aws.String(nextToken)}) - if err != nil { - return errors.New("Failed to list mq brokers") + conn := meta.(*AWSClient).mqconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &mq.ListBrokersInput{} + + var results []*mq.BrokerSummary + + err := conn.ListBrokersPages(input, func(page *mq.ListBrokersResponse, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, brokerSummary := range page.BrokerSummaries { + if brokerSummary == nil { + continue } - for _, broker := range out.BrokerSummaries { - if aws.StringValue(broker.BrokerName) == brokerName { - brokerId := aws.StringValue(broker.BrokerId) - d.Set("broker_id", brokerId) - d.SetId(brokerId) - } + + if v, ok := d.GetOk("broker_id"); ok && v.(string) != aws.StringValue(brokerSummary.BrokerId) { + continue } - if out.NextToken == nil { - break + + if v, ok := d.GetOk("broker_name"); ok && v.(string) != aws.StringValue(brokerSummary.BrokerName) { + continue } - nextToken = *out.NextToken - } - if d.Id() == "" { - return fmt.Errorf("Failed to determine mq broker: %s", brokerName) + results = append(results, brokerSummary) } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing MQ Brokers: %w", err) + } + + if len(results) != 1 { + return fmt.Errorf("Search returned %d results, please revise so only one is returned", len(results)) + } + + brokerId := aws.StringValue(results[0].BrokerId) + + output, err := conn.DescribeBroker(&mq.DescribeBrokerInput{ + BrokerId: aws.String(brokerId), + }) + + if err != nil { + return fmt.Errorf("error reading MQ broker (%s): %w", brokerId, err) + } + + if output == nil { + return fmt.Errorf("empty response while reading MQ broker (%s)", brokerId) + } + + d.SetId(brokerId) + + d.Set("arn", output.BrokerArn) + d.Set("authentication_strategy", output.AuthenticationStrategy) + d.Set("auto_minor_version_upgrade", output.AutoMinorVersionUpgrade) + d.Set("broker_id", brokerId) + d.Set("broker_name", output.BrokerName) + d.Set("deployment_mode", output.DeploymentMode) + d.Set("engine_type", output.EngineType) + d.Set("engine_version", output.EngineVersion) + d.Set("host_instance_type", output.HostInstanceType) + d.Set("instances", flattenMqBrokerInstances(output.BrokerInstances)) + d.Set("publicly_accessible", output.PubliclyAccessible) + d.Set("security_groups", aws.StringValueSlice(output.SecurityGroups)) + d.Set("storage_type", output.StorageType) + d.Set("subnet_ids", aws.StringValueSlice(output.SubnetIds)) + + if err := d.Set("configuration", flattenMqConfiguration(output.Configurations)); err != nil { + return fmt.Errorf("error setting configuration: %w", err) + } + + if err := d.Set("encryption_options", flattenMqEncryptionOptions(output.EncryptionOptions)); err != nil { + return fmt.Errorf("error setting encryption_options: %w", err) + } + + var password string + if v, ok := d.GetOk("ldap_server_metadata.0.service_account_password"); ok { + password = v.(string) + } + + if err := d.Set("ldap_server_metadata", flattenMQLDAPServerMetadata(output.LdapServerMetadata, password)); err != nil { + return fmt.Errorf("error setting ldap_server_metadata: %w", err) + } + + if err := d.Set("logs", flattenMqLogs(output.Logs)); err != nil { + return fmt.Errorf("error setting logs: %w", err) + } + + if err := d.Set("maintenance_window_start_time", flattenMqWeeklyStartTime(output.MaintenanceWindowStartTime)); err != nil { + return fmt.Errorf("error setting maintenance_window_start_time: %w", err) + } + + rawUsers, err := expandMqUsersForBroker(conn, brokerId, output.Users) + + if err != nil { + return fmt.Errorf("error retrieving user info for MQ broker (%s): %w", brokerId, err) + } + + if err := d.Set("user", flattenMqUsers(rawUsers, d.Get("user").(*schema.Set).List())); err != nil { + return fmt.Errorf("error setting user: %w", err) + } + + if err := d.Set("tags", keyvaluetags.MqKeyValueTags(output.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - return resourceAwsMqBrokerRead(d, meta) + return nil } diff --git a/aws/data_source_aws_mq_broker_test.go b/aws/data_source_aws_mq_broker_test.go index 523ebc36829d..089d4bcefcd6 100644 --- a/aws/data_source_aws_mq_broker_test.go +++ b/aws/data_source_aws_mq_broker_test.go @@ -19,14 +19,16 @@ func TestAccDataSourceAWSMqBroker_basic(t *testing.T) { resourceName := "aws_mq_broker.acctest" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(mq.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(mq.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, mq.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSMqBrokerConfig_byId(brokerName, prefix), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceByIdName, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "broker_name", resourceName, "broker_name"), + resource.TestCheckResourceAttrPair(dataSourceByIdName, "authentication_strategy", resourceName, "authentication_strategy"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "auto_minor_version_upgrade", resourceName, "auto_minor_version_upgrade"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "deployment_mode", resourceName, "deployment_mode"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "configuration.#", resourceName, "configuration.#"), @@ -40,6 +42,7 @@ func TestAccDataSourceAWSMqBroker_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceByIdName, "maintenance_window_start_time.#", resourceName, "maintenance_window_start_time.#"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "publicly_accessible", resourceName, "publicly_accessible"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "security_groups.#", resourceName, "security_groups.#"), + resource.TestCheckResourceAttrPair(dataSourceByIdName, "storage_type", resourceName, "storage_type"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "subnet_ids.#", resourceName, "subnet_ids.#"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "tags.%", resourceName, "tags.%"), resource.TestCheckResourceAttrPair(dataSourceByIdName, "user.#", resourceName, "user.#"), diff --git a/aws/data_source_aws_msk_broker_nodes.go b/aws/data_source_aws_msk_broker_nodes.go new file mode 100644 index 000000000000..ed63cffb4bf8 --- /dev/null +++ b/aws/data_source_aws_msk_broker_nodes.go @@ -0,0 +1,113 @@ +package aws + +import ( + "fmt" + "sort" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kafka" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsMskBrokerNodes() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsMskBrokerNodesRead, + + Schema: map[string]*schema.Schema{ + "cluster_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + + "node_info_list": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "attached_eni_id": { + Type: schema.TypeString, + Computed: true, + }, + "broker_id": { + Type: schema.TypeFloat, + Computed: true, + }, + "client_subnet": { + Type: schema.TypeString, + Computed: true, + }, + "client_vpc_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "endpoints": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "node_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceAwsMskBrokerNodesRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kafkaconn + + clusterARN := d.Get("cluster_arn").(string) + input := &kafka.ListNodesInput{ + ClusterArn: aws.String(clusterARN), + } + var nodeInfos []*kafka.NodeInfo + + err := conn.ListNodesPages(input, func(page *kafka.ListNodesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + nodeInfos = append(nodeInfos, page.NodeInfoList...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing MSK Cluster (%s) Broker Nodes: %w", clusterARN, err) + } + + // node list is returned unsorted sort on broker id + sort.Slice(nodeInfos, func(i, j int) bool { + iBrokerId := aws.Float64Value(nodeInfos[i].BrokerNodeInfo.BrokerId) + jBrokerId := aws.Float64Value(nodeInfos[j].BrokerNodeInfo.BrokerId) + return iBrokerId < jBrokerId + }) + + tfList := make([]interface{}, len(nodeInfos)) + + for i, apiObject := range nodeInfos { + brokerNodeInfo := apiObject.BrokerNodeInfo + tfMap := map[string]interface{}{ + "attached_eni_id": aws.StringValue(brokerNodeInfo.AttachedENIId), + "broker_id": aws.Float64Value(brokerNodeInfo.BrokerId), + "client_subnet": aws.StringValue(brokerNodeInfo.ClientSubnet), + "client_vpc_ip_address": aws.StringValue(brokerNodeInfo.ClientVpcIpAddress), + "endpoints": aws.StringValueSlice(brokerNodeInfo.Endpoints), + "node_arn": aws.StringValue(apiObject.NodeARN), + } + + tfList[i] = tfMap + } + + d.SetId(clusterARN) + + if err := d.Set("node_info_list", tfList); err != nil { + return fmt.Errorf("error setting node_info_list: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_msk_broker_nodes_test.go b/aws/data_source_aws_msk_broker_nodes_test.go new file mode 100644 index 000000000000..ed529018d06c --- /dev/null +++ b/aws/data_source_aws_msk_broker_nodes_test.go @@ -0,0 +1,59 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/kafka" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSMskBrokerNodesDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_msk_broker_nodes.test" + resourceName := "aws_msk_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSMsk(t) }, + ErrorCheck: testAccErrorCheck(t, kafka.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckMskClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMskBrokerNodesDataSourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "node_info_list.#", resourceName, "number_of_broker_nodes"), + resource.TestCheckResourceAttr(dataSourceName, "node_info_list.0.broker_id", "1"), + resource.TestCheckResourceAttr(dataSourceName, "node_info_list.1.broker_id", "2"), + resource.TestCheckResourceAttr(dataSourceName, "node_info_list.2.broker_id", "3"), + ), + }, + }, + }) +} +func testAccMskBrokerNodesDataSourceConfig(rName string) string { + return composeConfig(testAccMskClusterBaseConfig(rName), fmt.Sprintf(` +resource "aws_msk_cluster" "test" { + cluster_name = %[1]q + kafka_version = "2.2.1" + number_of_broker_nodes = 3 + + broker_node_group_info { + client_subnets = [aws_subnet.example_subnet_az1.id, aws_subnet.example_subnet_az2.id, aws_subnet.example_subnet_az3.id] + ebs_volume_size = 10 + instance_type = "kafka.t3.small" + security_groups = [aws_security_group.example_sg.id] + } + + tags = { + foo = "bar" + } +} + +data "aws_msk_broker_nodes" "test" { + cluster_arn = aws_msk_cluster.test.arn +} +`, rName)) +} diff --git a/aws/data_source_aws_msk_cluster.go b/aws/data_source_aws_msk_cluster.go index ac8a20a8c26f..53973ad07017 100644 --- a/aws/data_source_aws_msk_cluster.go +++ b/aws/data_source_aws_msk_cluster.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfkafka "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kafka" ) func dataSourceAwsMskCluster() *schema.Resource { @@ -23,6 +24,10 @@ func dataSourceAwsMskCluster() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "bootstrap_brokers_sasl_iam": { + Type: schema.TypeString, + Computed: true, + }, "bootstrap_brokers_sasl_scram": { Type: schema.TypeString, Computed: true, @@ -57,64 +62,60 @@ func dataSourceAwsMskClusterRead(d *schema.ResourceData, meta interface{}) error conn := meta.(*AWSClient).kafkaconn ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - listClustersInput := &kafka.ListClustersInput{ - ClusterNameFilter: aws.String(d.Get("cluster_name").(string)), + clusterName := d.Get("cluster_name").(string) + input := &kafka.ListClustersInput{ + ClusterNameFilter: aws.String(clusterName), } + var cluster *kafka.ClusterInfo - var clusters []*kafka.ClusterInfo - for { - listClustersOutput, err := conn.ListClusters(listClustersInput) - - if err != nil { - return fmt.Errorf("error listing MSK Clusters: %w", err) + err := conn.ListClustersPages(input, func(page *kafka.ListClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - if listClustersOutput == nil { - break - } + for _, clusterInfo := range page.ClusterInfoList { + if aws.StringValue(clusterInfo.ClusterName) == clusterName { + cluster = clusterInfo - clusters = append(clusters, listClustersOutput.ClusterInfoList...) - - if aws.StringValue(listClustersOutput.NextToken) == "" { - break + return false + } } - listClustersInput.NextToken = listClustersOutput.NextToken - } + return !lastPage + }) - if len(clusters) == 0 { - return fmt.Errorf("error reading MSK Cluster: no results found") + if err != nil { + return fmt.Errorf("error listing MSK Clusters: %w", err) } - if len(clusters) > 1 { - return fmt.Errorf("error reading MSK Cluster: multiple results found, try adjusting search criteria") + if cluster == nil { + return fmt.Errorf("error reading MSK Cluster (%s): no results found", clusterName) } - cluster := clusters[0] - bootstrapBrokersInput := &kafka.GetBootstrapBrokersInput{ ClusterArn: cluster.ClusterArn, } - bootstrapBrokersoOutput, err := conn.GetBootstrapBrokers(bootstrapBrokersInput) + bootstrapBrokersOutput, err := conn.GetBootstrapBrokers(bootstrapBrokersInput) if err != nil { return fmt.Errorf("error reading MSK Cluster (%s) bootstrap brokers: %w", aws.StringValue(cluster.ClusterArn), err) } - d.Set("arn", aws.StringValue(cluster.ClusterArn)) - d.Set("bootstrap_brokers", aws.StringValue(bootstrapBrokersoOutput.BootstrapBrokerString)) - d.Set("bootstrap_brokers_sasl_scram", aws.StringValue(bootstrapBrokersoOutput.BootstrapBrokerStringSaslScram)) - d.Set("bootstrap_brokers_tls", aws.StringValue(bootstrapBrokersoOutput.BootstrapBrokerStringTls)) - d.Set("cluster_name", aws.StringValue(cluster.ClusterName)) - d.Set("kafka_version", aws.StringValue(cluster.CurrentBrokerSoftwareInfo.KafkaVersion)) - d.Set("number_of_broker_nodes", aws.Int64Value(cluster.NumberOfBrokerNodes)) + d.Set("arn", cluster.ClusterArn) + d.Set("bootstrap_brokers", tfkafka.SortEndpointsString(aws.StringValue(bootstrapBrokersOutput.BootstrapBrokerString))) + d.Set("bootstrap_brokers_sasl_iam", tfkafka.SortEndpointsString(aws.StringValue(bootstrapBrokersOutput.BootstrapBrokerStringSaslIam))) + d.Set("bootstrap_brokers_sasl_scram", tfkafka.SortEndpointsString(aws.StringValue(bootstrapBrokersOutput.BootstrapBrokerStringSaslScram))) + d.Set("bootstrap_brokers_tls", tfkafka.SortEndpointsString(aws.StringValue(bootstrapBrokersOutput.BootstrapBrokerStringTls))) + d.Set("cluster_name", cluster.ClusterName) + d.Set("kafka_version", cluster.CurrentBrokerSoftwareInfo.KafkaVersion) + d.Set("number_of_broker_nodes", cluster.NumberOfBrokerNodes) if err := d.Set("tags", keyvaluetags.KafkaKeyValueTags(cluster.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } - d.Set("zookeeper_connect_string", aws.StringValue(cluster.ZookeeperConnectString)) + d.Set("zookeeper_connect_string", tfkafka.SortEndpointsString(aws.StringValue(cluster.ZookeeperConnectString))) d.SetId(aws.StringValue(cluster.ClusterArn)) diff --git a/aws/data_source_aws_msk_cluster_test.go b/aws/data_source_aws_msk_cluster_test.go index cb20092a3740..1b08e324edd5 100644 --- a/aws/data_source_aws_msk_cluster_test.go +++ b/aws/data_source_aws_msk_cluster_test.go @@ -2,43 +2,44 @@ package aws import ( "fmt" - "regexp" "testing" + "github.com/aws/aws-sdk-go/service/kafka" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSMskClusterDataSource_Name(t *testing.T) { +func TestAccAWSMskClusterDataSource_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") dataSourceName := "data.aws_msk_cluster.test" resourceName := "aws_msk_cluster.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSMsk(t) }, + ErrorCheck: testAccErrorCheck(t, kafka.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckMskClusterDestroy, Steps: []resource.TestStep{ { - Config: testAccMskClusterDataSourceConfigName(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), - resource.TestCheckResourceAttr(resourceName, "bootstrap_brokers", ""), - resource.TestCheckResourceAttrPair(resourceName, "bootstrap_brokers_sasl_scram", dataSourceName, "bootstrap_brokers_sasl_scram"), - resource.TestMatchResourceAttr(resourceName, "bootstrap_brokers_tls", regexp.MustCompile(`^(([-\w]+\.){1,}[\w]+:\d+,){2,}([-\w]+\.){1,}[\w]+:\d+$`)), // Hostname ordering not guaranteed between resource and data source reads - resource.TestCheckResourceAttrPair(resourceName, "cluster_name", dataSourceName, "cluster_name"), - resource.TestCheckResourceAttrPair(resourceName, "kafka_version", dataSourceName, "kafka_version"), - resource.TestCheckResourceAttrPair(resourceName, "number_of_broker_nodes", dataSourceName, "number_of_broker_nodes"), - resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), - resource.TestCheckResourceAttrPair(resourceName, "zookeeper_connect_string", dataSourceName, "zookeeper_connect_string"), + Config: testAccMskClusterDataSourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "bootstrap_brokers", resourceName, "bootstrap_brokers"), + resource.TestCheckResourceAttrPair(dataSourceName, "bootstrap_brokers_sasl_scram", resourceName, "bootstrap_brokers_sasl_scram"), + resource.TestCheckResourceAttrPair(dataSourceName, "bootstrap_brokers_tls", resourceName, "bootstrap_brokers_tls"), + resource.TestCheckResourceAttrPair(dataSourceName, "cluster_name", resourceName, "cluster_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "kafka_version", resourceName, "kafka_version"), + resource.TestCheckResourceAttrPair(dataSourceName, "number_of_broker_nodes", resourceName, "number_of_broker_nodes"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName, "zookeeper_connect_string", resourceName, "zookeeper_connect_string"), ), }, }, }) } -func testAccMskClusterDataSourceConfigName(rName string) string { - return testAccMskClusterBaseConfig() + fmt.Sprintf(` +func testAccMskClusterDataSourceConfig(rName string) string { + return composeConfig(testAccMskClusterBaseConfig(rName), fmt.Sprintf(` resource "aws_msk_cluster" "test" { cluster_name = %[1]q kafka_version = "2.2.1" @@ -52,12 +53,12 @@ resource "aws_msk_cluster" "test" { } tags = { - foo = "bar" + key1 = "value1" } } data "aws_msk_cluster" "test" { cluster_name = aws_msk_cluster.test.cluster_name } -`, rName) +`, rName)) } diff --git a/aws/data_source_aws_msk_configuration.go b/aws/data_source_aws_msk_configuration.go index 59cb35b3ce64..4f76bc102711 100644 --- a/aws/data_source_aws_msk_configuration.go +++ b/aws/data_source_aws_msk_configuration.go @@ -91,15 +91,15 @@ func dataSourceAwsMskConfigurationRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error describing MSK Configuration (%s) Revision (%d): missing result", d.Id(), aws.Int64Value(revision)) } - d.Set("arn", aws.StringValue(configuration.Arn)) - d.Set("description", aws.StringValue(configuration.Description)) + d.Set("arn", configuration.Arn) + d.Set("description", configuration.Description) if err := d.Set("kafka_versions", aws.StringValueSlice(configuration.KafkaVersions)); err != nil { return fmt.Errorf("error setting kafka_versions: %w", err) } - d.Set("latest_revision", aws.Int64Value(revision)) - d.Set("name", aws.StringValue(configuration.Name)) + d.Set("latest_revision", revision) + d.Set("name", configuration.Name) d.Set("server_properties", string(revisionOutput.ServerProperties)) d.SetId(aws.StringValue(configuration.Arn)) diff --git a/aws/data_source_aws_msk_configuration_test.go b/aws/data_source_aws_msk_configuration_test.go index 7211f6ea51fa..cd9fbcb0576a 100644 --- a/aws/data_source_aws_msk_configuration_test.go +++ b/aws/data_source_aws_msk_configuration_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/kafka" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,6 +16,7 @@ func TestAccAWSMskConfigurationDataSource_Name(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kafka.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckMskConfigurationDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_msk_kafka_version.go b/aws/data_source_aws_msk_kafka_version.go new file mode 100644 index 000000000000..d40fb088ed8f --- /dev/null +++ b/aws/data_source_aws_msk_kafka_version.go @@ -0,0 +1,103 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kafka" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsMskKafkaVersion() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsMskKafkaVersionRead, + + Schema: map[string]*schema.Schema{ + "preferred_versions": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ExactlyOneOf: []string{"version", "preferred_versions"}, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"version", "preferred_versions"}, + }, + }, + } +} + +func findMskKafkaVersion(preferredVersions []interface{}, versions []*kafka.KafkaVersion) *kafka.KafkaVersion { + var found *kafka.KafkaVersion + + for _, v := range preferredVersions { + preferredVersion, ok := v.(string) + + if !ok { + continue + } + + for _, kafkaVersion := range versions { + if preferredVersion == aws.StringValue(kafkaVersion.Version) { + found = kafkaVersion + + break + } + } + + if found != nil { + break + } + } + + return found +} + +func dataSourceAwsMskKafkaVersionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).kafkaconn + + var kafkaVersions []*kafka.KafkaVersion + + err := conn.ListKafkaVersionsPages(&kafka.ListKafkaVersionsInput{}, func(page *kafka.ListKafkaVersionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + kafkaVersions = append(kafkaVersions, page.KafkaVersions...) + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing Kafka versions: %w", err) + } + + if len(kafkaVersions) == 0 { + return fmt.Errorf("no Kafka versions found") + } + + var found *kafka.KafkaVersion + + if v, ok := d.GetOk("preferred_versions"); ok { + found = findMskKafkaVersion(v.([]interface{}), kafkaVersions) + } else if v, ok := d.GetOk("version"); ok { + found = findMskKafkaVersion([]interface{}{v}, kafkaVersions) + } + + if found == nil { + return fmt.Errorf("no Kafka versions match the criteria") + } + + d.SetId(aws.StringValue(found.Version)) + + d.Set("status", found.Status) + d.Set("version", found.Version) + + return nil +} diff --git a/aws/data_source_aws_msk_kafka_version_test.go b/aws/data_source_aws_msk_kafka_version_test.go new file mode 100644 index 000000000000..9f848b7f3ea2 --- /dev/null +++ b/aws/data_source_aws_msk_kafka_version_test.go @@ -0,0 +1,82 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/kafka" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSMskKafkaVersionDataSource_basic(t *testing.T) { + dataSourceName := "data.aws_msk_kafka_version.test" + version := "2.4.1.1" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAWSMskKafkaVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kafka.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSMskKafkaVersionDataSourceBasicConfig(version), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "version", version), + resource.TestCheckResourceAttrSet(dataSourceName, "status"), + ), + }, + }, + }) +} + +func TestAccAWSMskKafkaVersionDataSource_preferred(t *testing.T) { + dataSourceName := "data.aws_msk_kafka_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAWSMskKafkaVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, kafka.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSMskKafkaVersionDataSourcePreferredConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "version", "2.4.1.1"), + resource.TestCheckResourceAttrSet(dataSourceName, "status"), + ), + }, + }, + }) +} + +func testAccAWSMskKafkaVersionPreCheck(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).kafkaconn + + input := &kafka.ListKafkaVersionsInput{} + + _, err := conn.ListKafkaVersions(input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSMskKafkaVersionDataSourceBasicConfig(version string) string { + return fmt.Sprintf(` +data "aws_msk_kafka_version" "test" { + version = %[1]q +} +`, version) +} + +func testAccAWSMskKafkaVersionDataSourcePreferredConfig() string { + return ` +data "aws_msk_kafka_version" "test" { + preferred_versions = ["2.4.1.1", "2.4.1", "2.2.1"] +} +` +} diff --git a/aws/data_source_aws_nat_gateway.go b/aws/data_source_aws_nat_gateway.go index 30f743b7fd3f..dd28248e69f6 100644 --- a/aws/data_source_aws_nat_gateway.go +++ b/aws/data_source_aws_nat_gateway.go @@ -39,6 +39,10 @@ func dataSourceAwsNatGateway() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "connectivity_type": { + Type: schema.TypeString, + Computed: true, + }, "network_interface_id": { Type: schema.TypeString, Computed: true, @@ -121,6 +125,7 @@ func dataSourceAwsNatGatewayRead(d *schema.ResourceData, meta interface{}) error log.Printf("[DEBUG] NAT Gateway response: %s", ngw) d.SetId(aws.StringValue(ngw.NatGatewayId)) + d.Set("connectivity_type", ngw.ConnectivityType) d.Set("state", ngw.State) d.Set("subnet_id", ngw.SubnetId) d.Set("vpc_id", ngw.VpcId) @@ -130,7 +135,7 @@ func dataSourceAwsNatGatewayRead(d *schema.ResourceData, meta interface{}) error } for _, address := range ngw.NatGatewayAddresses { - if *address.AllocationId != "" { + if aws.StringValue(address.AllocationId) != "" { d.Set("allocation_id", address.AllocationId) d.Set("network_interface_id", address.NetworkInterfaceId) d.Set("private_ip", address.PrivateIp) diff --git a/aws/data_source_aws_nat_gateway_test.go b/aws/data_source_aws_nat_gateway_test.go index 8fdfba88cc11..2bae1302138b 100644 --- a/aws/data_source_aws_nat_gateway_test.go +++ b/aws/data_source_aws_nat_gateway_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,12 +14,14 @@ func TestAccDataSourceAwsNatGateway_basic(t *testing.T) { rInt := acctest.RandIntRange(4, 254) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsNatGatewayConfig(rInt), Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("data.aws_nat_gateway.test_by_id", "connectivity_type", "aws_nat_gateway.test", "connectivity_type"), resource.TestCheckResourceAttrPair( "data.aws_nat_gateway.test_by_id", "id", "aws_nat_gateway.test", "id"), diff --git a/aws/data_source_aws_neptune_engine_version_test.go b/aws/data_source_aws_neptune_engine_version_test.go index fd3f3b46e6a1..faf2fc8c395a 100644 --- a/aws/data_source_aws_neptune_engine_version_test.go +++ b/aws/data_source_aws_neptune_engine_version_test.go @@ -12,10 +12,11 @@ import ( func TestAccAWSNeptuneEngineVersionDataSource_basic(t *testing.T) { dataSourceName := "data.aws_neptune_engine_version.test" - version := "1.0.1.0" + version := "1.0.2.1" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSNeptuneEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, neptune.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -43,6 +44,7 @@ func TestAccAWSNeptuneEngineVersionDataSource_preferred(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSNeptuneEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, neptune.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -62,6 +64,7 @@ func TestAccAWSNeptuneEngineVersionDataSource_defaultOnly(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSNeptuneEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, neptune.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_neptune_orderable_db_instance_test.go b/aws/data_source_aws_neptune_orderable_db_instance_test.go index aca11e22234e..420bfae1a8a1 100644 --- a/aws/data_source_aws_neptune_orderable_db_instance_test.go +++ b/aws/data_source_aws_neptune_orderable_db_instance_test.go @@ -18,6 +18,7 @@ func TestAccAWSNeptuneOrderableDbInstanceDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSNeptuneOrderableDbInstance(t) }, + ErrorCheck: testAccErrorCheck(t, neptune.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -43,6 +44,7 @@ func TestAccAWSNeptuneOrderableDbInstanceDataSource_preferred(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSNeptuneOrderableDbInstance(t) }, + ErrorCheck: testAccErrorCheck(t, neptune.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_network_acls_test.go b/aws/data_source_aws_network_acls_test.go index e153fcbf3d86..b4bb0b89536d 100644 --- a/aws/data_source_aws_network_acls_test.go +++ b/aws/data_source_aws_network_acls_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,6 +16,7 @@ func TestAccDataSourceAwsNetworkAcls_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -39,6 +41,7 @@ func TestAccDataSourceAwsNetworkAcls_Filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -58,6 +61,7 @@ func TestAccDataSourceAwsNetworkAcls_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -77,6 +81,7 @@ func TestAccDataSourceAwsNetworkAcls_VpcID(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_network_interface_test.go b/aws/data_source_aws_network_interface_test.go index f5f101db7b35..c609fbcab6e5 100644 --- a/aws/data_source_aws_network_interface_test.go +++ b/aws/data_source_aws_network_interface_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsNetworkInterface_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsNetworkInterfaceConfigBasic(rName), @@ -41,8 +43,9 @@ func TestAccDataSourceAwsNetworkInterface_filters(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsNetworkInterfaceConfigFilters(rName), @@ -65,8 +68,9 @@ func TestAccDataSourceAwsNetworkInterface_CarrierIPAssociation(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsNetworkInterfaceConfigCarrierIPAssociation(rName), @@ -112,8 +116,9 @@ func TestAccDataSourceAwsNetworkInterface_PublicIPAssociation(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsNetworkInterfaceConfigPublicIPAssociation(rName), diff --git a/aws/data_source_aws_network_interfaces_test.go b/aws/data_source_aws_network_interfaces_test.go index d1341e75d284..c11165ebfdb3 100644 --- a/aws/data_source_aws_network_interfaces_test.go +++ b/aws/data_source_aws_network_interfaces_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccDataSourceAwsNetworkInterfaces_Filter(t *testing.T) { rName := acctest.RandString(5) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -29,6 +31,7 @@ func TestAccDataSourceAwsNetworkInterfaces_Tags(t *testing.T) { rName := acctest.RandString(5) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_organizations_delegated_administrators.go b/aws/data_source_aws_organizations_delegated_administrators.go new file mode 100644 index 000000000000..d4c47c743cd9 --- /dev/null +++ b/aws/data_source_aws_organizations_delegated_administrators.go @@ -0,0 +1,120 @@ +package aws + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/organizations" + "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" +) + +func dataSourceAwsOrganizationsDelegatedAdministrators() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceAwsOrganizationsDelegatedAdministratorsRead, + Schema: map[string]*schema.Schema{ + "service_principal": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "delegated_administrators": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "delegation_enabled_date": { + Type: schema.TypeString, + Computed: true, + }, + "email": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "joined_method": { + Type: schema.TypeString, + Computed: true, + }, + "joined_timestamp": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceAwsOrganizationsDelegatedAdministratorsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).organizationsconn + + input := &organizations.ListDelegatedAdministratorsInput{} + + if v, ok := d.GetOk("service_principal"); ok { + input.ServicePrincipal = aws.String(v.(string)) + } + + var delegators []*organizations.DelegatedAdministrator + + err := conn.ListDelegatedAdministratorsPagesWithContext(ctx, input, func(page *organizations.ListDelegatedAdministratorsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + delegators = append(delegators, page.DelegatedAdministrators...) + + return !lastPage + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error describing organizations delegated Administrators: %w", err)) + } + + if err = d.Set("delegated_administrators", flattenOrganizationsDelegatedAdministrators(delegators)); err != nil { + return diag.FromErr(fmt.Errorf("error setting delegated_administrators: %w", err)) + } + + d.SetId(meta.(*AWSClient).accountid) + + return nil +} + +func flattenOrganizationsDelegatedAdministrators(delegatedAdministrators []*organizations.DelegatedAdministrator) []map[string]interface{} { + if len(delegatedAdministrators) == 0 { + return nil + } + + var result []map[string]interface{} + for _, delegated := range delegatedAdministrators { + result = append(result, map[string]interface{}{ + "arn": aws.StringValue(delegated.Arn), + "delegation_enabled_date": aws.TimeValue(delegated.DelegationEnabledDate).Format(time.RFC3339), + "email": aws.StringValue(delegated.Email), + "id": aws.StringValue(delegated.Id), + "joined_method": aws.StringValue(delegated.JoinedMethod), + "joined_timestamp": aws.TimeValue(delegated.JoinedTimestamp).Format(time.RFC3339), + "name": aws.StringValue(delegated.Name), + "status": aws.StringValue(delegated.Status), + }) + } + return result +} diff --git a/aws/data_source_aws_organizations_delegated_administrators_test.go b/aws/data_source_aws_organizations_delegated_administrators_test.go new file mode 100644 index 000000000000..63a414baa0b3 --- /dev/null +++ b/aws/data_source_aws_organizations_delegated_administrators_test.go @@ -0,0 +1,171 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/organizations" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccDataSourceAwsOrganizationsDelegatedAdministrators_basic(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_administrators.test" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + dataSourceIdentity := "data.aws_caller_identity.delegated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedAdministratorsConfig(servicePrincipal), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_administrators.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "delegated_administrators.0.id", dataSourceIdentity, "account_id"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.delegation_enabled_date"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.joined_timestamp"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsOrganizationsDelegatedAdministrators_multiple(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_administrators.test" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + servicePrincipal2 := "config.amazonaws.com" + dataSourceIdentity := "data.aws_caller_identity.delegated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedAdministratorsMultipleConfig(servicePrincipal, servicePrincipal2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_administrators.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "delegated_administrators.0.id", dataSourceIdentity, "account_id"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.delegation_enabled_date"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.joined_timestamp"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsOrganizationsDelegatedAdministrators_servicePrincipal(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_administrators.test" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + dataSourceIdentity := "data.aws_caller_identity.delegated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedAdministratorsServicePrincipalConfig(servicePrincipal), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_administrators.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "delegated_administrators.0.id", dataSourceIdentity, "account_id"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.delegation_enabled_date"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_administrators.0.joined_timestamp"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsOrganizationsDelegatedAdministrators_empty(t *testing.T) { + dataSourceName := "data.aws_organizations_delegated_administrators.test" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedAdministratorsEmptyConfig(servicePrincipal), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_administrators.#", "0"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsOrganizationsDelegatedAdministratorsEmptyConfig(servicePrincipal string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_organizations_delegated_administrators" "test" { + service_principal = %[1]q +} +`, servicePrincipal) +} + +func testAccDataSourceAwsOrganizationsDelegatedAdministratorsConfig(servicePrincipal string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +resource "aws_organizations_delegated_administrator" "test" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[1]q +} + +data "aws_organizations_delegated_administrators" "test" {} +`, servicePrincipal) +} + +func testAccDataSourceAwsOrganizationsDelegatedAdministratorsMultipleConfig(servicePrincipal, servicePrincipal2 string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +resource "aws_organizations_delegated_administrator" "delegated" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[1]q +} + +resource "aws_organizations_delegated_administrator" "other_delegated" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[2]q +} + +data "aws_organizations_delegated_administrators" "test" {} +`, servicePrincipal, servicePrincipal2) +} + +func testAccDataSourceAwsOrganizationsDelegatedAdministratorsServicePrincipalConfig(servicePrincipal string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +resource "aws_organizations_delegated_administrator" "test" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[1]q +} + +data "aws_organizations_delegated_administrators" "test" { + service_principal = aws_organizations_delegated_administrator.test.service_principal +} +`, servicePrincipal) +} diff --git a/aws/data_source_aws_organizations_delegated_services.go b/aws/data_source_aws_organizations_delegated_services.go new file mode 100644 index 000000000000..c5b673537228 --- /dev/null +++ b/aws/data_source_aws_organizations_delegated_services.go @@ -0,0 +1,86 @@ +package aws + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/organizations" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsOrganizationsDelegatedServices() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceAwsOrganizationsDelegatedServicesRead, + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateAwsAccountId, + }, + "delegated_services": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delegation_enabled_date": { + Type: schema.TypeString, + Computed: true, + }, + "service_principal": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceAwsOrganizationsDelegatedServicesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).organizationsconn + + input := &organizations.ListDelegatedServicesForAccountInput{ + AccountId: aws.String(d.Get("account_id").(string)), + } + + var delegators []*organizations.DelegatedService + err := conn.ListDelegatedServicesForAccountPagesWithContext(ctx, input, func(page *organizations.ListDelegatedServicesForAccountOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + delegators = append(delegators, page.DelegatedServices...) + + return !lastPage + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error describing organizations delegated services: %w", err)) + } + + if err = d.Set("delegated_services", flattenOrganizationsDelegatedServices(delegators)); err != nil { + return diag.FromErr(fmt.Errorf("error setting delegated_services: %w", err)) + } + + d.SetId(meta.(*AWSClient).accountid) + + return nil +} + +func flattenOrganizationsDelegatedServices(delegatedServices []*organizations.DelegatedService) []map[string]interface{} { + if len(delegatedServices) == 0 { + return nil + } + + var result []map[string]interface{} + for _, delegated := range delegatedServices { + result = append(result, map[string]interface{}{ + "delegation_enabled_date": aws.TimeValue(delegated.DelegationEnabledDate).Format(time.RFC3339), + "service_principal": aws.StringValue(delegated.ServicePrincipal), + }) + } + return result +} diff --git a/aws/data_source_aws_organizations_delegated_services_test.go b/aws/data_source_aws_organizations_delegated_services_test.go new file mode 100644 index 000000000000..0e320092d84d --- /dev/null +++ b/aws/data_source_aws_organizations_delegated_services_test.go @@ -0,0 +1,142 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/organizations" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccDataSourceAwsOrganizationsDelegatedServices_basic(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_services.test" + dataSourceIdentity := "data.aws_caller_identity.delegated" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedServicesConfig(servicePrincipal), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "account_id", dataSourceIdentity, "account_id"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_services.0.delegation_enabled_date"), + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.0.service_principal", servicePrincipal), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsOrganizationsDelegatedServices_empty(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_services.test" + dataSourceIdentity := "data.aws_caller_identity.delegated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedServicesEmptyConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.#", "0"), + resource.TestCheckResourceAttrPair(dataSourceName, "account_id", dataSourceIdentity, "account_id"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsOrganizationsDelegatedServices_multiple(t *testing.T) { + var providers []*schema.Provider + dataSourceName := "data.aws_organizations_delegated_services.test" + dataSourceIdentity := "data.aws_caller_identity.delegated" + servicePrincipal := "config-multiaccountsetup.amazonaws.com" + servicePrincipal2 := "config.amazonaws.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + ProviderFactories: testAccProviderFactoriesAlternate(&providers), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsOrganizationsDelegatedServicesMultipleConfig(servicePrincipal, servicePrincipal2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.#", "2"), + resource.TestCheckResourceAttrPair(dataSourceName, "account_id", dataSourceIdentity, "account_id"), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_services.0.delegation_enabled_date"), + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.0.service_principal", servicePrincipal), + testAccCheckResourceAttrRfc3339(dataSourceName, "delegated_services.1.delegation_enabled_date"), + resource.TestCheckResourceAttr(dataSourceName, "delegated_services.1.service_principal", servicePrincipal2), + ), + }, + }, + }) +} + +func testAccDataSourceAwsOrganizationsDelegatedServicesEmptyConfig() string { + return testAccAlternateAccountProviderConfig() + ` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +data "aws_organizations_delegated_services" "test" { + account_id = data.aws_caller_identity.delegated.account_id +} +` +} + +func testAccDataSourceAwsOrganizationsDelegatedServicesConfig(servicePrincipal string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +resource "aws_organizations_delegated_administrator" "delegated" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[1]q +} + +data "aws_organizations_delegated_services" "test" { + account_id = aws_organizations_delegated_administrator.delegated.account_id +} +`, servicePrincipal) +} + +func testAccDataSourceAwsOrganizationsDelegatedServicesMultipleConfig(servicePrincipal, servicePrincipal2 string) string { + return testAccAlternateAccountProviderConfig() + fmt.Sprintf(` +data "aws_caller_identity" "delegated" { + provider = "awsalternate" +} + +resource "aws_organizations_delegated_administrator" "delegated" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[1]q +} + +resource "aws_organizations_delegated_administrator" "other_delegated" { + account_id = data.aws_caller_identity.delegated.account_id + service_principal = %[2]q +} + +data "aws_organizations_delegated_services" "test" { + account_id = aws_organizations_delegated_administrator.other_delegated.account_id +} +`, servicePrincipal, servicePrincipal2) +} diff --git a/aws/data_source_aws_organizations_organization_test.go b/aws/data_source_aws_organizations_organization_test.go index ee6258b56a9f..725fb60a733a 100644 --- a/aws/data_source_aws_organizations_organization_test.go +++ b/aws/data_source_aws_organizations_organization_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/organizations" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,7 +16,8 @@ func testAccDataSourceAwsOrganizationsOrganization_basic(t *testing.T) { testAccPreCheck(t) testAccOrganizationsAccountPreCheck(t) }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsOrganizationResourceOnlyConfig, diff --git a/aws/data_source_aws_organizations_organizational_units_test.go b/aws/data_source_aws_organizations_organizational_units_test.go index 9301852dcb8d..f1640d4003cf 100644 --- a/aws/data_source_aws_organizations_organizational_units_test.go +++ b/aws/data_source_aws_organizations_organizational_units_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/organizations" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,7 +15,8 @@ func testAccDataSourceAwsOrganizationsOrganizationalUnits_basic(t *testing.T) { testAccPreCheck(t) testAccOrganizationsAccountPreCheck(t) }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, organizations.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsOrganizationsOrganizationalUnitsConfig, diff --git a/aws/data_source_aws_outposts_outpost.go b/aws/data_source_aws_outposts_outpost.go index 54f22fa0e6ac..33e2a83aded7 100644 --- a/aws/data_source_aws_outposts_outpost.go +++ b/aws/data_source_aws_outposts_outpost.go @@ -43,6 +43,7 @@ func dataSourceAwsOutpostsOutpost() *schema.Resource { }, "owner_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, "site_id": { @@ -82,6 +83,10 @@ func dataSourceAwsOutpostsOutpostRead(d *schema.ResourceData, meta interface{}) continue } + if v, ok := d.GetOk("owner_id"); ok && v.(string) != aws.StringValue(outpost.OwnerId) { + continue + } + results = append(results, outpost) } diff --git a/aws/data_source_aws_outposts_outpost_instance_type_test.go b/aws/data_source_aws_outposts_outpost_instance_type_test.go index 5b01b6a96452..e2a0fec6a6a4 100644 --- a/aws/data_source_aws_outposts_outpost_instance_type_test.go +++ b/aws/data_source_aws_outposts_outpost_instance_type_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/outposts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccAWSOutpostsOutpostInstanceTypeDataSource_InstanceType(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -30,6 +32,7 @@ func TestAccAWSOutpostsOutpostInstanceTypeDataSource_PreferredInstanceTypes(t *t resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_outposts_outpost_instance_types_test.go b/aws/data_source_aws_outposts_outpost_instance_types_test.go index be5e3b1cd825..eaed60144e17 100644 --- a/aws/data_source_aws_outposts_outpost_instance_types_test.go +++ b/aws/data_source_aws_outposts_outpost_instance_types_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/outposts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -13,6 +14,7 @@ func TestAccAWSOutpostsOutpostInstanceTypesDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_outposts_outpost_test.go b/aws/data_source_aws_outposts_outpost_test.go index c4bc6899ac44..f860955ea3b1 100644 --- a/aws/data_source_aws_outposts_outpost_test.go +++ b/aws/data_source_aws_outposts_outpost_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/outposts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,19 +13,20 @@ func TestAccAWSOutpostsOutpostDataSource_Id(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccAWSOutpostsOutpostDataSourceConfigId(), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN(dataSourceName, "arn", "outposts", regexp.MustCompile(`outpost/op-.+$`)), + testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount(dataSourceName, "arn", "outposts", regexp.MustCompile(`outpost/op-.+$`).String()), resource.TestMatchResourceAttr(dataSourceName, "availability_zone", regexp.MustCompile(`^.+$`)), resource.TestMatchResourceAttr(dataSourceName, "availability_zone_id", regexp.MustCompile(`^.+$`)), resource.TestCheckResourceAttrSet(dataSourceName, "description"), resource.TestMatchResourceAttr(dataSourceName, "id", regexp.MustCompile(`^op-.+$`)), resource.TestMatchResourceAttr(dataSourceName, "name", regexp.MustCompile(`^.+$`)), - testAccCheckResourceAttrAccountID(dataSourceName, "owner_id"), + testAccMatchResourceAttrAccountID(dataSourceName, "owner_id"), ), }, }, @@ -37,6 +39,7 @@ func TestAccAWSOutpostsOutpostDataSource_Name(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -62,6 +65,7 @@ func TestAccAWSOutpostsOutpostDataSource_Arn(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -81,6 +85,32 @@ func TestAccAWSOutpostsOutpostDataSource_Arn(t *testing.T) { }) } +func TestAccAWSOutpostsOutpostDataSource_OwnerId(t *testing.T) { + sourceDataSourceName := "data.aws_outposts_outpost.source" + dataSourceName := "data.aws_outposts_outpost.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSOutpostsOutpostDataSourceConfigOwnerId(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", sourceDataSourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone", sourceDataSourceName, "availability_zone"), + resource.TestCheckResourceAttrPair(dataSourceName, "availability_zone_id", sourceDataSourceName, "availability_zone_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", sourceDataSourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "id", sourceDataSourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", sourceDataSourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner_id", sourceDataSourceName, "owner_id"), + ), + }, + }, + }) +} + func testAccAWSOutpostsOutpostDataSourceConfigId() string { return ` data "aws_outposts_outposts" "test" {} @@ -118,3 +148,17 @@ data "aws_outposts_outpost" "test" { } ` } + +func testAccAWSOutpostsOutpostDataSourceConfigOwnerId() string { + return ` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "source" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +data "aws_outposts_outpost" "test" { + owner_id = data.aws_outposts_outpost.source.owner_id +} +` +} diff --git a/aws/data_source_aws_outposts_outposts.go b/aws/data_source_aws_outposts_outposts.go index 43b296ee5a76..5df28557e723 100644 --- a/aws/data_source_aws_outposts_outposts.go +++ b/aws/data_source_aws_outposts_outposts.go @@ -38,6 +38,11 @@ func dataSourceAwsOutpostsOutposts() *schema.Resource { Optional: true, Computed: true, }, + "owner_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, }, } } @@ -71,6 +76,10 @@ func dataSourceAwsOutpostsOutpostsRead(d *schema.ResourceData, meta interface{}) continue } + if v, ok := d.GetOk("owner_id"); ok && v.(string) != aws.StringValue(outpost.OwnerId) { + continue + } + arns = append(arns, aws.StringValue(outpost.OutpostArn)) ids = append(ids, aws.StringValue(outpost.OutpostId)) } diff --git a/aws/data_source_aws_outposts_outposts_test.go b/aws/data_source_aws_outposts_outposts_test.go index 8c64f4dd36e4..cb2da8da7d2c 100644 --- a/aws/data_source_aws_outposts_outposts_test.go +++ b/aws/data_source_aws_outposts_outposts_test.go @@ -14,6 +14,7 @@ func TestAccAWSOutpostsOutpostsDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_outposts_site_test.go b/aws/data_source_aws_outposts_site_test.go index 491e015b1dab..ec8778c82f8c 100644 --- a/aws/data_source_aws_outposts_site_test.go +++ b/aws/data_source_aws_outposts_site_test.go @@ -4,6 +4,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/outposts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccAWSOutpostsSiteDataSource_Id(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsSites(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -34,6 +36,7 @@ func TestAccAWSOutpostsSiteDataSource_Name(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsSites(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_outposts_sites_test.go b/aws/data_source_aws_outposts_sites_test.go index e5a2dbce1da5..ec52762b696f 100644 --- a/aws/data_source_aws_outposts_sites_test.go +++ b/aws/data_source_aws_outposts_sites_test.go @@ -14,6 +14,7 @@ func TestAccAWSOutpostsSitesDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsSites(t) }, + ErrorCheck: testAccErrorCheck(t, outposts.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_partition_test.go b/aws/data_source_aws_partition_test.go index f23951978dd1..eaa1d576be51 100644 --- a/aws/data_source_aws_partition_test.go +++ b/aws/data_source_aws_partition_test.go @@ -10,8 +10,9 @@ import ( func TestAccAWSPartition_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsPartitionConfig_basic, diff --git a/aws/data_source_aws_prefix_list.go b/aws/data_source_aws_prefix_list.go index 59a667dbd6aa..586edfa23e90 100644 --- a/aws/data_source_aws_prefix_list.go +++ b/aws/data_source_aws_prefix_list.go @@ -65,12 +65,7 @@ func dataSourceAwsPrefixListRead(d *schema.ResourceData, meta interface{}) error d.SetId(aws.StringValue(pl.PrefixListId)) d.Set("name", pl.PrefixListName) - - cidrs := make([]string, len(pl.Cidrs)) - for i, v := range pl.Cidrs { - cidrs[i] = *v - } - d.Set("cidr_blocks", cidrs) + d.Set("cidr_blocks", aws.StringValueSlice(pl.Cidrs)) return nil } diff --git a/aws/data_source_aws_prefix_list_test.go b/aws/data_source_aws_prefix_list_test.go index 4c5304e7d9c1..a28adc29b796 100644 --- a/aws/data_source_aws_prefix_list_test.go +++ b/aws/data_source_aws_prefix_list_test.go @@ -14,8 +14,9 @@ import ( func TestAccDataSourceAwsPrefixList_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsPrefixListConfig, @@ -30,8 +31,9 @@ func TestAccDataSourceAwsPrefixList_basic(t *testing.T) { func TestAccDataSourceAwsPrefixList_filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsPrefixListConfigFilter, @@ -46,8 +48,9 @@ func TestAccDataSourceAwsPrefixList_filter(t *testing.T) { func TestAccDataSourceAwsPrefixList_nameDoesNotOverrideFilter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsPrefixListConfig_nameDoesNotOverrideFilter, diff --git a/aws/data_source_aws_pricing_product_test.go b/aws/data_source_aws_pricing_product_test.go index 46ea0cded56c..33c215fe3a9f 100644 --- a/aws/data_source_aws_pricing_product_test.go +++ b/aws/data_source_aws_pricing_product_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/pricing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -12,6 +13,7 @@ import ( func TestAccDataSourceAwsPricingProduct_ec2(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckPricing(t) }, + ErrorCheck: testAccErrorCheck(t, pricing.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { @@ -28,6 +30,7 @@ func TestAccDataSourceAwsPricingProduct_ec2(t *testing.T) { func TestAccDataSourceAwsPricingProduct_redshift(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckPricing(t) }, + ErrorCheck: testAccErrorCheck(t, pricing.EndpointsID), ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_qldb_ledger.go b/aws/data_source_aws_qldb_ledger.go index 1dbe9e834bef..d1385d3f39f5 100644 --- a/aws/data_source_aws_qldb_ledger.go +++ b/aws/data_source_aws_qldb_ledger.go @@ -29,6 +29,11 @@ func dataSourceAwsQLDBLedger() *schema.Resource { ), }, + "permissions_mode": { + Type: schema.TypeString, + Computed: true, + }, + "deletion_protection": { Type: schema.TypeBool, Computed: true, @@ -56,6 +61,7 @@ func dataSourceAwsQLDBLedgerRead(d *schema.ResourceData, meta interface{}) error d.SetId(aws.StringValue(resp.Name)) d.Set("arn", resp.Arn) d.Set("deletion_protection", resp.DeletionProtection) + d.Set("permissions_mode", resp.PermissionsMode) return nil } diff --git a/aws/data_source_aws_qldb_ledger_test.go b/aws/data_source_aws_qldb_ledger_test.go index e2b1b0a8fd0c..75de8ab622d5 100644 --- a/aws/data_source_aws_qldb_ledger_test.go +++ b/aws/data_source_aws_qldb_ledger_test.go @@ -13,8 +13,9 @@ func TestAccDataSourceAwsQLDBLedger_basic(t *testing.T) { rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(7)) // QLDB name cannot be longer than 32 characters resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(qldb.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(qldb.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, qldb.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsQLDBLedgerConfig(rName), @@ -22,6 +23,7 @@ func TestAccDataSourceAwsQLDBLedger_basic(t *testing.T) { resource.TestCheckResourceAttrPair("data.aws_qldb_ledger.by_name", "arn", "aws_qldb_ledger.tf_test", "arn"), resource.TestCheckResourceAttrPair("data.aws_qldb_ledger.by_name", "deletion_protection", "aws_qldb_ledger.tf_test", "deletion_protection"), resource.TestCheckResourceAttrPair("data.aws_qldb_ledger.by_name", "name", "aws_qldb_ledger.tf_test", "name"), + resource.TestCheckResourceAttrPair("data.aws_qldb_ledger.by_name", "permissions_mode", "aws_qldb_ledger.tf_test", "permissions_mode"), ), }, }, @@ -32,16 +34,19 @@ func testAccDataSourceAwsQLDBLedgerConfig(rName string) string { return fmt.Sprintf(` resource "aws_qldb_ledger" "tf_wrong1" { name = "%[1]s1" + permissions_mode = "STANDARD" deletion_protection = false } resource "aws_qldb_ledger" "tf_test" { name = "%[1]s2" + permissions_mode = "STANDARD" deletion_protection = false } resource "aws_qldb_ledger" "tf_wrong2" { name = "%[1]s3" + permissions_mode = "STANDARD" deletion_protection = false } diff --git a/aws/data_source_aws_ram_resource_share.go b/aws/data_source_aws_ram_resource_share.go index 383d8f65c202..e5b6926a62ba 100644 --- a/aws/data_source_aws_ram_resource_share.go +++ b/aws/data_source_aws_ram_resource_share.go @@ -103,9 +103,9 @@ func dataSourceAwsRamResourceShareRead(d *schema.ResourceData, meta interface{}) for _, r := range resp.ResourceShares { if aws.StringValue(r.Name) == name { d.SetId(aws.StringValue(r.ResourceShareArn)) - d.Set("arn", aws.StringValue(r.ResourceShareArn)) - d.Set("owning_account_id", aws.StringValue(r.OwningAccountId)) - d.Set("status", aws.StringValue(r.Status)) + d.Set("arn", r.ResourceShareArn) + d.Set("owning_account_id", r.OwningAccountId) + d.Set("status", r.Status) if err := d.Set("tags", keyvaluetags.RamKeyValueTags(r.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) diff --git a/aws/data_source_aws_ram_resource_share_test.go b/aws/data_source_aws_ram_resource_share_test.go index 9e304bd91e84..81b46bcecf2d 100644 --- a/aws/data_source_aws_ram_resource_share_test.go +++ b/aws/data_source_aws_ram_resource_share_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ram" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsRamResourceShare_basic(t *testing.T) { datasourceName := "data.aws_ram_resource_share.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ram.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRamResourceShareConfig_NonExistent, @@ -40,8 +42,9 @@ func TestAccDataSourceAwsRamResourceShare_Tags(t *testing.T) { datasourceName := "data.aws_ram_resource_share.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ram.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRamResourceShareConfig_Tags(rName), diff --git a/aws/data_source_aws_rds_certificate_test.go b/aws/data_source_aws_rds_certificate_test.go index 0d3a630217df..31aca530c240 100644 --- a/aws/data_source_aws_rds_certificate_test.go +++ b/aws/data_source_aws_rds_certificate_test.go @@ -13,6 +13,7 @@ func TestAccAWSRDSCertificateDataSource_Id(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSCertificatePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -31,6 +32,7 @@ func TestAccAWSRDSCertificateDataSource_LatestValidTill(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSCertificatePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_rds_cluster.go b/aws/data_source_aws_rds_cluster.go index 0616e3aff8a9..9f2a2b721084 100644 --- a/aws/data_source_aws_rds_cluster.go +++ b/aws/data_source_aws_rds_cluster.go @@ -203,7 +203,7 @@ func dataSourceAwsRdsClusterRead(d *schema.ResourceData, meta interface{}) error arn := dbc.DBClusterArn d.Set("arn", arn) - d.Set("backtrack_window", int(aws.Int64Value(dbc.BacktrackWindow))) + d.Set("backtrack_window", dbc.BacktrackWindow) d.Set("backup_retention_period", dbc.BackupRetentionPeriod) d.Set("cluster_identifier", dbc.DBClusterIdentifier) diff --git a/aws/data_source_aws_rds_cluster_test.go b/aws/data_source_aws_rds_cluster_test.go index fe4d956d078d..701ca1ad5472 100644 --- a/aws/data_source_aws_rds_cluster_test.go +++ b/aws/data_source_aws_rds_cluster_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAWSRDSCluster_basic(t *testing.T) { resourceName := "aws_rds_cluster.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRdsClusterConfigBasic(clusterName), diff --git a/aws/data_source_aws_rds_engine_version_test.go b/aws/data_source_aws_rds_engine_version_test.go index ae24f8358fba..cfe939a69702 100644 --- a/aws/data_source_aws_rds_engine_version_test.go +++ b/aws/data_source_aws_rds_engine_version_test.go @@ -18,6 +18,7 @@ func TestAccAWSRDSEngineVersionDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -52,6 +53,7 @@ func TestAccAWSRDSEngineVersionDataSource_upgradeTargets(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -70,6 +72,7 @@ func TestAccAWSRDSEngineVersionDataSource_preferred(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -88,6 +91,7 @@ func TestAccAWSRDSEngineVersionDataSource_defaultOnly(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRDSEngineVersionPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_rds_orderable_db_instance_test.go b/aws/data_source_aws_rds_orderable_db_instance_test.go index 0e7cacd68294..faa7622eb773 100644 --- a/aws/data_source_aws_rds_orderable_db_instance_test.go +++ b/aws/data_source_aws_rds_orderable_db_instance_test.go @@ -19,6 +19,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -42,6 +43,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_preferredClass(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -61,6 +63,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_preferredVersion(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -81,6 +84,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_preferredClassAndVersion(t *test resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -100,6 +104,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsEnhancedMonitoring(t *te resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -118,6 +123,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsIAMDatabaseAuthenticatio resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -136,6 +142,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsIops(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -154,6 +161,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsKerberosAuthentication(t resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -176,6 +184,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsPerformanceInsights(t *t testAccAWSRdsOrderableDbInstancePreCheck(t) testAccRDSPerformanceInsightsDefaultVersionPreCheck(t, "mysql") }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -194,6 +203,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsStorageAutoscaling(t *te resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -212,6 +222,7 @@ func TestAccAWSRdsOrderableDbInstanceDataSource_supportsStorageEncryption(t *tes resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRdsOrderableDbInstancePreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_redshift_cluster.go b/aws/data_source_aws_redshift_cluster.go index a6b3222d3a6e..630a44711c74 100644 --- a/aws/data_source_aws_redshift_cluster.go +++ b/aws/data_source_aws_redshift_cluster.go @@ -185,11 +185,11 @@ func dataSourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error describing Redshift Cluster: %s, error: %w", cluster, err) } - if resp.Clusters == nil || len(resp.Clusters) == 0 { + if resp.Clusters == nil || len(resp.Clusters) == 0 || resp.Clusters[0] == nil { return fmt.Errorf("Error describing Redshift Cluster: %s, cluster information not found", cluster) } - rsc := *resp.Clusters[0] + rsc := resp.Clusters[0] d.SetId(cluster) d.Set("allow_version_upgrade", rsc.AllowVersionUpgrade) diff --git a/aws/data_source_aws_redshift_cluster_test.go b/aws/data_source_aws_redshift_cluster_test.go index 5d91358d91ee..34fd5c2f130c 100644 --- a/aws/data_source_aws_redshift_cluster_test.go +++ b/aws/data_source_aws_redshift_cluster_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/redshift" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,8 +12,9 @@ import ( func TestAccAWSDataSourceRedshiftCluster_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceRedshiftClusterConfig(rInt), @@ -44,8 +46,9 @@ func TestAccAWSDataSourceRedshiftCluster_basic(t *testing.T) { func TestAccAWSDataSourceRedshiftCluster_vpc(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceRedshiftClusterConfigWithVpc(rInt), @@ -63,8 +66,9 @@ func TestAccAWSDataSourceRedshiftCluster_vpc(t *testing.T) { func TestAccAWSDataSourceRedshiftCluster_logging(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceRedshiftClusterConfigWithLogging(rInt), diff --git a/aws/data_source_aws_redshift_orderable_cluster_test.go b/aws/data_source_aws_redshift_orderable_cluster_test.go index 48ba04454c0f..6b0d606d7a58 100644 --- a/aws/data_source_aws_redshift_orderable_cluster_test.go +++ b/aws/data_source_aws_redshift_orderable_cluster_test.go @@ -14,6 +14,7 @@ func TestAccAWSRedshiftOrderableClusterDataSource_ClusterType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRedshiftOrderableClusterPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -32,6 +33,7 @@ func TestAccAWSRedshiftOrderableClusterDataSource_ClusterVersion(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRedshiftOrderableClusterPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -51,6 +53,7 @@ func TestAccAWSRedshiftOrderableClusterDataSource_NodeType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRedshiftOrderableClusterPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -70,6 +73,7 @@ func TestAccAWSRedshiftOrderableClusterDataSource_PreferredNodeTypes(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAWSRedshiftOrderableClusterPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_redshift_service_account.go b/aws/data_source_aws_redshift_service_account.go index bcd877cbeac1..efd2c731a18a 100644 --- a/aws/data_source_aws_redshift_service_account.go +++ b/aws/data_source_aws_redshift_service_account.go @@ -16,7 +16,7 @@ var redshiftServiceAccountPerRegionMap = map[string]string{ endpoints.ApEast1RegionID: "313564881002", endpoints.ApNortheast1RegionID: "404641285394", endpoints.ApNortheast2RegionID: "760740231472", - "ap-northeast-3": "090321488786", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "090321488786", endpoints.ApSouth1RegionID: "865932855811", endpoints.ApSoutheast1RegionID: "361669875840", endpoints.ApSoutheast2RegionID: "762762565011", diff --git a/aws/data_source_aws_redshift_service_account_test.go b/aws/data_source_aws_redshift_service_account_test.go index 3e6d2be4bba2..7218d1cb301c 100644 --- a/aws/data_source_aws_redshift_service_account_test.go +++ b/aws/data_source_aws_redshift_service_account_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/redshift" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSRedshiftServiceAccount_basic(t *testing.T) { dataSourceName := "data.aws_redshift_service_account.main" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsRedshiftServiceAccountConfig, @@ -32,8 +34,9 @@ func TestAccAWSRedshiftServiceAccount_Region(t *testing.T) { dataSourceName := "data.aws_redshift_service_account.regional" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, redshift.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsRedshiftServiceAccountExplicitRegionConfig, diff --git a/aws/data_source_aws_region_test.go b/aws/data_source_aws_region_test.go index 11a847dba41c..54a29f005153 100644 --- a/aws/data_source_aws_region_test.go +++ b/aws/data_source_aws_region_test.go @@ -77,8 +77,9 @@ func TestAccDataSourceAwsRegion_basic(t *testing.T) { dataSourceName := "data.aws_region.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionConfig_empty, @@ -96,8 +97,9 @@ func TestAccDataSourceAwsRegion_endpoint(t *testing.T) { dataSourceName := "data.aws_region.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionConfig_endpoint(), @@ -115,8 +117,9 @@ func TestAccDataSourceAwsRegion_endpointAndName(t *testing.T) { dataSourceName := "data.aws_region.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionConfig_endpointAndName(), @@ -134,8 +137,9 @@ func TestAccDataSourceAwsRegion_name(t *testing.T) { dataSourceName := "data.aws_region.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionConfig_name(), diff --git a/aws/data_source_aws_regions_test.go b/aws/data_source_aws_regions_test.go index b4a3c0803320..e68e812b06dc 100644 --- a/aws/data_source_aws_regions_test.go +++ b/aws/data_source_aws_regions_test.go @@ -5,6 +5,7 @@ import ( "strconv" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsRegions_basic(t *testing.T) { resourceName := "data.aws_regions.empty" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionsConfig_empty(), @@ -31,8 +33,9 @@ func TestAccDataSourceAwsRegions_Filter(t *testing.T) { resourceName := "data.aws_regions.opt_in_status" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionsConfig_allRegionsFiltered("opt-in-not-required"), @@ -48,8 +51,9 @@ func TestAccDataSourceAwsRegions_AllRegions(t *testing.T) { resourceAllRegions := "data.aws_regions.all_regions" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRegionsConfig_allRegions(), diff --git a/aws/data_source_aws_resourcegroupstaggingapi_resources.go b/aws/data_source_aws_resourcegroupstaggingapi_resources.go new file mode 100644 index 000000000000..b1c1eebe723c --- /dev/null +++ b/aws/data_source_aws_resourcegroupstaggingapi_resources.go @@ -0,0 +1,193 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsResourceGroupsTaggingAPIResources() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsResourceGroupsTaggingAPIResourcesRead, + + Schema: map[string]*schema.Schema{ + "exclude_compliant_resources": { + Type: schema.TypeBool, + Optional: true, + }, + "include_compliance_details": { + Type: schema.TypeBool, + Optional: true, + }, + "resource_arn_list": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"tag_filter"}, + }, + "resource_type_filters": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"resource_arn_list"}, + }, + "tag_filter": { + Type: schema.TypeList, + Optional: true, + MaxItems: 50, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 20, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "resource_tag_mapping_list": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_arn": { + Type: schema.TypeString, + Computed: true, + }, + "compliance_details": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "compliance_status": { + Type: schema.TypeBool, + Computed: true, + }, + "keys_with_noncompliant_values": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "non_compliant_keys": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tags": tagsSchemaComputed(), + }, + }, + }, + }, + } +} + +func dataSourceAwsResourceGroupsTaggingAPIResourcesRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).resourcegroupstaggingapiconn + + input := &resourcegroupstaggingapi.GetResourcesInput{} + + if v, ok := d.GetOk("include_compliance_details"); ok { + input.IncludeComplianceDetails = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("exclude_compliant_resources"); ok { + input.ExcludeCompliantResources = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("resource_arn_list"); ok && v.(*schema.Set).Len() > 0 { + input.ResourceARNList = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("tag_filter"); ok { + input.TagFilters = expandAwsResourceGroupsTaggingAPITagFilters(v.([]interface{})) + } + + if v, ok := d.GetOk("resource_type_filters"); ok && v.(*schema.Set).Len() > 0 { + input.ResourceTypeFilters = expandStringSet(v.(*schema.Set)) + } + + var taggings []*resourcegroupstaggingapi.ResourceTagMapping + + err := conn.GetResourcesPages(input, func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + taggings = append(taggings, page.ResourceTagMappingList...) + return !lastPage + }) + if err != nil { + return fmt.Errorf("error getting Resource Groups Tags API Resources: %w", err) + } + + d.SetId(meta.(*AWSClient).partition) + + if err := d.Set("resource_tag_mapping_list", flattenAwsResourceGroupsTaggingAPIResourcesTagMappingList(taggings)); err != nil { + return fmt.Errorf("error setting resource tag mapping list: %w", err) + } + + return nil +} + +func expandAwsResourceGroupsTaggingAPITagFilters(filters []interface{}) []*resourcegroupstaggingapi.TagFilter { + result := make([]*resourcegroupstaggingapi.TagFilter, len(filters)) + + for i, filter := range filters { + m := filter.(map[string]interface{}) + + result[i] = &resourcegroupstaggingapi.TagFilter{ + Key: aws.String(m["key"].(string)), + } + + if v, ok := m["values"]; ok && v.(*schema.Set).Len() > 0 { + result[i].Values = expandStringSet(v.(*schema.Set)) + } + } + + return result +} + +func flattenAwsResourceGroupsTaggingAPIResourcesTagMappingList(list []*resourcegroupstaggingapi.ResourceTagMapping) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + + for _, i := range list { + l := map[string]interface{}{ + "resource_arn": aws.StringValue(i.ResourceARN), + "tags": keyvaluetags.ResourcegroupstaggingapiKeyValueTags(i.Tags).Map(), + } + + if i.ComplianceDetails != nil { + l["compliance_details"] = flattenAwsResourceGroupsTaggingAPIComplianceDetails(i.ComplianceDetails) + } + + result = append(result, l) + } + + return result +} + +func flattenAwsResourceGroupsTaggingAPIComplianceDetails(details *resourcegroupstaggingapi.ComplianceDetails) []map[string]interface{} { + if details == nil { + return []map[string]interface{}{} + } + + m := map[string]interface{}{ + "compliance_status": aws.BoolValue(details.ComplianceStatus), + "keys_with_noncompliant_values": flattenStringSet(details.KeysWithNoncompliantValues), + "non_compliant_keys": flattenStringSet(details.NoncompliantKeys), + } + + return []map[string]interface{}{m} +} diff --git a/aws/data_source_aws_resourcegroupstaggingapi_resources_test.go b/aws/data_source_aws_resourcegroupstaggingapi_resources_test.go new file mode 100644 index 000000000000..368d07cc191e --- /dev/null +++ b/aws/data_source_aws_resourcegroupstaggingapi_resources_test.go @@ -0,0 +1,170 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsResourceGroupsTaggingAPIResources_TagFilter(t *testing.T) { + dataSourceName := "data.aws_resourcegroupstaggingapi_resources.test" + resourceName := "aws_vpc.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, resourcegroupstaggingapi.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigTagFilter(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckTypeSetElemNestedAttrs(dataSourceName, "resource_tag_mapping_list.*", map[string]string{ + "tags.Key": rName, + }), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "resource_tag_mapping_list.*.resource_arn", resourceName, "arn"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsResourceGroupsTaggingAPIResources_IncludeComplianceDetails(t *testing.T) { + dataSourceName := "data.aws_resourcegroupstaggingapi_resources.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, resourcegroupstaggingapi.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigIncludeComplianceDetails(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "resource_tag_mapping_list.0.compliance_details.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "resource_tag_mapping_list.0.compliance_details.0.compliance_status", "true"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsResourceGroupsTaggingAPIResources_ResourceTypeFilters(t *testing.T) { + dataSourceName := "data.aws_resourcegroupstaggingapi_resources.test" + resourceName := "aws_vpc.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, resourcegroupstaggingapi.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigResourceTypeFilters(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckTypeSetElemNestedAttrs(dataSourceName, "resource_tag_mapping_list.*", map[string]string{ + "tags.Key": rName, + }), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "resource_tag_mapping_list.*.resource_arn", resourceName, "arn"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsResourceGroupsTaggingAPIResources_ResourceArnList(t *testing.T) { + dataSourceName := "data.aws_resourcegroupstaggingapi_resources.test" + resourceName := "aws_vpc.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, resourcegroupstaggingapi.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigResourceARNList(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckTypeSetElemNestedAttrs(dataSourceName, "resource_tag_mapping_list.*", map[string]string{ + "tags.Key": rName, + }), + resource.TestCheckTypeSetElemAttrPair(dataSourceName, "resource_tag_mapping_list.*.resource_arn", resourceName, "arn"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigTagFilter(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Key = %[1]q + } +} + +data "aws_resourcegroupstaggingapi_resources" "test" { + tag_filter { + key = "Key" + values = [aws_vpc.test.tags["Key"]] + } +} +`, rName) +} + +func testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigResourceTypeFilters(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Key = %[1]q + } +} + +data "aws_resourcegroupstaggingapi_resources" "test" { + resource_type_filters = ["ec2:vpc"] + + depends_on = [aws_vpc.test] +} +`, rName) +} + +func testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigResourceARNList(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Key = %[1]q + } +} + +data "aws_resourcegroupstaggingapi_resources" "test" { + resource_arn_list = [aws_vpc.test.arn] +} +`, rName) +} + +func testAccDataSourceAwsResourceGroupsTaggingAPIResourcesConfigIncludeComplianceDetails(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Key = %[1]q + } +} + +data "aws_resourcegroupstaggingapi_resources" "test" { + include_compliance_details = true + exclude_compliant_resources = false + resource_arn_list = [aws_vpc.test.arn] +} +`, rName) +} diff --git a/aws/data_source_aws_route.go b/aws/data_source_aws_route.go index 3d34bc11cd94..c05f601dc6e0 100644 --- a/aws/data_source_aws_route.go +++ b/aws/data_source_aws_route.go @@ -2,10 +2,13 @@ package aws import ( "fmt" - "log" + "strings" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" ) func dataSourceAwsRoute() *schema.Resource { @@ -17,6 +20,10 @@ func dataSourceAwsRoute() *schema.Resource { Type: schema.TypeString, Required: true, }, + + /// + // Destinations. + /// "destination_cidr_block": { Type: schema.TypeString, Optional: true, @@ -27,6 +34,21 @@ func dataSourceAwsRoute() *schema.Resource { Optional: true, Computed: true, }, + + "destination_prefix_list_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + // + // Targets. + // + "carrier_gateway_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "egress_only_gateway_id": { Type: schema.TypeString, Optional: true, @@ -42,27 +64,27 @@ func dataSourceAwsRoute() *schema.Resource { Optional: true, Computed: true, }, - "nat_gateway_id": { + "local_gateway_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "local_gateway_id": { + "nat_gateway_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "transit_gateway_id": { + "network_interface_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "vpc_peering_connection_id": { + "transit_gateway_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - "network_interface_id": { + "vpc_peering_connection_id": { Type: schema.TypeString, Optional: true, Computed: true, @@ -73,132 +95,109 @@ func dataSourceAwsRoute() *schema.Resource { func dataSourceAwsRouteRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - req := &ec2.DescribeRouteTablesInput{} - rtbId := d.Get("route_table_id") - cidr := d.Get("destination_cidr_block") - ipv6Cidr := d.Get("destination_ipv6_cidr_block") - - req.Filters = buildEC2AttributeFilterList( - map[string]string{ - "route-table-id": rtbId.(string), - "route.destination-cidr-block": cidr.(string), - "route.destination-ipv6-cidr-block": ipv6Cidr.(string), - }, - ) - log.Printf("[DEBUG] Reading Route Table: %s", req) - resp, err := conn.DescribeRouteTables(req) - if err != nil { - return err - } - if resp == nil || len(resp.RouteTables) == 0 { - return fmt.Errorf("Your query returned no results. Please change your search criteria and try again.") - } - if len(resp.RouteTables) > 1 { - return fmt.Errorf("Your query returned more than one route table. Please change your search criteria and try again.") - } + routeTableID := d.Get("route_table_id").(string) - results := getRoutes(resp.RouteTables[0], d) + routeTable, err := finder.RouteTableByID(conn, routeTableID) - if len(results) == 0 { - return fmt.Errorf("No routes matching supplied arguments found in table(s)") - } - if len(results) > 1 { - return fmt.Errorf("Multiple routes matched; use additional constraints to reduce matches to a single route") + if err != nil { + return fmt.Errorf("error reading Route Table (%s): %w", routeTableID, err) } - route := results[0] - d.SetId(resourceAwsRouteID(d, route)) // using function from "resource_aws_route.go" - d.Set("destination_cidr_block", route.DestinationCidrBlock) - d.Set("destination_ipv6_cidr_block", route.DestinationIpv6CidrBlock) - d.Set("egress_only_gateway_id", route.EgressOnlyInternetGatewayId) - d.Set("gateway_id", route.GatewayId) - d.Set("instance_id", route.InstanceId) - d.Set("nat_gateway_id", route.NatGatewayId) - d.Set("local_gateway_id", route.LocalGatewayId) - d.Set("transit_gateway_id", route.TransitGatewayId) - d.Set("vpc_peering_connection_id", route.VpcPeeringConnectionId) - d.Set("network_interface_id", route.NetworkInterfaceId) + routes := []*ec2.Route{} - return nil -} - -func getRoutes(table *ec2.RouteTable, d *schema.ResourceData) []*ec2.Route { - ec2Routes := table.Routes - routes := make([]*ec2.Route, 0, len(ec2Routes)) - // Loop through the routes and add them to the set - for _, r := range ec2Routes { - - if r.Origin != nil && *r.Origin == "EnableVgwRoutePropagation" { + for _, r := range routeTable.Routes { + if aws.StringValue(r.Origin) == ec2.RouteOriginEnableVgwRoutePropagation { continue } - if r.DestinationPrefixListId != nil { + if r.DestinationPrefixListId != nil && strings.HasPrefix(aws.StringValue(r.GatewayId), "vpce-") { // Skipping because VPC endpoint routes are handled separately // See aws_vpc_endpoint continue } - if v, ok := d.GetOk("destination_cidr_block"); ok { - if r.DestinationCidrBlock == nil || *r.DestinationCidrBlock != v.(string) { - continue - } + if v, ok := d.GetOk("destination_cidr_block"); ok && aws.StringValue(r.DestinationCidrBlock) != v.(string) { + continue + } + + if v, ok := d.GetOk("destination_ipv6_cidr_block"); ok && aws.StringValue(r.DestinationIpv6CidrBlock) != v.(string) { + continue } - if v, ok := d.GetOk("destination_ipv6_cidr_block"); ok { - if r.DestinationIpv6CidrBlock == nil || *r.DestinationIpv6CidrBlock != v.(string) { - continue - } + if v, ok := d.GetOk("destination_prefix_list_id"); ok && aws.StringValue(r.DestinationPrefixListId) != v.(string) { + continue + } + + if v, ok := d.GetOk("carrier_gateway_id"); ok && aws.StringValue(r.CarrierGatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("egress_only_gateway_id"); ok { - if r.EgressOnlyInternetGatewayId == nil || *r.EgressOnlyInternetGatewayId != v.(string) { - continue - } + if v, ok := d.GetOk("egress_only_gateway_id"); ok && aws.StringValue(r.EgressOnlyInternetGatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("gateway_id"); ok { - if r.GatewayId == nil || *r.GatewayId != v.(string) { - continue - } + if v, ok := d.GetOk("gateway_id"); ok && aws.StringValue(r.GatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("instance_id"); ok { - if r.InstanceId == nil || *r.InstanceId != v.(string) { - continue - } + if v, ok := d.GetOk("instance_id"); ok && aws.StringValue(r.InstanceId) != v.(string) { + continue } - if v, ok := d.GetOk("nat_gateway_id"); ok { - if r.NatGatewayId == nil || *r.NatGatewayId != v.(string) { - continue - } + if v, ok := d.GetOk("local_gateway_id"); ok && aws.StringValue(r.LocalGatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("local_gateway_id"); ok { - if r.LocalGatewayId == nil || *r.LocalGatewayId != v.(string) { - continue - } + if v, ok := d.GetOk("nat_gateway_id"); ok && aws.StringValue(r.NatGatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("transit_gateway_id"); ok { - if r.TransitGatewayId == nil || *r.TransitGatewayId != v.(string) { - continue - } + if v, ok := d.GetOk("network_interface_id"); ok && aws.StringValue(r.NetworkInterfaceId) != v.(string) { + continue } - if v, ok := d.GetOk("vpc_peering_connection_id"); ok { - if r.VpcPeeringConnectionId == nil || *r.VpcPeeringConnectionId != v.(string) { - continue - } + if v, ok := d.GetOk("transit_gateway_id"); ok && aws.StringValue(r.TransitGatewayId) != v.(string) { + continue } - if v, ok := d.GetOk("network_interface_id"); ok { - if r.NetworkInterfaceId == nil || *r.NetworkInterfaceId != v.(string) { - continue - } + if v, ok := d.GetOk("vpc_peering_connection_id"); ok && aws.StringValue(r.VpcPeeringConnectionId) != v.(string) { + continue } + routes = append(routes, r) } - return routes + + if len(routes) == 0 { + return fmt.Errorf("No routes matching supplied arguments found in Route Table (%s)", routeTableID) + } + + if len(routes) > 1 { + return fmt.Errorf("%d routes matched in Route Table (%s); use additional constraints to reduce matches to a single route", len(routes), routeTableID) + } + + route := routes[0] + + if destination := aws.StringValue(route.DestinationCidrBlock); destination != "" { + d.SetId(tfec2.RouteCreateID(routeTableID, destination)) + } else if destination := aws.StringValue(route.DestinationIpv6CidrBlock); destination != "" { + d.SetId(tfec2.RouteCreateID(routeTableID, destination)) + } else if destination := aws.StringValue(route.DestinationPrefixListId); destination != "" { + d.SetId(tfec2.RouteCreateID(routeTableID, destination)) + } + + d.Set("carrier_gateway_id", route.CarrierGatewayId) + d.Set("destination_cidr_block", route.DestinationCidrBlock) + d.Set("destination_ipv6_cidr_block", route.DestinationIpv6CidrBlock) + d.Set("destination_prefix_list_id", route.DestinationPrefixListId) + d.Set("egress_only_gateway_id", route.EgressOnlyInternetGatewayId) + d.Set("gateway_id", route.GatewayId) + d.Set("instance_id", route.InstanceId) + d.Set("local_gateway_id", route.LocalGatewayId) + d.Set("nat_gateway_id", route.NatGatewayId) + d.Set("network_interface_id", route.NetworkInterfaceId) + d.Set("transit_gateway_id", route.TransitGatewayId) + d.Set("vpc_peering_connection_id", route.VpcPeeringConnectionId) + + return nil } diff --git a/aws/data_source_aws_route53_delegation_set.go b/aws/data_source_aws_route53_delegation_set.go index 723a17c7b2d9..5a0774812a2a 100644 --- a/aws/data_source_aws_route53_delegation_set.go +++ b/aws/data_source_aws_route53_delegation_set.go @@ -5,6 +5,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -14,6 +15,10 @@ func dataSourceAwsDelegationSet() *schema.Resource { Read: dataSourceAwsDelegationSetRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "id": { Type: schema.TypeString, Required: true, @@ -50,9 +55,16 @@ func dataSourceAwsDelegationSetRead(d *schema.ResourceData, meta interface{}) er d.SetId(dSetID) d.Set("caller_reference", resp.DelegationSet.CallerReference) - if err := d.Set("name_servers", expandNameServers(resp.DelegationSet.NameServers)); err != nil { + if err := d.Set("name_servers", aws.StringValueSlice(resp.DelegationSet.NameServers)); err != nil { return fmt.Errorf("error setting name_servers: %w", err) } + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "route53", + Resource: fmt.Sprintf("delegationset/%s", d.Id()), + }.String() + d.Set("arn", arn) + return nil } diff --git a/aws/data_source_aws_route53_delegation_set_test.go b/aws/data_source_aws_route53_delegation_set_test.go index 337118822d75..b397149d7344 100644 --- a/aws/data_source_aws_route53_delegation_set_test.go +++ b/aws/data_source_aws_route53_delegation_set_test.go @@ -1,9 +1,11 @@ package aws import ( + "fmt" "regexp" "testing" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -11,40 +13,38 @@ func TestAccAWSRoute53DelegationSetDataSource_basic(t *testing.T) { dataSourceName := "data.aws_route53_delegation_set.dset" resourceName := "aws_route53_delegation_set.dset" + zoneName := testAccRandomDomainName() + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAWSDataSourceAWSRoute53DelegationSetConfig_basic, + Config: testAccAWSDataSourceAWSRoute53DelegationSetConfig_basic(zoneName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair( - dataSourceName, "name_servers.#", - resourceName, "name_servers.#", - ), - resource.TestMatchResourceAttr( - "data.aws_route53_delegation_set.dset", - "caller_reference", - regexp.MustCompile("DynDNS(.*)"), - ), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name_servers.#", resourceName, "name_servers.#"), + resource.TestMatchResourceAttr("data.aws_route53_delegation_set.dset", "caller_reference", regexp.MustCompile("DynDNS(.*)")), ), }, }, }) } -const testAccAWSDataSourceAWSRoute53DelegationSetConfig_basic = ` +func testAccAWSDataSourceAWSRoute53DelegationSetConfig_basic(zoneName string) string { + return fmt.Sprintf(` resource "aws_route53_delegation_set" "dset" { reference_name = "DynDNS" } resource "aws_route53_zone" "primary" { - name = "example.xyz" + name = %[1]q delegation_set_id = aws_route53_delegation_set.dset.id } data "aws_route53_delegation_set" "dset" { id = aws_route53_delegation_set.dset.id } -` +`, zoneName) +} diff --git a/aws/data_source_aws_route53_resolver_endpoint.go b/aws/data_source_aws_route53_resolver_endpoint.go index 0e2acf9be6d5..bd8470eb7ace 100644 --- a/aws/data_source_aws_route53_resolver_endpoint.go +++ b/aws/data_source_aws_route53_resolver_endpoint.go @@ -117,11 +117,11 @@ func dataSourceAwsRoute53ResolverEndpointRead(d *schema.ResourceData, meta inter d.SetId(aws.StringValue(resolver.Id)) d.Set("resolver_endpoint_id", resolver.Id) - d.Set("arn", aws.StringValue(resolver.Arn)) - d.Set("status", aws.StringValue(resolver.Status)) - d.Set("name", aws.StringValue(resolver.Name)) - d.Set("vpc_id", aws.StringValue(resolver.HostVPCId)) - d.Set("direction", aws.StringValue(resolver.Direction)) + d.Set("arn", resolver.Arn) + d.Set("status", resolver.Status) + d.Set("name", resolver.Name) + d.Set("vpc_id", resolver.HostVPCId) + d.Set("direction", resolver.Direction) if resp.NextToken == nil { break diff --git a/aws/data_source_aws_route53_resolver_endpoint_test.go b/aws/data_source_aws_route53_resolver_endpoint_test.go index 8c7cccbb0166..bbcb9ddc621e 100644 --- a/aws/data_source_aws_route53_resolver_endpoint_test.go +++ b/aws/data_source_aws_route53_resolver_endpoint_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -18,7 +19,7 @@ func TestAccAWSRoute53ResolverEndpointDataSource_Basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { @@ -47,7 +48,7 @@ func TestAccAWSRoute53ResolverEndpointDataSource_Filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_route53_resolver_rule.go b/aws/data_source_aws_route53_resolver_rule.go index da80eed78d03..4a094939d554 100644 --- a/aws/data_source_aws_route53_resolver_rule.go +++ b/aws/data_source_aws_route53_resolver_rule.go @@ -104,24 +104,26 @@ func dataSourceAwsRoute53ResolverRuleRead(d *schema.ResourceData, meta interface }), } + rules := []*route53resolver.ResolverRule{} log.Printf("[DEBUG] Listing Route53 Resolver rules: %s", req) - resp, err := conn.ListResolverRules(req) + err := conn.ListResolverRulesPages(req, func(page *route53resolver.ListResolverRulesOutput, lastPage bool) bool { + rules = append(rules, page.ResolverRules...) + return !lastPage + }) if err != nil { return fmt.Errorf("error getting Route53 Resolver rules: %w", err) } - - if n := len(resp.ResolverRules); n == 0 { + if n := len(rules); n == 0 { return fmt.Errorf("no Route53 Resolver rules matched") } else if n > 1 { return fmt.Errorf("%d Route53 Resolver rules matched; use additional constraints to reduce matches to a rule", n) } - rule = resp.ResolverRules[0] + rule = rules[0] } d.SetId(aws.StringValue(rule.Id)) - arn := *rule.Arn - d.Set("arn", arn) + d.Set("arn", rule.Arn) // To be consistent with other AWS services that do not accept a trailing period, // we remove the suffix from the Domain Name returned from the API d.Set("domain_name", trimTrailingPeriod(aws.StringValue(rule.DomainName))) @@ -134,6 +136,7 @@ func dataSourceAwsRoute53ResolverRuleRead(d *schema.ResourceData, meta interface d.Set("share_status", shareStatus) // https://github.com/hashicorp/terraform-provider-aws/issues/10211 if shareStatus != route53resolver.ShareStatusSharedWithMe { + arn := aws.StringValue(rule.Arn) tags, err := keyvaluetags.Route53resolverListTags(conn, arn) if err != nil { diff --git a/aws/data_source_aws_route53_resolver_rule_test.go b/aws/data_source_aws_route53_resolver_rule_test.go index adc1324a0452..5774911ad258 100644 --- a/aws/data_source_aws_route53_resolver_rule_test.go +++ b/aws/data_source_aws_route53_resolver_rule_test.go @@ -4,11 +4,16 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/route53resolver" "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/helper/schema" ) +func init() { + RegisterServiceErrorCheckFunc(route53resolver.EndpointsID, testAccErrorCheckSkipRoute53) +} + func TestAccAWSRoute53ResolverRuleDataSource_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_route53_resolver_rule.example" @@ -18,7 +23,7 @@ func TestAccAWSRoute53ResolverRuleDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { @@ -69,7 +74,7 @@ func TestAccAWSRoute53ResolverRuleDataSource_ResolverEndpointIdWithTags(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { @@ -106,7 +111,7 @@ func TestAccAWSRoute53ResolverRuleDataSource_SharedByMe(t *testing.T) { testAccAlternateAccountPreCheck(t) testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), Steps: []resource.TestStep{ { @@ -144,7 +149,7 @@ func TestAccAWSRoute53ResolverRuleDataSource_SharedWithMe(t *testing.T) { testAccAlternateAccountPreCheck(t) testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_route53_resolver_rules.go b/aws/data_source_aws_route53_resolver_rules.go index 9d5f7ca222a9..cfbdfa9168d7 100644 --- a/aws/data_source_aws_route53_resolver_rules.go +++ b/aws/data_source_aws_route53_resolver_rules.go @@ -67,7 +67,7 @@ func dataSourceAwsRoute53ResolverRulesRead(d *schema.ResourceData, meta interfac resolverRuleIds := []*string{} log.Printf("[DEBUG] Listing Route53 Resolver rules: %s", req) - err := conn.ListResolverRulesPages(req, func(page *route53resolver.ListResolverRulesOutput, isLast bool) bool { + err := conn.ListResolverRulesPages(req, func(page *route53resolver.ListResolverRulesOutput, lastPage bool) bool { for _, rule := range page.ResolverRules { if v, ok := d.GetOk("owner_id"); ok && aws.StringValue(rule.OwnerId) != v.(string) { continue @@ -84,7 +84,7 @@ func dataSourceAwsRoute53ResolverRulesRead(d *schema.ResourceData, meta interfac resolverRuleIds = append(resolverRuleIds, rule.Id) } - return !isLast + return !lastPage }) if err != nil { return fmt.Errorf("error getting Route53 Resolver rules: %w", err) diff --git a/aws/data_source_aws_route53_resolver_rules_test.go b/aws/data_source_aws_route53_resolver_rules_test.go index e22287e880b9..213a82d046dc 100644 --- a/aws/data_source_aws_route53_resolver_rules_test.go +++ b/aws/data_source_aws_route53_resolver_rules_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,7 +14,7 @@ func TestAccAWSRoute53ResolverRulesDataSource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { @@ -36,7 +37,7 @@ func TestAccAWSRoute53ResolverRulesDataSource_ResolverEndpointId(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSRoute53Resolver(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53resolver.EndpointsID), Providers: testAccProviders, Steps: []resource.TestStep{ { diff --git a/aws/data_source_aws_route53_zone.go b/aws/data_source_aws_route53_zone.go index 025ee88125d5..c183ea86ae5f 100644 --- a/aws/data_source_aws_route53_zone.go +++ b/aws/data_source_aws_route53_zone.go @@ -5,6 +5,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" @@ -15,6 +16,10 @@ func dataSourceAwsRoute53Zone() *schema.Resource { Read: dataSourceAwsRoute53ZoneRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, "zone_id": { Type: schema.TypeString, Optional: true, @@ -190,6 +195,13 @@ func dataSourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) erro return fmt.Errorf("error setting tags: %w", err) } + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "route53", + Resource: fmt.Sprintf("hostedzone/%s", d.Id()), + }.String() + d.Set("arn", arn) + return nil } diff --git a/aws/data_source_aws_route53_zone_test.go b/aws/data_source_aws_route53_zone_test.go index 5e504e8418ac..454f34192cf9 100644 --- a/aws/data_source_aws_route53_zone_test.go +++ b/aws/data_source_aws_route53_zone_test.go @@ -4,25 +4,27 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/service/servicediscovery" + "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccAWSRoute53ZoneDataSource_id(t *testing.T) { - rInt := acctest.RandInt() resourceName := "aws_route53_zone.test" dataSourceName := "data.aws_route53_zone.test" + fqdn := testAccRandomFQDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckRoute53ZoneDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsRoute53ZoneConfigId(rInt), + Config: testAccDataSourceAwsRoute53ZoneConfigId(fqdn), Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "id"), resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), resource.TestCheckResourceAttrPair(resourceName, "name_servers.#", dataSourceName, "name_servers.#"), @@ -34,18 +36,19 @@ func TestAccAWSRoute53ZoneDataSource_id(t *testing.T) { } func TestAccAWSRoute53ZoneDataSource_name(t *testing.T) { - rInt := acctest.RandInt() resourceName := "aws_route53_zone.test" dataSourceName := "data.aws_route53_zone.test" + fqdn := testAccRandomFQDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckRoute53ZoneDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsRoute53ZoneConfigName(rInt), + Config: testAccDataSourceAwsRoute53ZoneConfigName(fqdn), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "id"), resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), @@ -62,14 +65,16 @@ func TestAccAWSRoute53ZoneDataSource_tags(t *testing.T) { resourceName := "aws_route53_zone.test" dataSourceName := "data.aws_route53_zone.test" + fqdn := testAccRandomFQDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckRoute53ZoneDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsRoute53ZoneConfigTagsPrivate(rInt), + Config: testAccDataSourceAwsRoute53ZoneConfigTagsPrivate(fqdn, rInt), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(resourceName, "id", dataSourceName, "id"), resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), @@ -88,7 +93,7 @@ func TestAccAWSRoute53ZoneDataSource_vpc(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckRoute53ZoneDestroy, Steps: []resource.TestStep{ @@ -111,8 +116,8 @@ func TestAccAWSRoute53ZoneDataSource_serviceDiscovery(t *testing.T) { dataSourceName := "data.aws_route53_zone.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicediscovery.EndpointsID, t) }, - ErrorCheck: testAccErrorCheckSkipRoute53(t), + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck("servicediscovery", t) }, + ErrorCheck: testAccErrorCheck(t, route53.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckRoute53ZoneDestroy, Steps: []resource.TestStep{ @@ -128,50 +133,46 @@ func TestAccAWSRoute53ZoneDataSource_serviceDiscovery(t *testing.T) { }) } -func testAccDataSourceAwsRoute53ZoneConfigId(rInt int) string { +func testAccDataSourceAwsRoute53ZoneConfigId(fqdn string) string { return fmt.Sprintf(` resource "aws_route53_zone" "test" { - name = "terraformtestacchz-%[1]d.com." + name = %[1]q } data "aws_route53_zone" "test" { zone_id = aws_route53_zone.test.zone_id } -`, rInt) +`, fqdn) } -func testAccDataSourceAwsRoute53ZoneConfigName(rInt int) string { +func testAccDataSourceAwsRoute53ZoneConfigName(fqdn string) string { return fmt.Sprintf(` resource "aws_route53_zone" "test" { - name = "terraformtestacchz-%[1]d.com." + name = %[1]q } data "aws_route53_zone" "test" { name = aws_route53_zone.test.name } -`, rInt) +`, fqdn) } -func testAccDataSourceAwsRoute53ZoneConfigTagsPrivate(rInt int) string { +func testAccDataSourceAwsRoute53ZoneConfigTagsPrivate(fqdn string, rInt int) string { return fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" - - tags = { - Name = "terraform-testacc-r53-zone-data-source-%[1]d" - } } resource "aws_route53_zone" "test" { - name = "terraformtestacchz-%[1]d.com." + name = %[1]q vpc { vpc_id = aws_vpc.test.id } tags = { - Environment = "tf-acc-test-%[1]d" - Name = "tf-acc-test-%[1]d" + Environment = "tf-acc-test-%[2]d" + Name = "tf-acc-test-%[2]d" } } @@ -181,10 +182,10 @@ data "aws_route53_zone" "test" { vpc_id = aws_vpc.test.id tags = { - Environment = "tf-acc-test-%[1]d" + Environment = "tf-acc-test-%[2]d" } } -`, rInt) +`, fqdn, rInt) } func testAccDataSourceAwsRoute53ZoneConfigVpc(rInt int) string { diff --git a/aws/data_source_aws_route_table.go b/aws/data_source_aws_route_table.go index 024751391409..27fdfb9dd09d 100644 --- a/aws/data_source_aws_route_table.go +++ b/aws/data_source_aws_route_table.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" @@ -43,6 +44,9 @@ func dataSourceAwsRouteTable() *schema.Resource { Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + /// + // Destinations. + /// "cidr_block": { Type: schema.TypeString, Computed: true, @@ -53,6 +57,19 @@ func dataSourceAwsRouteTable() *schema.Resource { Computed: true, }, + "destination_prefix_list_id": { + Type: schema.TypeString, + Computed: true, + }, + + /// + // Targets. + /// + "carrier_gateway_id": { + Type: schema.TypeString, + Computed: true, + }, + "egress_only_gateway_id": { Type: schema.TypeString, Computed: true, @@ -68,38 +85,39 @@ func dataSourceAwsRouteTable() *schema.Resource { Computed: true, }, - "nat_gateway_id": { + "local_gateway_id": { Type: schema.TypeString, Computed: true, }, - "local_gateway_id": { + "nat_gateway_id": { Type: schema.TypeString, Computed: true, }, - "transit_gateway_id": { + "network_interface_id": { Type: schema.TypeString, Computed: true, }, - "vpc_endpoint_id": { + "transit_gateway_id": { Type: schema.TypeString, Computed: true, }, - "vpc_peering_connection_id": { + "vpc_endpoint_id": { Type: schema.TypeString, Computed: true, }, - "network_interface_id": { + "vpc_peering_connection_id": { Type: schema.TypeString, Computed: true, }, }, }, }, + "associations": { Type: schema.TypeList, Computed: true, @@ -132,6 +150,12 @@ func dataSourceAwsRouteTable() *schema.Resource { }, }, }, + + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "owner_id": { Type: schema.TypeString, Computed: true, @@ -153,7 +177,7 @@ func dataSourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error filter, filterOk := d.GetOk("filter") if !rtbOk && !vpcIdOk && !subnetIdOk && !gatewayIdOk && !filterOk && !tagsOk { - return fmt.Errorf("One of route_table_id, vpc_id, subnet_id, gateway_id, filters, or tags must be assigned") + return fmt.Errorf("one of route_table_id, vpc_id, subnet_id, gateway_id, filters, or tags must be assigned") } req.Filters = buildEC2AttributeFilterList( map[string]string{ @@ -176,15 +200,27 @@ func dataSourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error return err } if resp == nil || len(resp.RouteTables) == 0 { - return fmt.Errorf("Your query returned no results. Please change your search criteria and try again") + return fmt.Errorf("query returned no results. Please change your search criteria and try again") } if len(resp.RouteTables) > 1 { - return fmt.Errorf("Multiple Route Table matched; use additional constraints to reduce matches to a single Route Table") + return fmt.Errorf("multiple Route Tables matched; use additional constraints to reduce matches to a single Route Table") } rt := resp.RouteTables[0] d.SetId(aws.StringValue(rt.RouteTableId)) + + ownerID := aws.StringValue(rt.OwnerId) + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: ec2.ServiceName, + Region: meta.(*AWSClient).region, + AccountID: ownerID, + Resource: fmt.Sprintf("route-table/%s", d.Id()), + }.String() + d.Set("arn", arn) + d.Set("owner_id", ownerID) + d.Set("route_table_id", rt.RouteTableId) d.Set("vpc_id", rt.VpcId) @@ -192,7 +228,6 @@ func dataSourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting tags: %w", err) } - d.Set("owner_id", rt.OwnerId) if err := d.Set("routes", dataSourceRoutesRead(rt.Routes)); err != nil { return err } @@ -216,7 +251,7 @@ func dataSourceRoutesRead(ec2Routes []*ec2.Route) []map[string]interface{} { continue } - if r.DestinationPrefixListId != nil { + if r.DestinationPrefixListId != nil && strings.HasPrefix(aws.StringValue(r.GatewayId), "vpce-") { // Skipping because VPC endpoint routes are handled separately // See aws_vpc_endpoint continue @@ -230,6 +265,12 @@ func dataSourceRoutesRead(ec2Routes []*ec2.Route) []map[string]interface{} { if r.DestinationIpv6CidrBlock != nil { m["ipv6_cidr_block"] = *r.DestinationIpv6CidrBlock } + if r.DestinationPrefixListId != nil { + m["destination_prefix_list_id"] = *r.DestinationPrefixListId + } + if r.CarrierGatewayId != nil { + m["carrier_gateway_id"] = *r.CarrierGatewayId + } if r.EgressOnlyInternetGatewayId != nil { m["egress_only_gateway_id"] = *r.EgressOnlyInternetGatewayId } diff --git a/aws/data_source_aws_route_table_test.go b/aws/data_source_aws_route_table_test.go index 7c9094037973..fb52049ba024 100644 --- a/aws/data_source_aws_route_table_test.go +++ b/aws/data_source_aws_route_table_test.go @@ -2,8 +2,10 @@ package aws import ( "fmt" + "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -21,13 +23,15 @@ func TestAccDataSourceAwsRouteTable_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRouteTableConfigBasic(rName), Check: resource.ComposeTestCheckFunc( // By tags. + testAccMatchResourceAttrRegionalARN(datasource1Name, "arn", "ec2", regexp.MustCompile(`route-table/.+$`)), resource.TestCheckResourceAttrPair(datasource1Name, "id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource1Name, "route_table_id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource1Name, "owner_id", rtResourceName, "owner_id"), @@ -39,6 +43,7 @@ func TestAccDataSourceAwsRouteTable_basic(t *testing.T) { testAccCheckListHasSomeElementAttrPair(datasource1Name, "associations", "gateway_id", igwResourceName, "id"), resource.TestCheckResourceAttr(datasource1Name, "tags.Name", rName), // By filter. + testAccMatchResourceAttrRegionalARN(datasource2Name, "arn", "ec2", regexp.MustCompile(`route-table/.+$`)), resource.TestCheckResourceAttrPair(datasource2Name, "id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource2Name, "route_table_id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource2Name, "owner_id", rtResourceName, "owner_id"), @@ -50,6 +55,7 @@ func TestAccDataSourceAwsRouteTable_basic(t *testing.T) { testAccCheckListHasSomeElementAttrPair(datasource2Name, "associations", "gateway_id", igwResourceName, "id"), resource.TestCheckResourceAttr(datasource2Name, "tags.Name", rName), // By subnet ID. + testAccMatchResourceAttrRegionalARN(datasource3Name, "arn", "ec2", regexp.MustCompile(`route-table/.+$`)), resource.TestCheckResourceAttrPair(datasource3Name, "id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource3Name, "route_table_id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource3Name, "owner_id", rtResourceName, "owner_id"), @@ -61,6 +67,7 @@ func TestAccDataSourceAwsRouteTable_basic(t *testing.T) { testAccCheckListHasSomeElementAttrPair(datasource3Name, "associations", "gateway_id", igwResourceName, "id"), resource.TestCheckResourceAttr(datasource3Name, "tags.Name", rName), // By route table ID. + testAccMatchResourceAttrRegionalARN(datasource4Name, "arn", "ec2", regexp.MustCompile(`route-table/.+$`)), resource.TestCheckResourceAttrPair(datasource4Name, "id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource4Name, "route_table_id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource4Name, "owner_id", rtResourceName, "owner_id"), @@ -72,6 +79,7 @@ func TestAccDataSourceAwsRouteTable_basic(t *testing.T) { testAccCheckListHasSomeElementAttrPair(datasource4Name, "associations", "gateway_id", igwResourceName, "id"), resource.TestCheckResourceAttr(datasource4Name, "tags.Name", rName), // By gateway ID. + testAccMatchResourceAttrRegionalARN(datasource5Name, "arn", "ec2", regexp.MustCompile(`route-table/.+$`)), resource.TestCheckResourceAttrPair(datasource5Name, "id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource5Name, "route_table_id", rtResourceName, "id"), resource.TestCheckResourceAttrPair(datasource5Name, "owner_id", rtResourceName, "owner_id"), @@ -93,8 +101,9 @@ func TestAccDataSourceAwsRouteTable_main(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRouteTableConfigMain(rName), diff --git a/aws/data_source_aws_route_tables_test.go b/aws/data_source_aws_route_tables_test.go index 710f47f8d906..abbcd124f439 100644 --- a/aws/data_source_aws_route_tables_test.go +++ b/aws/data_source_aws_route_tables_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccDataSourceAwsRouteTables_basic(t *testing.T) { rInt := acctest.RandIntRange(0, 256) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_route_test.go b/aws/data_source_aws_route_test.go index f1a97b41cb31..05d71a62de93 100644 --- a/aws/data_source_aws_route_test.go +++ b/aws/data_source_aws_route_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "regexp" "testing" "github.com/aws/aws-sdk-go/service/ec2" @@ -21,8 +22,9 @@ func TestAccAWSRouteDataSource_basic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsRouteConfigBasic(rName), @@ -47,20 +49,19 @@ func TestAccAWSRouteDataSource_basic(t *testing.T) { } func TestAccAWSRouteDataSource_TransitGatewayID(t *testing.T) { - var route ec2.Route dataSourceName := "data.aws_route.test" resourceName := "aws_route.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSRouteDestroy, Steps: []resource.TestStep{ { Config: testAccAWSRouteDataSourceConfigIpv4TransitGateway(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSRouteExists(resourceName, &route), resource.TestCheckResourceAttrPair(resourceName, "destination_cidr_block", dataSourceName, "destination_cidr_block"), resource.TestCheckResourceAttrPair(resourceName, "route_table_id", dataSourceName, "route_table_id"), resource.TestCheckResourceAttrPair(resourceName, "transit_gateway_id", dataSourceName, "transit_gateway_id"), @@ -77,6 +78,7 @@ func TestAccAWSRouteDataSource_IPv6DestinationCidr(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSRouteDestroy, Steps: []resource.TestStep{ @@ -92,20 +94,19 @@ func TestAccAWSRouteDataSource_IPv6DestinationCidr(t *testing.T) { } func TestAccAWSRouteDataSource_LocalGatewayID(t *testing.T) { - var route ec2.Route dataSourceName := "data.aws_route.by_local_gateway_id" resourceName := "aws_route.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSRouteDestroy, Steps: []resource.TestStep{ { Config: testAccAWSRouteDataSourceConfigIpv4LocalGateway(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSRouteExists(resourceName, &route), resource.TestCheckResourceAttrPair(resourceName, "destination_cidr_block", dataSourceName, "destination_cidr_block"), resource.TestCheckResourceAttrPair(resourceName, "route_table_id", dataSourceName, "route_table_id"), resource.TestCheckResourceAttrPair(resourceName, "local_gateway_id", dataSourceName, "local_gateway_id"), @@ -115,6 +116,81 @@ func TestAccAWSRouteDataSource_LocalGatewayID(t *testing.T) { }) } +func TestAccAWSRouteDataSource_CarrierGatewayID(t *testing.T) { + dataSourceName := "data.aws_route.test" + resourceName := "aws_route.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWavelengthZoneAvailable(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRouteDataSourceConfigIpv4CarrierGateway(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "destination_cidr_block", dataSourceName, "destination_cidr_block"), + resource.TestCheckResourceAttrPair(resourceName, "route_table_id", dataSourceName, "route_table_id"), + resource.TestCheckResourceAttrPair(resourceName, "carrier_gateway_id", dataSourceName, "carrier_gateway_id"), + ), + }, + }, + }) +} + +func TestAccAWSRouteDataSource_DestinationPrefixListId(t *testing.T) { + dataSourceName := "data.aws_route.test" + resourceName := "aws_route.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckEc2ManagedPrefixList(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRouteDataSourceConfigPrefixListNatGateway(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "destination_prefix_list_id", dataSourceName, "destination_prefix_list_id"), + resource.TestCheckResourceAttrPair(resourceName, "nat_gateway_id", dataSourceName, "nat_gateway_id"), + resource.TestCheckResourceAttrPair(resourceName, "route_table_id", dataSourceName, "route_table_id"), + ), + }, + }, + }) +} + +func TestAccAWSRouteDataSource_GatewayVpcEndpoint(t *testing.T) { + var routeTable ec2.RouteTable + var vpce ec2.VpcEndpoint + rtResourceName := "aws_route_table.test" + vpceResourceName := "aws_vpc_endpoint.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRouteDataSourceConfigGatewayVpcEndpointNoDataSource(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRouteTableExists(rtResourceName, &routeTable), + testAccCheckVpcEndpointExists(vpceResourceName, &vpce), + testAccCheckAWSRouteTableWaitForVpcEndpointRoute(&routeTable, &vpce), + ), + }, + { + Config: testAccAWSRouteDataSourceConfigGatewayVpcEndpointWithDataSource(rName), + ExpectError: regexp.MustCompile(`No routes matching supplied arguments found in Route Table`), + }, + }, + }) +} + func testAccDataSourceAwsRouteConfigBasic(rName string) string { return composeConfig( testAccLatestAmazonLinuxHvmEbsAmiConfig(), @@ -352,3 +428,159 @@ data "aws_route" "by_local_gateway_id" { } `, rName) } + +func testAccAWSRouteDataSourceConfigIpv4CarrierGateway(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_ec2_carrier_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_route_table" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_route" "test" { + destination_cidr_block = "0.0.0.0/0" + route_table_id = aws_route_table.test.id + carrier_gateway_id = aws_ec2_carrier_gateway.test.id +} + +data "aws_route" "test" { + route_table_id = aws_route.test.route_table_id + carrier_gateway_id = aws_route.test.carrier_gateway_id +} +`, rName) +} + +func testAccAWSRouteDataSourceConfigPrefixListNatGateway(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + cidr_block = "10.1.1.0/24" + vpc_id = aws_vpc.test.id + + map_public_ip_on_launch = true + + tags = { + Name = %[1]q + } +} + +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_eip" "test" { + vpc = true + + tags = { + Name = %[1]q + } +} + +resource "aws_nat_gateway" "test" { + allocation_id = aws_eip.test.id + subnet_id = aws_subnet.test.id + + tags = { + Name = %[1]q + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_ec2_managed_prefix_list" "test" { + address_family = "IPv4" + max_entries = 1 + name = %[1]q +} + +resource "aws_route_table" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_route" "test" { + route_table_id = aws_route_table.test.id + destination_prefix_list_id = aws_ec2_managed_prefix_list.test.id + nat_gateway_id = aws_nat_gateway.test.id +} + +data "aws_route" "test" { + route_table_id = aws_route.test.route_table_id + destination_prefix_list_id = aws_route.test.destination_prefix_list_id + nat_gateway_id = aws_route.test.nat_gateway_id +} +`, rName) +} + +func testAccAWSRouteDataSourceConfigGatewayVpcEndpointNoDataSource(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} + +data "aws_region" "current" {} + +resource "aws_vpc_endpoint" "test" { + vpc_id = aws_vpc.test.id + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + route_table_ids = [aws_route_table.test.id] +} + +resource "aws_route_table" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName) +} + +func testAccAWSRouteDataSourceConfigGatewayVpcEndpointWithDataSource(rName string) string { + return composeConfig(testAccAWSRouteDataSourceConfigGatewayVpcEndpointNoDataSource(rName), ` +data "aws_prefix_list" "test" { + name = aws_vpc_endpoint.test.service_name +} + +data "aws_route" "test" { + route_table_id = aws_route_table.test.id + destination_prefix_list_id = data.aws_prefix_list.test.id +} + `) +} diff --git a/aws/data_source_aws_s3_bucket_object.go b/aws/data_source_aws_s3_bucket_object.go index 637429362450..3c48e5ac54f3 100644 --- a/aws/data_source_aws_s3_bucket_object.go +++ b/aws/data_source_aws_s3_bucket_object.go @@ -27,6 +27,10 @@ func dataSourceAwsS3BucketObject() *schema.Resource { Type: schema.TypeString, Required: true, }, + "bucket_key_enabled": { + Type: schema.TypeBool, + Computed: true, + }, "cache_control": { Type: schema.TypeString, Computed: true, @@ -157,6 +161,7 @@ func dataSourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) e d.SetId(uniqueId) + d.Set("bucket_key_enabled", out.BucketKeyEnabled) d.Set("cache_control", out.CacheControl) d.Set("content_disposition", out.ContentDisposition) d.Set("content_encoding", out.ContentEncoding) @@ -175,7 +180,7 @@ func dataSourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) e d.Set("metadata", pointersMapToStringList(out.Metadata)) d.Set("object_lock_legal_hold_status", out.ObjectLockLegalHoldStatus) d.Set("object_lock_mode", out.ObjectLockMode) - d.Set("object_lock_retain_until_date", flattenS3ObjectLockRetainUntilDate(out.ObjectLockRetainUntilDate)) + d.Set("object_lock_retain_until_date", flattenS3ObjectDate(out.ObjectLockRetainUntilDate)) d.Set("server_side_encryption", out.ServerSideEncryption) d.Set("sse_kms_key_id", out.SSEKMSKeyId) d.Set("version_id", out.VersionId) @@ -216,7 +221,7 @@ func dataSourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) e if out.ContentType == nil { contentType = "" } else { - contentType = *out.ContentType + contentType = aws.StringValue(out.ContentType) } log.Printf("[INFO] Ignoring body of S3 object %s with Content-Type %q", uniqueId, contentType) diff --git a/aws/data_source_aws_s3_bucket_object_test.go b/aws/data_source_aws_s3_bucket_object_test.go index dc555b570c9b..db66fe256af9 100644 --- a/aws/data_source_aws_s3_bucket_object_test.go +++ b/aws/data_source_aws_s3_bucket_object_test.go @@ -26,6 +26,7 @@ func TestAccDataSourceAWSS3BucketObject_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -57,8 +58,9 @@ func TestAccDataSourceAWSS3BucketObject_basicViaAccessPoint(t *testing.T) { accessPointResourceName := "aws_s3_access_point.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceS3ObjectConfig_basicViaAccessPoint(rName), @@ -85,6 +87,7 @@ func TestAccDataSourceAWSS3BucketObject_readableBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -118,6 +121,7 @@ func TestAccDataSourceAWSS3BucketObject_kmsEncrypted(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -142,6 +146,43 @@ func TestAccDataSourceAWSS3BucketObject_kmsEncrypted(t *testing.T) { }) } +func TestAccDataSourceAWSS3BucketObject_bucketKeyEnabled(t *testing.T) { + rInt := acctest.RandInt() + + var rObj s3.GetObjectOutput + var dsObj s3.GetObjectOutput + + resourceName := "aws_s3_bucket_object.object" + dataSourceName := "data.aws_s3_bucket_object.obj" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, + PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testAccAWSDataSourceS3ObjectConfig_bucketKeyEnabled(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists(resourceName, &rObj), + testAccCheckAwsS3ObjectDataSourceExists(dataSourceName, &dsObj), + resource.TestCheckResourceAttr(dataSourceName, "content_length", "22"), + resource.TestCheckResourceAttrPair(dataSourceName, "content_type", resourceName, "content_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "server_side_encryption", resourceName, "server_side_encryption"), + resource.TestCheckResourceAttrPair(dataSourceName, "sse_kms_key_id", resourceName, "kms_key_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "bucket_key_enabled", resourceName, "bucket_key_enabled"), + resource.TestMatchResourceAttr(dataSourceName, "last_modified", regexp.MustCompile(rfc1123RegexPattern)), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_legal_hold_status", resourceName, "object_lock_legal_hold_status"), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_mode", resourceName, "object_lock_mode"), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_retain_until_date", resourceName, "object_lock_retain_until_date"), + resource.TestCheckResourceAttr(dataSourceName, "body", "Keep Calm and Carry On"), + ), + }, + }, + }) +} + func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) { rInt := acctest.RandInt() @@ -153,6 +194,7 @@ func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -167,6 +209,7 @@ func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) { resource.TestMatchResourceAttr(dataSourceName, "last_modified", regexp.MustCompile(rfc1123RegexPattern)), resource.TestCheckResourceAttrPair(dataSourceName, "version_id", resourceName, "version_id"), resource.TestCheckNoResourceAttr(dataSourceName, "body"), + resource.TestCheckResourceAttrPair(dataSourceName, "bucket_key_enabled", resourceName, "bucket_key_enabled"), resource.TestCheckResourceAttrPair(dataSourceName, "cache_control", resourceName, "cache_control"), resource.TestCheckResourceAttrPair(dataSourceName, "content_disposition", resourceName, "content_disposition"), resource.TestCheckResourceAttrPair(dataSourceName, "content_encoding", resourceName, "content_encoding"), @@ -202,6 +245,7 @@ func TestAccDataSourceAWSS3BucketObject_ObjectLockLegalHoldOff(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -236,6 +280,7 @@ func TestAccDataSourceAWSS3BucketObject_ObjectLockLegalHoldOn(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -272,6 +317,7 @@ func TestAccDataSourceAWSS3BucketObject_LeadingSlash(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -325,6 +371,7 @@ func TestAccDataSourceAWSS3BucketObject_MultipleSlashes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -366,6 +413,7 @@ func TestAccDataSourceAWSS3BucketObject_SingleSlashAsKey(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -496,6 +544,33 @@ data "aws_s3_bucket_object" "obj" { `, randInt) } +func testAccAWSDataSourceS3ObjectConfig_bucketKeyEnabled(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%[1]d" +} + +resource "aws_kms_key" "example" { + description = "TF Acceptance Test KMS key" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket_object" "object" { + bucket = aws_s3_bucket.object_bucket.bucket + key = "tf-testing-obj-%[1]d-encrypted" + content = "Keep Calm and Carry On" + content_type = "text/plain" + kms_key_id = aws_kms_key.example.arn + bucket_key_enabled = true +} + +data "aws_s3_bucket_object" "obj" { + bucket = aws_s3_bucket.object_bucket.bucket + key = aws_s3_bucket_object.object.key +} +`, randInt) +} + func testAccAWSDataSourceS3ObjectConfig_allParams(randInt int) string { return fmt.Sprintf(` resource "aws_s3_bucket" "object_bucket" { diff --git a/aws/data_source_aws_s3_bucket_objects_test.go b/aws/data_source_aws_s3_bucket_objects_test.go index f97f3adfd034..385a7b089a0a 100644 --- a/aws/data_source_aws_s3_bucket_objects_test.go +++ b/aws/data_source_aws_s3_bucket_objects_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/s3" "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" @@ -14,6 +15,7 @@ func TestAccDataSourceAWSS3BucketObjects_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -39,6 +41,7 @@ func TestAccDataSourceAWSS3BucketObjects_basicViaAccessPoint(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -64,6 +67,7 @@ func TestAccDataSourceAWSS3BucketObjects_all(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -94,6 +98,7 @@ func TestAccDataSourceAWSS3BucketObjects_prefixes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -123,6 +128,7 @@ func TestAccDataSourceAWSS3BucketObjects_encoded(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -148,6 +154,7 @@ func TestAccDataSourceAWSS3BucketObjects_maxKeys(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -173,6 +180,7 @@ func TestAccDataSourceAWSS3BucketObjects_startAfter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ @@ -197,6 +205,7 @@ func TestAccDataSourceAWSS3BucketObjects_fetchOwner(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), Providers: testAccProviders, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_s3_bucket_test.go b/aws/data_source_aws_s3_bucket_test.go index bcf5d4b1da0a..42cea10b5778 100644 --- a/aws/data_source_aws_s3_bucket_test.go +++ b/aws/data_source_aws_s3_bucket_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceS3Bucket_basic(t *testing.T) { hostedZoneID, _ := HostedZoneIDForRegion(region) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceS3BucketConfig_basic(bucketName), @@ -38,8 +40,9 @@ func TestAccDataSourceS3Bucket_website(t *testing.T) { region := testAccGetRegion() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAWSDataSourceS3BucketWebsiteConfig(bucketName), diff --git a/aws/data_source_aws_sagemaker_prebuilt_ecr_image_test.go b/aws/data_source_aws_sagemaker_prebuilt_ecr_image_test.go index b50bde85dd75..8c8a8c5cc63b 100644 --- a/aws/data_source_aws_sagemaker_prebuilt_ecr_image_test.go +++ b/aws/data_source_aws_sagemaker_prebuilt_ecr_image_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/sagemaker" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSSageMakerPrebuiltECRImage_basic(t *testing.T) { dataSourceName := "data.aws_sagemaker_prebuilt_ecr_image.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSageMakerPrebuiltECRImageConfig, @@ -33,8 +35,9 @@ func TestAccAWSSageMakerPrebuiltECRImage_region(t *testing.T) { dataSourceName := "data.aws_sagemaker_prebuilt_ecr_image.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sagemaker.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSageMakerPrebuiltECRImageExplicitRegionConfig, diff --git a/aws/data_source_aws_secretsmanager_secret_rotation_test.go b/aws/data_source_aws_secretsmanager_secret_rotation_test.go index d1b35731d88e..aff33bd113c0 100644 --- a/aws/data_source_aws_secretsmanager_secret_rotation_test.go +++ b/aws/data_source_aws_secretsmanager_secret_rotation_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsSecretsManagerSecretRotation_basic(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret_rotation.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretRotationConfig_NonExistent, diff --git a/aws/data_source_aws_secretsmanager_secret_test.go b/aws/data_source_aws_secretsmanager_secret_test.go index cf48bcce885a..6038dbb9fff2 100644 --- a/aws/data_source_aws_secretsmanager_secret_test.go +++ b/aws/data_source_aws_secretsmanager_secret_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/secretsmanager" "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" @@ -12,8 +13,9 @@ import ( func TestAccDataSourceAwsSecretsManagerSecret_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretConfig_MissingRequired, @@ -37,8 +39,9 @@ func TestAccDataSourceAwsSecretsManagerSecret_ARN(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretConfig_ARN(rName), @@ -56,8 +59,9 @@ func TestAccDataSourceAwsSecretsManagerSecret_Name(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretConfig_Name(rName), @@ -75,8 +79,9 @@ func TestAccDataSourceAwsSecretsManagerSecret_Policy(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretConfig_Policy(rName), diff --git a/aws/data_source_aws_secretsmanager_secret_version.go b/aws/data_source_aws_secretsmanager_secret_version.go index 1fbfc7620270..53979624a169 100644 --- a/aws/data_source_aws_secretsmanager_secret_version.go +++ b/aws/data_source_aws_secretsmanager_secret_version.go @@ -86,7 +86,7 @@ func dataSourceAwsSecretsManagerSecretVersionRead(d *schema.ResourceData, meta i d.Set("secret_id", secretID) d.Set("secret_string", output.SecretString) d.Set("version_id", output.VersionId) - d.Set("secret_binary", fmt.Sprintf("%s", output.SecretBinary)) + d.Set("secret_binary", string(output.SecretBinary)) d.Set("arn", output.ARN) if err := d.Set("version_stages", flattenStringList(output.VersionStages)); err != nil { diff --git a/aws/data_source_aws_secretsmanager_secret_version_test.go b/aws/data_source_aws_secretsmanager_secret_version_test.go index 83862ed41050..6b9ecb921fa4 100644 --- a/aws/data_source_aws_secretsmanager_secret_version_test.go +++ b/aws/data_source_aws_secretsmanager_secret_version_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/secretsmanager" "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" @@ -16,8 +17,9 @@ func TestAccDataSourceAwsSecretsManagerSecretVersion_basic(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret_version.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretVersionConfig_NonExistent, @@ -39,8 +41,9 @@ func TestAccDataSourceAwsSecretsManagerSecretVersion_VersionID(t *testing.T) { datasourceName := "data.aws_secretsmanager_secret_version.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretVersionConfig_VersionID(rName), @@ -58,8 +61,9 @@ func TestAccDataSourceAwsSecretsManagerSecretVersion_VersionStage(t *testing.T) datasourceName := "data.aws_secretsmanager_secret_version.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSecretsManager(t) }, + ErrorCheck: testAccErrorCheck(t, secretsmanager.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecretsManagerSecretVersionConfig_VersionStage_Custom(rName), diff --git a/aws/data_source_aws_security_group.go b/aws/data_source_aws_security_group.go index f3555b7e1a46..1bcb54a89804 100644 --- a/aws/data_source_aws_security_group.go +++ b/aws/data_source_aws_security_group.go @@ -1,14 +1,16 @@ package aws import ( + "errors" "fmt" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func dataSourceAwsSecurityGroup() *schema.Resource { @@ -76,19 +78,16 @@ func dataSourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) er req.Filters = nil } - log.Printf("[DEBUG] Reading Security Group: %s", req) - resp, err := conn.DescribeSecurityGroups(req) - if err != nil { - return err - } - if resp == nil || len(resp.SecurityGroups) == 0 { + sg, err := finder.SecurityGroup(conn, req) + if errors.Is(err, tfresource.ErrEmptyResult) { return fmt.Errorf("no matching SecurityGroup found") } - if len(resp.SecurityGroups) > 1 { + if errors.Is(err, tfresource.ErrTooManyResults) { return fmt.Errorf("multiple Security Groups matched; use additional constraints to reduce matches to a single Security Group") } - - sg := resp.SecurityGroups[0] + if err != nil { + return err + } d.SetId(aws.StringValue(sg.GroupId)) d.Set("name", sg.GroupName) @@ -101,7 +100,7 @@ func dataSourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) er arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, AccountID: *sg.OwnerId, Resource: fmt.Sprintf("security-group/%s", *sg.GroupId), diff --git a/aws/data_source_aws_security_group_test.go b/aws/data_source_aws_security_group_test.go index fe89ea606240..b805b7f6ce23 100644 --- a/aws/data_source_aws_security_group_test.go +++ b/aws/data_source_aws_security_group_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "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" @@ -13,8 +14,9 @@ import ( func TestAccDataSourceAwsSecurityGroup_basic(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecurityGroupConfig(rInt), diff --git a/aws/data_source_aws_security_groups.go b/aws/data_source_aws_security_groups.go index 46fc8434d2e0..af160de90cac 100644 --- a/aws/data_source_aws_security_groups.go +++ b/aws/data_source_aws_security_groups.go @@ -5,6 +5,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" @@ -28,6 +29,11 @@ func dataSourceAwsSecurityGroups() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "arns": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } } @@ -55,7 +61,7 @@ func dataSourceAwsSecurityGroupsRead(d *schema.ResourceData, meta interface{}) e log.Printf("[DEBUG] Reading Security Groups with request: %s", req) - var ids, vpc_ids []string + var ids, vpcIds, arns []string for { resp, err := conn.DescribeSecurityGroups(req) if err != nil { @@ -64,7 +70,17 @@ func dataSourceAwsSecurityGroupsRead(d *schema.ResourceData, meta interface{}) e for _, sg := range resp.SecurityGroups { ids = append(ids, aws.StringValue(sg.GroupId)) - vpc_ids = append(vpc_ids, aws.StringValue(sg.VpcId)) + vpcIds = append(vpcIds, aws.StringValue(sg.VpcId)) + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: ec2.ServiceName, + Region: meta.(*AWSClient).region, + AccountID: aws.StringValue(sg.OwnerId), + Resource: fmt.Sprintf("security-group/%s", aws.StringValue(sg.GroupId)), + }.String() + + arns = append(arns, arn) } if resp.NextToken == nil { @@ -86,6 +102,13 @@ func dataSourceAwsSecurityGroupsRead(d *schema.ResourceData, meta interface{}) e return err } - err = d.Set("vpc_ids", vpc_ids) - return err + if err = d.Set("vpc_ids", vpcIds); err != nil { + return fmt.Errorf("error setting vpc_ids: %s", err) + } + + if err = d.Set("arns", arns); err != nil { + return fmt.Errorf("error setting arns: %s", err) + } + + return nil } diff --git a/aws/data_source_aws_security_groups_test.go b/aws/data_source_aws_security_groups_test.go index 7275be856f3c..4e004a4befd1 100644 --- a/aws/data_source_aws_security_groups_test.go +++ b/aws/data_source_aws_security_groups_test.go @@ -4,21 +4,25 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsSecurityGroups_tag(t *testing.T) { rInt := acctest.RandInt() + dataSourceName := "data.aws_security_groups.by_tag" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecurityGroupsConfig_tag(rInt), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_security_groups.by_tag", "ids.#", "3"), - resource.TestCheckResourceAttr("data.aws_security_groups.by_tag", "vpc_ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "vpc_ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "arns.#", "3"), ), }, }, @@ -27,15 +31,18 @@ func TestAccDataSourceAwsSecurityGroups_tag(t *testing.T) { func TestAccDataSourceAwsSecurityGroups_filter(t *testing.T) { rInt := acctest.RandInt() + dataSourceName := "data.aws_security_groups.by_filter" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSecurityGroupsConfig_filter(rInt), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_security_groups.by_filter", "ids.#", "3"), - resource.TestCheckResourceAttr("data.aws_security_groups.by_filter", "vpc_ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "vpc_ids.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "arns.#", "3"), ), }, }, diff --git a/aws/data_source_aws_serverlessapplicationrepository_application_test.go b/aws/data_source_aws_serverlessapplicationrepository_application_test.go index a22b68bdb52e..c99cd4dc2408 100644 --- a/aws/data_source_aws_serverlessapplicationrepository_application_test.go +++ b/aws/data_source_aws_serverlessapplicationrepository_application_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/serverlessapplicationrepository" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Basic(t *tes appARN := testAccAwsServerlessApplicationRepositoryCloudFormationApplicationID() resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, serverlessapplicationrepository.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig(appARN), @@ -35,6 +37,7 @@ func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Basic(t *tes }, }) } + func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Versioned(t *testing.T) { datasourceName := "data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator" appARN := testAccAwsServerlessApplicationRepositoryCloudFormationApplicationID() @@ -45,8 +48,9 @@ func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Versioned(t ) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, serverlessapplicationrepository.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned(appARN, version1), diff --git a/aws/data_source_aws_service_discovery_dns_namespace.go b/aws/data_source_aws_service_discovery_dns_namespace.go new file mode 100644 index 000000000000..24e2cfd46c1e --- /dev/null +++ b/aws/data_source_aws_service_discovery_dns_namespace.go @@ -0,0 +1,123 @@ +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicediscovery" + "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" +) + +func dataSourceServiceDiscoveryDnsNamespace() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceServiceDiscoveryDnsNamespaceRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "hosted_zone": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + servicediscovery.NamespaceTypeDnsPublic, + servicediscovery.NamespaceTypeDnsPrivate, + }, false), + }, + }, + } +} + +func dataSourceServiceDiscoveryDnsNamespaceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).sdconn + + name := d.Get("name").(string) + + input := &servicediscovery.ListNamespacesInput{} + + var filters []*servicediscovery.NamespaceFilter + + filter := &servicediscovery.NamespaceFilter{ + Condition: aws.String(servicediscovery.FilterConditionEq), + Name: aws.String(servicediscovery.NamespaceFilterNameType), + Values: []*string{aws.String(d.Get("type").(string))}, + } + + filters = append(filters, filter) + + input.Filters = filters + + namespaceIds := make([]string, 0) + + err := conn.ListNamespacesPagesWithContext(ctx, input, func(page *servicediscovery.ListNamespacesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, namespace := range page.Namespaces { + if namespace == nil { + continue + } + + if name == aws.StringValue(namespace.Name) { + namespaceIds = append(namespaceIds, aws.StringValue(namespace.Id)) + } + } + return !lastPage + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing Service Discovery DNS Namespaces: %w", err)) + } + + if len(namespaceIds) == 0 { + return diag.Errorf("no matching Service Discovery DNS Namespace found") + } + + if len(namespaceIds) != 1 { + return diag.FromErr(fmt.Errorf("search returned %d Service Discovery DNS Namespaces, please revise so only one is returned", len(namespaceIds))) + } + + d.SetId(namespaceIds[0]) + + req := &servicediscovery.GetNamespaceInput{ + Id: aws.String(d.Id()), + } + + output, err := conn.GetNamespaceWithContext(ctx, req) + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Service Discovery DNS Namespace (%s): %w", d.Id(), err)) + } + + if output == nil || output.Namespace == nil { + return diag.FromErr(fmt.Errorf("error reading Service Discovery DNS Namespace (%s): empty output", d.Id())) + } + + namespace := output.Namespace + + d.Set("name", namespace.Name) + d.Set("description", namespace.Description) + d.Set("arn", namespace.Arn) + if namespace.Properties != nil && namespace.Properties.DnsProperties != nil { + d.Set("hosted_zone", namespace.Properties.DnsProperties.HostedZoneId) + } + + return nil +} diff --git a/aws/data_source_aws_service_discovery_dns_namespace_test.go b/aws/data_source_aws_service_discovery_dns_namespace_test.go new file mode 100644 index 000000000000..9b8befdd38f8 --- /dev/null +++ b/aws/data_source_aws_service_discovery_dns_namespace_test.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/servicediscovery" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceDiscoveryDnsNamespaceDataSource_private(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_service_discovery_dns_namespace.test" + resourceName := "aws_service_discovery_private_dns_namespace.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPartitionHasServicePreCheck(servicediscovery.EndpointsID, t) + testAccPreCheckAWSServiceDiscovery(t) + }, + ErrorCheck: testAccErrorCheck(t, servicediscovery.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsServiceDiscoveryPrivateDnsNamespaceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "hosted_zone", resourceName, "hosted_zone"), + ), + }, + }, + }) +} + +func TestAccAWSServiceDiscoveryDnsNamespaceDataSource_public(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_service_discovery_dns_namespace.test" + resourceName := "aws_service_discovery_public_dns_namespace.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPartitionHasServicePreCheck(servicediscovery.EndpointsID, t) + testAccPreCheckAWSServiceDiscovery(t) + }, + ErrorCheck: testAccErrorCheck(t, servicediscovery.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsServiceDiscoveryPublicDnsNamespaceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "hosted_zone", resourceName, "hosted_zone"), + ), + }, + }, + }) +} + +func testAccCheckAwsServiceDiscoveryPrivateDnsNamespaceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_service_discovery_private_dns_namespace" "test" { + name = "%[1]s.tf" + vpc = aws_vpc.test.id +} + +data "aws_service_discovery_dns_namespace" "test" { + name = aws_service_discovery_private_dns_namespace.test.name + type = "DNS_PRIVATE" +} +`, rName) +} + +func testAccCheckAwsServiceDiscoveryPublicDnsNamespaceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_service_discovery_public_dns_namespace" "test" { + name = "%[1]s.tf" +} + +data "aws_service_discovery_dns_namespace" "test" { + name = aws_service_discovery_public_dns_namespace.test.name + type = "DNS_PUBLIC" +} +`, rName) +} diff --git a/aws/data_source_aws_servicecatalog_constraint.go b/aws/data_source_aws_servicecatalog_constraint.go new file mode 100644 index 000000000000..4277b4c17714 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_constraint.go @@ -0,0 +1,96 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" +) + +func dataSourceAwsServiceCatalogConstraint() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogConstraintRead, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: tfservicecatalog.AcceptLanguageEnglish, + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Required: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "parameters": { + Type: schema.TypeString, + Computed: true, + }, + "portfolio_id": { + Type: schema.TypeString, + Computed: true, + }, + "product_id": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsServiceCatalogConstraintRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + output, err := waiter.ConstraintReady(conn, d.Get("accept_language").(string), d.Get("id").(string)) + + if err != nil { + return fmt.Errorf("error describing Service Catalog Constraint: %w", err) + } + + if output == nil || output.ConstraintDetail == nil { + return fmt.Errorf("error getting Service Catalog Constraint: empty response") + } + + acceptLanguage := d.Get("accept_language").(string) + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + d.Set("accept_language", acceptLanguage) + + d.Set("parameters", output.ConstraintParameters) + d.Set("status", output.Status) + + detail := output.ConstraintDetail + + d.Set("description", detail.Description) + d.Set("owner", detail.Owner) + d.Set("portfolio_id", detail.PortfolioId) + d.Set("product_id", detail.ProductId) + d.Set("type", detail.Type) + + d.SetId(aws.StringValue(detail.ConstraintId)) + + return nil +} diff --git a/aws/data_source_aws_servicecatalog_constraint_test.go b/aws/data_source_aws_servicecatalog_constraint_test.go new file mode 100644 index 000000000000..132f6cbf8586 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_constraint_test.go @@ -0,0 +1,46 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogConstraintDataSource_basic(t *testing.T) { + resourceName := "aws_servicecatalog_constraint.test" + dataSourceName := "data.aws_servicecatalog_constraint.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsServiceCatalogConstraintDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogConstraintDataSourceConfig_basic(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServiceCatalogConstraintExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "accept_language", dataSourceName, "accept_language"), + resource.TestCheckResourceAttrPair(resourceName, "description", dataSourceName, "description"), + resource.TestCheckResourceAttrPair(resourceName, "owner", dataSourceName, "owner"), + resource.TestCheckResourceAttrPair(resourceName, "parameters", dataSourceName, "parameters"), + resource.TestCheckResourceAttrPair(resourceName, "portfolio_id", dataSourceName, "portfolio_id"), + resource.TestCheckResourceAttrPair(resourceName, "product_id", dataSourceName, "product_id"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "type", dataSourceName, "type"), + ), + }, + }, + }) +} + +func testAccAWSServiceCatalogConstraintDataSourceConfig_basic(rName, description string) string { + return composeConfig(testAccAWSServiceCatalogConstraintConfig_basic(rName, description), ` +data "aws_servicecatalog_constraint" "test" { + id = aws_servicecatalog_constraint.test.id +} +`) +} diff --git a/aws/data_source_aws_servicecatalog_launch_paths.go b/aws/data_source_aws_servicecatalog_launch_paths.go new file mode 100644 index 000000000000..c9482923d9c3 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_launch_paths.go @@ -0,0 +1,164 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" +) + +func dataSourceAwsServiceCatalogLaunchPaths() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogLaunchPathsRead, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: "en", + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "product_id": { + Type: schema.TypeString, + Required: true, + }, + "summaries": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "constraint_summaries": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "path_id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + }, + }, + }, + }, + } +} + +func dataSourceAwsServiceCatalogLaunchPathsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + summaries, err := waiter.LaunchPathsReady(conn, d.Get("accept_language").(string), d.Get("product_id").(string)) + + if err != nil { + return fmt.Errorf("error describing Service Catalog Launch Paths: %w", err) + } + + if err := d.Set("summaries", flattenServiceCatalogLaunchPathSummaries(summaries, ignoreTagsConfig)); err != nil { + return fmt.Errorf("error setting summaries: %w", err) + } + + d.SetId(d.Get("product_id").(string)) + + return nil +} + +func flattenServiceCatalogLaunchPathSummary(apiObject *servicecatalog.LaunchPathSummary, ignoreTagsConfig *keyvaluetags.IgnoreConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if len(apiObject.ConstraintSummaries) > 0 { + tfMap["constraint_summaries"] = flattenServiceCatalogConstraintSummaries(apiObject.ConstraintSummaries) + } + + if apiObject.Id != nil { + tfMap["path_id"] = aws.StringValue(apiObject.Id) + } + + if apiObject.Name != nil { + tfMap["name"] = aws.StringValue(apiObject.Name) + } + + tags := keyvaluetags.ServicecatalogKeyValueTags(apiObject.Tags) + + tfMap["tags"] = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map() + + return tfMap +} + +func flattenServiceCatalogLaunchPathSummaries(apiObjects []*servicecatalog.LaunchPathSummary, ignoreTagsConfig *keyvaluetags.IgnoreConfig) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenServiceCatalogLaunchPathSummary(apiObject, ignoreTagsConfig)) + } + + return tfList +} + +func flattenServiceCatalogConstraintSummary(apiObject *servicecatalog.ConstraintSummary) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if apiObject.Description != nil { + tfMap["description"] = aws.StringValue(apiObject.Description) + } + + if apiObject.Type != nil { + tfMap["type"] = aws.StringValue(apiObject.Type) + } + + return tfMap +} + +func flattenServiceCatalogConstraintSummaries(apiObjects []*servicecatalog.ConstraintSummary) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenServiceCatalogConstraintSummary(apiObject)) + } + + return tfList +} diff --git a/aws/data_source_aws_servicecatalog_launch_paths_test.go b/aws/data_source_aws_servicecatalog_launch_paths_test.go new file mode 100644 index 000000000000..87533feb8ad7 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_launch_paths_test.go @@ -0,0 +1,136 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogLaunchPathsDataSource_basic(t *testing.T) { + dataSourceName := "data.aws_servicecatalog_launch_paths.test" + resourceNameProduct := "aws_servicecatalog_product.test" + resourceNamePortfolio := "aws_servicecatalog_portfolio.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + domain := fmt.Sprintf("http://%s", testAccRandomDomainName()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogLaunchPathsDataSourceConfig_basic(rName, domain, testAccDefaultEmailAddress), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "accept_language", "en"), + resource.TestCheckResourceAttrPair(dataSourceName, "product_id", resourceNameProduct, "id"), + resource.TestCheckResourceAttr(dataSourceName, "summaries.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "summaries.0.name", resourceNamePortfolio, "name"), + resource.TestCheckResourceAttrSet(dataSourceName, "summaries.0.path_id"), + ), + }, + }, + }) +} + +func testAccAWSServiceCatalogLaunchPathsDataSourceConfig_base(rName, domain, email string) string { + return fmt.Sprintf(` +resource "aws_cloudformation_stack" "test" { + name = %[1]q + + template_body = jsonencode({ + AWSTemplateFormatVersion = "2010-09-09" + + Resources = { + MyVPC = { + Type = "AWS::EC2::VPC" + Properties = { + CidrBlock = "10.1.0.0/16" + } + } + } + + Outputs = { + VpcID = { + Description = "VPC ID" + Value = { + Ref = "MyVPC" + } + } + } + }) +} + +resource "aws_servicecatalog_product" "test" { + description = "beskrivning" + distributor = "distributör" + name = %[1]q + owner = "ägare" + type = "CLOUD_FORMATION_TEMPLATE" + support_description = "supportbeskrivning" + support_email = %[3]q + support_url = %[2]q + + provisioning_artifact_parameters { + description = "artefaktbeskrivning" + name = %[1]q + template_physical_id = aws_cloudformation_stack.test.id + type = "CLOUD_FORMATION_TEMPLATE" + } + + tags = { + Name = %[1]q + } +} + +resource "aws_servicecatalog_portfolio" "test" { + name = %[1]q + provider_name = %[1]q +} + +resource "aws_servicecatalog_product_portfolio_association" "test" { + portfolio_id = aws_servicecatalog_principal_portfolio_association.test.portfolio_id + product_id = aws_servicecatalog_product.test.id +} + +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "servicecatalog.${data.aws_partition.current.dns_suffix}" + } + Sid = "" + }] + }) +} + +data "aws_caller_identity" "current" {} + +data "aws_iam_session_context" "current" { + arn = data.aws_caller_identity.current.arn +} + +resource "aws_servicecatalog_principal_portfolio_association" "test" { + portfolio_id = aws_servicecatalog_portfolio.test.id + principal_arn = data.aws_iam_session_context.current.issuer_arn +} +`, rName, domain, email) +} + +func testAccAWSServiceCatalogLaunchPathsDataSourceConfig_basic(rName, domain, email string) string { + return composeConfig(testAccAWSServiceCatalogLaunchPathsDataSourceConfig_base(rName, domain, email), ` +data "aws_servicecatalog_launch_paths" "test" { + product_id = aws_servicecatalog_product_portfolio_association.test.product_id +} +`) +} diff --git a/aws/data_source_aws_servicecatalog_portfolio.go b/aws/data_source_aws_servicecatalog_portfolio.go new file mode 100644 index 000000000000..8ad11c0924e5 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio.go @@ -0,0 +1,98 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" +) + +func dataSourceAwsServiceCatalogPortfolio() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogPortfolioRead, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: "en", + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "provider_name": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + }, + } +} + +func dataSourceAwsServiceCatalogPortfolioRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + input := &servicecatalog.DescribePortfolioInput{ + Id: aws.String(d.Get("id").(string)), + } + + if v, ok := d.GetOk("accept_language"); ok { + input.AcceptLanguage = aws.String(v.(string)) + } + + output, err := conn.DescribePortfolio(input) + + if err != nil { + return fmt.Errorf("error getting Service Catalog Portfolio (%s): %w", d.Get("id").(string), err) + } + + if output == nil || output.PortfolioDetail == nil { + return fmt.Errorf("error getting Service Catalog Portfolio (%s): empty response", d.Get("id").(string)) + } + + detail := output.PortfolioDetail + + d.SetId(aws.StringValue(detail.Id)) + + if err := d.Set("created_time", aws.TimeValue(detail.CreatedTime).Format(time.RFC3339)); err != nil { + log.Printf("[DEBUG] Error setting created_time: %s", err) + } + + d.Set("arn", detail.ARN) + d.Set("description", detail.Description) + d.Set("name", detail.DisplayName) + d.Set("provider_name", detail.ProviderName) + + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + tags := keyvaluetags.ServicecatalogKeyValueTags(output.Tags) + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_servicecatalog_portfolio_constraints.go b/aws/data_source_aws_servicecatalog_portfolio_constraints.go new file mode 100644 index 000000000000..0b89f5068518 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio_constraints.go @@ -0,0 +1,151 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" +) + +func dataSourceAwsServiceCatalogPortfolioConstraints() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogPortfolioConstraintsRead, + + Schema: map[string]*schema.Schema{ + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: tfservicecatalog.AcceptLanguageEnglish, + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "details": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "constraint_id": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "portfolio_id": { + Type: schema.TypeString, + Computed: true, + }, + "product_id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "portfolio_id": { + Type: schema.TypeString, + Required: true, + }, + "product_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceAwsServiceCatalogPortfolioConstraintsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + output, err := waiter.PortfolioConstraintsReady(conn, d.Get("accept_language").(string), d.Get("portfolio_id").(string), d.Get("product_id").(string)) + + if err != nil { + return fmt.Errorf("error describing Service Catalog Portfolio Constraints: %w", err) + } + + if len(output) == 0 { + return fmt.Errorf("error getting Service Catalog Portfolio Constraints: no results, change your input") + } + + acceptLanguage := d.Get("accept_language").(string) + + if acceptLanguage == "" { + acceptLanguage = tfservicecatalog.AcceptLanguageEnglish + } + + d.Set("accept_language", acceptLanguage) + d.Set("portfolio_id", d.Get("portfolio_id").(string)) + d.Set("product_id", d.Get("product_id").(string)) + + if err := d.Set("details", flattenServiceCatalogConstraintDetails(output)); err != nil { + return fmt.Errorf("error setting details: %w", err) + } + + d.SetId(tfservicecatalog.PortfolioConstraintsID(d.Get("accept_language").(string), d.Get("portfolio_id").(string), d.Get("product_id").(string))) + + return nil +} + +func flattenServiceCatalogConstraintDetail(apiObject *servicecatalog.ConstraintDetail) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.ConstraintId; v != nil { + tfMap["constraint_id"] = aws.StringValue(v) + } + + if v := apiObject.Description; v != nil { + tfMap["description"] = aws.StringValue(v) + } + + if v := apiObject.Owner; v != nil { + tfMap["owner"] = aws.StringValue(v) + } + + if v := apiObject.PortfolioId; v != nil { + tfMap["portfolio_id"] = aws.StringValue(v) + } + + if v := apiObject.ProductId; v != nil { + tfMap["product_id"] = aws.StringValue(v) + } + + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenServiceCatalogConstraintDetails(apiObjects []*servicecatalog.ConstraintDetail) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenServiceCatalogConstraintDetail(apiObject)) + } + + return tfList +} diff --git a/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go b/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go new file mode 100644 index 000000000000..c63eda588518 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio_constraints_test.go @@ -0,0 +1,45 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogPortfolioConstraintDataSource_basic(t *testing.T) { + resourceName := "aws_servicecatalog_constraint.test" + dataSourceName := "data.aws_servicecatalog_portfolio_constraints.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogPortfolioConstraintDataSourceConfig_basic(rName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "accept_language", resourceName, "accept_language"), + resource.TestCheckResourceAttr(dataSourceName, "details.#", "1"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.constraint_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.owner", resourceName, "owner"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.portfolio_id", resourceName, "portfolio_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.product_id", resourceName, "product_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "details.0.type", resourceName, "type"), + resource.TestCheckResourceAttrPair(dataSourceName, "portfolio_id", resourceName, "portfolio_id"), + ), + }, + }, + }) +} + +func testAccAWSServiceCatalogPortfolioConstraintDataSourceConfig_basic(rName, description string) string { + return composeConfig(testAccAWSServiceCatalogConstraintConfig_basic(rName, description), ` +data "aws_servicecatalog_portfolio_constraints" "test" { + portfolio_id = aws_servicecatalog_constraint.test.portfolio_id +} +`) +} diff --git a/aws/data_source_aws_servicecatalog_portfolio_test.go b/aws/data_source_aws_servicecatalog_portfolio_test.go new file mode 100644 index 000000000000..4306faa9df18 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_portfolio_test.go @@ -0,0 +1,44 @@ +package aws + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogPortfolioDataSource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_servicecatalog_portfolio.test" + resourceName := "aws_servicecatalog_portfolio.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceCatlaogPortfolioDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsServiceCatalogPortfolioDataSourceConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_time", dataSourceName, "created_time"), + resource.TestCheckResourceAttrPair(resourceName, "description", dataSourceName, "description"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "provider_name", dataSourceName, "provider_name"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "tags.Chicane", dataSourceName, "tags.Chicane"), + ), + }, + }, + }) +} + +func testAccCheckAwsServiceCatalogPortfolioDataSourceConfigBasic(rName string) string { + return composeConfig(testAccCheckAwsServiceCatalogPortfolioResourceConfigTags1(rName, "Chicane", "Nick"), ` +data "aws_servicecatalog_portfolio" "test" { + id = aws_servicecatalog_portfolio.test.id +} +`) +} diff --git a/aws/data_source_aws_servicecatalog_product.go b/aws/data_source_aws_servicecatalog_product.go new file mode 100644 index 000000000000..f54a52a60849 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_product.go @@ -0,0 +1,122 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/waiter" +) + +func dataSourceAwsServiceCatalogProduct() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServiceCatalogProductRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "accept_language": { + Type: schema.TypeString, + Optional: true, + Default: "en", + ValidateFunc: validation.StringInSlice(tfservicecatalog.AcceptLanguage_Values(), false), + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "distributor": { + Type: schema.TypeString, + Computed: true, + }, + "has_default_path": { + Type: schema.TypeBool, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "support_description": { + Type: schema.TypeString, + Computed: true, + }, + "support_email": { + Type: schema.TypeString, + Computed: true, + }, + "support_url": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsServiceCatalogProductRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).scconn + + output, err := waiter.ProductReady(conn, d.Get("accept_language").(string), d.Get("id").(string)) + + if err != nil { + return fmt.Errorf("error describing Service Catalog Product: %w", err) + } + + if output == nil || output.ProductViewDetail == nil || output.ProductViewDetail.ProductViewSummary == nil { + return fmt.Errorf("error getting Service Catalog Product: empty response") + } + + pvs := output.ProductViewDetail.ProductViewSummary + + d.Set("arn", output.ProductViewDetail.ProductARN) + if output.ProductViewDetail.CreatedTime != nil { + d.Set("created_time", output.ProductViewDetail.CreatedTime.Format(time.RFC3339)) + } + d.Set("description", pvs.ShortDescription) + d.Set("distributor", pvs.Distributor) + d.Set("has_default_path", pvs.HasDefaultPath) + d.Set("name", pvs.Name) + d.Set("owner", pvs.Owner) + d.Set("status", output.ProductViewDetail.Status) + d.Set("support_description", pvs.SupportDescription) + d.Set("support_email", pvs.SupportEmail) + d.Set("support_url", pvs.SupportUrl) + d.Set("type", pvs.Type) + + d.SetId(aws.StringValue(pvs.ProductId)) + + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + if err := d.Set("tags", keyvaluetags.ServicecatalogKeyValueTags(output.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_servicecatalog_product_test.go b/aws/data_source_aws_servicecatalog_product_test.go new file mode 100644 index 000000000000..7a2afc7aef48 --- /dev/null +++ b/aws/data_source_aws_servicecatalog_product_test.go @@ -0,0 +1,98 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSServiceCatalogProductDataSource_basic(t *testing.T) { + resourceName := "aws_servicecatalog_product.test" + dataSourceName := "data.aws_servicecatalog_product.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + domain := fmt.Sprintf("http://%s", testAccRandomDomainName()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsServiceCatalogProductDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogProductDataSourceConfig_basic(rName, "beskrivning", "supportbeskrivning", domain, testAccDefaultEmailAddress), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_time", dataSourceName, "created_time"), + resource.TestCheckResourceAttrPair(resourceName, "description", dataSourceName, "description"), + resource.TestCheckResourceAttrPair(resourceName, "distributor", dataSourceName, "distributor"), + resource.TestCheckResourceAttrPair(resourceName, "has_default_path", dataSourceName, "has_default_path"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "owner", dataSourceName, "owner"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "support_description", dataSourceName, "support_description"), + resource.TestCheckResourceAttrPair(resourceName, "support_email", dataSourceName, "support_email"), + resource.TestCheckResourceAttrPair(resourceName, "support_url", dataSourceName, "support_url"), + resource.TestCheckResourceAttrPair(resourceName, "type", dataSourceName, "type"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "tags.Name", dataSourceName, "tags.Name"), + ), + }, + }, + }) +} + +func TestAccAWSServiceCatalogProductDataSource_physicalID(t *testing.T) { + resourceName := "aws_servicecatalog_product.test" + dataSourceName := "data.aws_servicecatalog_product.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + domain := fmt.Sprintf("http://%s", testAccRandomDomainName()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicecatalog.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsServiceCatalogProductDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServiceCatalogProductDataSourceConfig_physicalID(rName, domain, testAccDefaultEmailAddress), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "created_time", dataSourceName, "created_time"), + resource.TestCheckResourceAttrPair(resourceName, "description", dataSourceName, "description"), + resource.TestCheckResourceAttrPair(resourceName, "distributor", dataSourceName, "distributor"), + resource.TestCheckResourceAttrPair(resourceName, "has_default_path", dataSourceName, "has_default_path"), + resource.TestCheckResourceAttrPair(resourceName, "name", dataSourceName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "owner", dataSourceName, "owner"), + resource.TestCheckResourceAttrPair(resourceName, "status", dataSourceName, "status"), + resource.TestCheckResourceAttrPair(resourceName, "support_description", dataSourceName, "support_description"), + resource.TestCheckResourceAttrPair(resourceName, "support_email", dataSourceName, "support_email"), + resource.TestCheckResourceAttrPair(resourceName, "support_url", dataSourceName, "support_url"), + resource.TestCheckResourceAttrPair(resourceName, "type", dataSourceName, "type"), + resource.TestCheckResourceAttrPair(resourceName, "tags.%", dataSourceName, "tags.%"), + resource.TestCheckResourceAttrPair(resourceName, "tags.Name", dataSourceName, "tags.Name"), + ), + }, + }, + }) +} + +func testAccAWSServiceCatalogProductDataSourceConfig_basic(rName, description, supportDescription, domain, email string) string { + return composeConfig(testAccAWSServiceCatalogProductConfig_basic(rName, description, supportDescription, domain, email), ` +data "aws_servicecatalog_product" "test" { + id = aws_servicecatalog_product.test.id +} +`) +} + +func testAccAWSServiceCatalogProductDataSourceConfig_physicalID(rName, domain, email string) string { + return composeConfig(testAccAWSServiceCatalogProductConfig_physicalID(rName, domain, email), ` +data "aws_servicecatalog_product" "test" { + id = aws_servicecatalog_product.test.id +} +`) +} diff --git a/aws/data_source_aws_servicequotas_service_quota.go b/aws/data_source_aws_servicequotas_service_quota.go index eb621f04b312..6f8b011e3c38 100644 --- a/aws/data_source_aws_servicequotas_service_quota.go +++ b/aws/data_source_aws_servicequotas_service_quota.go @@ -105,13 +105,21 @@ func dataSourceAwsServiceQuotasServiceQuotaRead(d *schema.ResourceData, meta int return fmt.Errorf("error getting Service (%s) Quota (%s): %w", serviceCode, quotaCode, err) } - if output == nil { + if output == nil || output.Quota == nil { return fmt.Errorf("error getting Service (%s) Quota (%s): empty result", serviceCode, quotaCode) } serviceQuota = output.Quota } + if serviceQuota.ErrorReason != nil { + return fmt.Errorf("error getting Service (%s) Quota (%s): %s: %s", serviceCode, quotaCode, aws.StringValue(serviceQuota.ErrorReason.ErrorCode), aws.StringValue(serviceQuota.ErrorReason.ErrorMessage)) + } + + if serviceQuota.Value == nil { + return fmt.Errorf("error getting Service (%s) Quota (%s): empty value", serviceCode, quotaCode) + } + input := &servicequotas.GetAWSDefaultServiceQuotaInput{ QuotaCode: serviceQuota.QuotaCode, ServiceCode: serviceQuota.ServiceCode, diff --git a/aws/data_source_aws_servicequotas_service_quota_test.go b/aws/data_source_aws_servicequotas_service_quota_test.go index 0b6ce5f7e0d7..ece9f2ee6198 100644 --- a/aws/data_source_aws_servicequotas_service_quota_test.go +++ b/aws/data_source_aws_servicequotas_service_quota_test.go @@ -13,8 +13,9 @@ func TestAccAwsServiceQuotasServiceQuotaDataSource_QuotaCode(t *testing.T) { dataSourceName := "data.aws_servicequotas_service_quota.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, servicequotas.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsServiceQuotasServiceQuotaDataSourceConfigQuotaCode("vpc", "L-F678F1CE"), @@ -34,12 +35,28 @@ func TestAccAwsServiceQuotasServiceQuotaDataSource_QuotaCode(t *testing.T) { }) } +func TestAccAwsServiceQuotasServiceQuotaDataSource_PermissionError_QuotaCode(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSServiceQuotas(t); testAccAssumeRoleARNPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicequotas.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsServiceQuotasServiceQuotaDataSourceConfig_PermissionError_QuotaCode("elasticloadbalancing", "L-53DA6B97"), + ExpectError: regexp.MustCompile(`DEPENDENCY_ACCESS_DENIED_ERROR`), + }, + }, + }) +} + func TestAccAwsServiceQuotasServiceQuotaDataSource_QuotaName(t *testing.T) { dataSourceName := "data.aws_servicequotas_service_quota.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, servicequotas.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsServiceQuotasServiceQuotaDataSourceConfigQuotaName("vpc", "VPCs per Region"), @@ -59,6 +76,21 @@ func TestAccAwsServiceQuotasServiceQuotaDataSource_QuotaName(t *testing.T) { }) } +func TestAccAwsServiceQuotasServiceQuotaDataSource_PermissionError_QuotaName(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSServiceQuotas(t); testAccAssumeRoleARNPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, servicequotas.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAwsServiceQuotasServiceQuotaDataSourceConfig_PermissionError_QuotaName("elasticloadbalancing", "Application Load Balancers per Region"), + ExpectError: regexp.MustCompile(`DEPENDENCY_ACCESS_DENIED_ERROR`), + }, + }, + }) +} + func testAccAwsServiceQuotasServiceQuotaDataSourceConfigQuotaCode(serviceCode, quotaCode string) string { return fmt.Sprintf(` data "aws_servicequotas_service_quota" "test" { @@ -68,6 +100,37 @@ data "aws_servicequotas_service_quota" "test" { `, quotaCode, serviceCode) } +func testAccAwsServiceQuotasServiceQuotaDataSourceConfig_PermissionError_QuotaCode(serviceCode, quotaCode string) string { + policy := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "servicequotas:GetServiceQuota" + ], + "Resource": "*" + }, + { + "Effect": "Deny", + "Action": [ + "elasticloadbalancing:*" + ], + "Resource": "*" + } + ] +}` + + return composeConfig( + testAccProviderConfigAssumeRolePolicy(policy), + fmt.Sprintf(` +data "aws_servicequotas_service_quota" "test" { + service_code = %[1]q + quota_code = %[2]q +} +`, serviceCode, quotaCode)) +} + func testAccAwsServiceQuotasServiceQuotaDataSourceConfigQuotaName(serviceCode, quotaName string) string { return fmt.Sprintf(` data "aws_servicequotas_service_quota" "test" { @@ -76,3 +139,34 @@ data "aws_servicequotas_service_quota" "test" { } `, quotaName, serviceCode) } + +func testAccAwsServiceQuotasServiceQuotaDataSourceConfig_PermissionError_QuotaName(serviceCode, quotaName string) string { + policy := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "servicequotas:ListServiceQuotas" + ], + "Resource": "*" + }, + { + "Effect": "Deny", + "Action": [ + "elasticloadbalancing:*" + ], + "Resource": "*" + } + ] +}` + + return composeConfig( + testAccProviderConfigAssumeRolePolicy(policy), + fmt.Sprintf(` +data "aws_servicequotas_service_quota" "test" { + service_code = %[1]q + quota_name = %[2]q +} +`, serviceCode, quotaName)) +} diff --git a/aws/data_source_aws_servicequotas_service_test.go b/aws/data_source_aws_servicequotas_service_test.go index 9aa922bc8d96..7979ed3a356b 100644 --- a/aws/data_source_aws_servicequotas_service_test.go +++ b/aws/data_source_aws_servicequotas_service_test.go @@ -12,8 +12,9 @@ func TestAccAwsServiceQuotasServiceDataSource_ServiceName(t *testing.T) { dataSourceName := "data.aws_servicequotas_service.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(servicequotas.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, servicequotas.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccAwsServiceQuotasServiceDataSourceConfigServiceName("Amazon Virtual Private Cloud (Amazon VPC)"), diff --git a/aws/data_source_aws_sfn_activity.go b/aws/data_source_aws_sfn_activity.go index 714855ef0072..dc226f6c71fb 100644 --- a/aws/data_source_aws_sfn_activity.go +++ b/aws/data_source_aws_sfn_activity.go @@ -50,13 +50,13 @@ func dataSourceAwsSfnActivityRead(d *schema.ResourceData, meta interface{}) erro name := nm.(string) var acts []*sfn.ActivityListItem - err := conn.ListActivitiesPages(&sfn.ListActivitiesInput{}, func(page *sfn.ListActivitiesOutput, b bool) bool { + err := conn.ListActivitiesPages(&sfn.ListActivitiesInput{}, func(page *sfn.ListActivitiesOutput, lastPage bool) bool { for _, a := range page.Activities { if name == aws.StringValue(a.Name) { acts = append(acts, a) } } - return true + return !lastPage }) if err != nil { diff --git a/aws/data_source_aws_sfn_activity_test.go b/aws/data_source_aws_sfn_activity_test.go index 4d107a0b9de7..b7a002ec1733 100644 --- a/aws/data_source_aws_sfn_activity_test.go +++ b/aws/data_source_aws_sfn_activity_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/sfn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccAWSStepFunctionsActivityDataSource_basic(t *testing.T) { dataName := "data.aws_sfn_activity.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sfn.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAWSStepFunctionsActivityDataSourceConfig_ActivityArn(rName), diff --git a/aws/data_source_aws_sfn_state_machine.go b/aws/data_source_aws_sfn_state_machine.go index e869e8075b25..302d43eed598 100644 --- a/aws/data_source_aws_sfn_state_machine.go +++ b/aws/data_source_aws_sfn_state_machine.go @@ -52,8 +52,8 @@ func dataSourceAwsSfnStateMachineRead(d *schema.ResourceData, meta interface{}) err := conn.ListStateMachinesPages(params, func(page *sfn.ListStateMachinesOutput, lastPage bool) bool { for _, sm := range page.StateMachines { - if *sm.Name == target { - arns = append(arns, *sm.StateMachineArn) + if aws.StringValue(sm.Name) == target { + arns = append(arns, aws.StringValue(sm.StateMachineArn)) } } return true diff --git a/aws/data_source_aws_sfn_state_machine_test.go b/aws/data_source_aws_sfn_state_machine_test.go index 6d5913461bf3..b4c0cb17d19f 100644 --- a/aws/data_source_aws_sfn_state_machine_test.go +++ b/aws/data_source_aws_sfn_state_machine_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/sfn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsSfnStateMachine_basic(t *testing.T) { resourceName := "aws_sfn_state_machine.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sfn.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSfnStateMachineConfig(rName), diff --git a/aws/data_source_aws_signer_signing_job_test.go b/aws/data_source_aws_signer_signing_job_test.go index 522a40ab2e55..27929e150aff 100644 --- a/aws/data_source_aws_signer_signing_job_test.go +++ b/aws/data_source_aws_signer_signing_job_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/signer" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAWSSignerSigningJob_basic(t *testing.T) { resourceName := "aws_signer_signing_job.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckSingerSigningProfile(t, "AWSLambda-SHA384-ECDSA") }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckSingerSigningProfile(t, "AWSLambda-SHA384-ECDSA") }, + ErrorCheck: testAccErrorCheck(t, signer.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSignerSigningJobConfigBasic(rName), diff --git a/aws/data_source_aws_signer_signing_profile_test.go b/aws/data_source_aws_signer_signing_profile_test.go index c8f96b41d987..c4a8c9707bc8 100644 --- a/aws/data_source_aws_signer_signing_profile_test.go +++ b/aws/data_source_aws_signer_signing_profile_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/signer" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAWSSignerSigningProfile_basic(t *testing.T) { profileName := fmt.Sprintf("tf_acc_sp_basic_%s", rString) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckSingerSigningProfile(t, "AWSLambda-SHA384-ECDSA") }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckSingerSigningProfile(t, "AWSLambda-SHA384-ECDSA") }, + ErrorCheck: testAccErrorCheck(t, signer.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSignerSigningProfileConfigBasic(profileName), diff --git a/aws/data_source_aws_sns.go b/aws/data_source_aws_sns.go deleted file mode 100644 index 2f4a6c433e7b..000000000000 --- a/aws/data_source_aws_sns.go +++ /dev/null @@ -1,71 +0,0 @@ -package aws - -import ( - "fmt" - "log" - "regexp" - - "github.com/aws/aws-sdk-go/service/sns" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceAwsSnsTopic() *schema.Resource { - return &schema.Resource{ - Read: dataSourceAwsSnsTopicsRead, - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - validNamePattern := "^[A-Za-z0-9_-]+$" - validName, nameMatchErr := regexp.MatchString(validNamePattern, value) - if !validName || nameMatchErr != nil { - errors = append(errors, fmt.Errorf( - "%q must match regex '%v'", k, validNamePattern)) - } - return - }, - }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func dataSourceAwsSnsTopicsRead(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*AWSClient).snsconn - params := &sns.ListTopicsInput{} - - target := d.Get("name") - var arns []string - log.Printf("[DEBUG] Reading SNS Topic: %s", params) - err := conn.ListTopicsPages(params, func(page *sns.ListTopicsOutput, lastPage bool) bool { - for _, topic := range page.Topics { - topicPattern := fmt.Sprintf(".*:%v$", target) - matched, regexpErr := regexp.MatchString(topicPattern, *topic.TopicArn) - if matched && regexpErr == nil { - arns = append(arns, *topic.TopicArn) - } - } - - return true - }) - if err != nil { - return fmt.Errorf("Error describing topics: %w", err) - } - - if len(arns) == 0 { - return fmt.Errorf("No topic with name %q found in this region.", target) - } - if len(arns) > 1 { - return fmt.Errorf("Multiple topics with name %q found in this region.", target) - } - - d.SetId(arns[0]) - d.Set("arn", arns[0]) - - return nil -} diff --git a/aws/data_source_aws_sns_test.go b/aws/data_source_aws_sns_test.go deleted file mode 100644 index 00ccb8f6ae2d..000000000000 --- a/aws/data_source_aws_sns_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package aws - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestAccDataSourceAwsSnsTopic_basic(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccDataSourceAwsSnsTopicConfig, - Check: resource.ComposeTestCheckFunc( - testAccDataSourceAwsSnsTopicCheck("data.aws_sns_topic.by_name"), - ), - }, - }, - }) -} - -func testAccDataSourceAwsSnsTopicCheck(name string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[name] - if !ok { - return fmt.Errorf("root module has no resource called %s", name) - } - - snsTopicRs, ok := s.RootModule().Resources["aws_sns_topic.tf_test"] - if !ok { - return fmt.Errorf("can't find aws_sns_topic.tf_test in state") - } - - attr := rs.Primary.Attributes - - if attr["name"] != snsTopicRs.Primary.Attributes["name"] { - return fmt.Errorf( - "name is %s; want %s", - attr["name"], - snsTopicRs.Primary.Attributes["name"], - ) - } - - return nil - } -} - -const testAccDataSourceAwsSnsTopicConfig = ` -resource "aws_sns_topic" "tf_wrong1" { - name = "wrong1" -} - -resource "aws_sns_topic" "tf_test" { - name = "tf_test" -} - -resource "aws_sns_topic" "tf_wrong2" { - name = "wrong2" -} - -data "aws_sns_topic" "by_name" { - name = aws_sns_topic.tf_test.name -} -` diff --git a/aws/data_source_aws_sns_topic.go b/aws/data_source_aws_sns_topic.go new file mode 100644 index 000000000000..54e2a11f2083 --- /dev/null +++ b/aws/data_source_aws_sns_topic.go @@ -0,0 +1,73 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsSnsTopic() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsSnsTopicsRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func dataSourceAwsSnsTopicsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).snsconn + + resourceArn := "" + name := d.Get("name").(string) + + err := conn.ListTopicsPages(&sns.ListTopicsInput{}, func(page *sns.ListTopicsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, topic := range page.Topics { + topicArn := aws.StringValue(topic.TopicArn) + arn, err := arn.Parse(topicArn) + + if err != nil { + log.Printf("[ERROR] %s", err) + + continue + } + + if arn.Resource == name { + resourceArn = topicArn + + break + } + } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error listing SNS Topics: %w", err) + } + + if resourceArn == "" { + return fmt.Errorf("no matching SNS Topic found") + } + + d.SetId(resourceArn) + d.Set("arn", resourceArn) + + return nil +} diff --git a/aws/data_source_aws_sns_topic_test.go b/aws/data_source_aws_sns_topic_test.go new file mode 100644 index 000000000000..53b93de0e6f3 --- /dev/null +++ b/aws/data_source_aws_sns_topic_test.go @@ -0,0 +1,43 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsSnsTopic_basic(t *testing.T) { + resourceName := "aws_sns_topic.test" + datasourceName := "data.aws_sns_topic.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sns.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsSnsTopicConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsSnsTopicConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_sns_topic" "test" { + name = %[1]q +} + +data "aws_sns_topic" "test" { + name = aws_sns_topic.test.name +} +`, rName) +} diff --git a/aws/data_source_aws_sqs_queue.go b/aws/data_source_aws_sqs_queue.go index bb9aca92d9ef..73c70c563e85 100644 --- a/aws/data_source_aws_sqs_queue.go +++ b/aws/data_source_aws_sqs_queue.go @@ -53,7 +53,7 @@ func dataSourceAwsSqsQueueRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error getting queue attributes: %w", err) } - d.Set("arn", aws.StringValue(attributesOutput.Attributes[sqs.QueueAttributeNameQueueArn])) + d.Set("arn", attributesOutput.Attributes[sqs.QueueAttributeNameQueueArn]) d.Set("url", queueURL) d.SetId(queueURL) diff --git a/aws/data_source_aws_sqs_queue_test.go b/aws/data_source_aws_sqs_queue_test.go index eb710cca0a45..602671d943c3 100644 --- a/aws/data_source_aws_sqs_queue_test.go +++ b/aws/data_source_aws_sqs_queue_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/sqs" "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" @@ -15,8 +16,9 @@ func TestAccDataSourceAwsSqsQueue_basic(t *testing.T) { datasourceName := "data.aws_sqs_queue.by_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sqs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSqsQueueConfig(rName), @@ -35,8 +37,9 @@ func TestAccDataSourceAwsSqsQueue_tags(t *testing.T) { datasourceName := "data.aws_sqs_queue.by_name" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, sqs.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSqsQueueConfigTags(rName), diff --git a/aws/data_source_aws_ssm_document_test.go b/aws/data_source_aws_ssm_document_test.go index d2bf0d727e5c..1e86e69a48df 100644 --- a/aws/data_source_aws_ssm_document_test.go +++ b/aws/data_source_aws_ssm_document_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ssm" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccAWSSsmDocumentDataSource_basic(t *testing.T) { name := fmt.Sprintf("test_document-%d", acctest.RandInt()) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSsmDocumentDataSourceConfig(name, "JSON"), diff --git a/aws/data_source_aws_ssm_parameter_test.go b/aws/data_source_aws_ssm_parameter_test.go index 65c484943fd8..b8f64fd13182 100644 --- a/aws/data_source_aws_ssm_parameter_test.go +++ b/aws/data_source_aws_ssm_parameter_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ssm" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,10 +14,9 @@ func TestAccAWSSsmParameterDataSource_basic(t *testing.T) { name := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSsmParameterDataSourceConfig(name, "false"), @@ -48,10 +48,9 @@ func TestAccAWSSsmParameterDataSource_fullPath(t *testing.T) { name := acctest.RandomWithPrefix("/tf-acc-test/tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSsmParameterDataSourceConfig(name, "false"), diff --git a/aws/data_source_aws_ssm_parameters_by_path.go b/aws/data_source_aws_ssm_parameters_by_path.go new file mode 100644 index 000000000000..f02e116d669e --- /dev/null +++ b/aws/data_source_aws_ssm_parameters_by_path.go @@ -0,0 +1,90 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsSsmParametersByPath() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsSsmParametersReadByPath, + + Schema: map[string]*schema.Schema{ + "arns": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "names": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "path": { + Type: schema.TypeString, + Required: true, + }, + "types": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "values": { + Type: schema.TypeList, + Computed: true, + Sensitive: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "with_decryption": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func dataSourceAwsSsmParametersReadByPath(d *schema.ResourceData, meta interface{}) error { + ssmconn := meta.(*AWSClient).ssmconn + + path := d.Get("path").(string) + input := &ssm.GetParametersByPathInput{ + Path: aws.String(path), + WithDecryption: aws.Bool(d.Get("with_decryption").(bool)), + } + + arns := make([]string, 0) + names := make([]string, 0) + types := make([]string, 0) + values := make([]string, 0) + + err := ssmconn.GetParametersByPathPages(input, func(page *ssm.GetParametersByPathOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, param := range page.Parameters { + arns = append(arns, aws.StringValue(param.ARN)) + names = append(names, aws.StringValue(param.Name)) + types = append(types, aws.StringValue(param.Type)) + values = append(values, aws.StringValue(param.Value)) + } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error getting SSM parameters by path (%s): %w", path, err) + } + + d.SetId(path) + d.Set("arns", arns) + d.Set("names", names) + d.Set("types", types) + d.Set("values", values) + + return nil +} diff --git a/aws/data_source_aws_ssm_parameters_by_path_test.go b/aws/data_source_aws_ssm_parameters_by_path_test.go new file mode 100644 index 000000000000..dd14198835f3 --- /dev/null +++ b/aws/data_source_aws_ssm_parameters_by_path_test.go @@ -0,0 +1,67 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAWSSsmParametersByPathDataSource_basic(t *testing.T) { + resourceName := "data.aws_ssm_parameters_by_path.test" + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsSsmParametersByPathDataSourceConfig(rName1, rName2, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "arns.#", "2"), + resource.TestCheckResourceAttr(resourceName, "names.#", "2"), + resource.TestCheckResourceAttr(resourceName, "types.#", "2"), + resource.TestCheckResourceAttr(resourceName, "values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "with_decryption", "false"), + ), + }, + }, + }) +} + +func testAccCheckAwsSsmParametersByPathDataSourceConfig(rName1, rName2 string, withDecryption bool) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "test1" { + name = "/%[1]s/param-a" + type = "String" + value = "TestValueA" +} + +resource "aws_ssm_parameter" "test2" { + name = "/%[1]s/param-b" + type = "String" + value = "TestValueB" +} + +resource "aws_ssm_parameter" "test3" { + name = "/%[2]s/param-c" + type = "String" + value = "TestValueC" +} + +data "aws_ssm_parameters_by_path" "test" { + path = "/%[1]s" + with_decryption = %[3]t + + depends_on = [ + aws_ssm_parameter.test1, + aws_ssm_parameter.test2, + aws_ssm_parameter.test3, + ] +} +`, rName1, rName2, withDecryption) +} diff --git a/aws/data_source_aws_ssm_patch_baseline.go b/aws/data_source_aws_ssm_patch_baseline.go index 61ed4a5f3c2c..082b694af784 100644 --- a/aws/data_source_aws_ssm_patch_baseline.go +++ b/aws/data_source_aws_ssm_patch_baseline.go @@ -82,7 +82,7 @@ func dataAwsSsmPatchBaselineRead(d *schema.ResourceData, meta interface{}) error var filteredBaselines []*ssm.PatchBaselineIdentity if v, ok := d.GetOk("operating_system"); ok { for _, baseline := range resp.BaselineIdentities { - if v.(string) == *baseline.OperatingSystem { + if v.(string) == aws.StringValue(baseline.OperatingSystem) { filteredBaselines = append(filteredBaselines, baseline) } } @@ -97,7 +97,7 @@ func dataAwsSsmPatchBaselineRead(d *schema.ResourceData, meta interface{}) error } } - if len(filteredBaselines) < 1 { + if len(filteredBaselines) < 1 || filteredBaselines[0] == nil { return fmt.Errorf("Your query returned no results. Please change your search criteria and try again.") } @@ -105,7 +105,7 @@ func dataAwsSsmPatchBaselineRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Your query returned more than one result. Please try a more specific search criteria") } - baseline := *filteredBaselines[0] + baseline := filteredBaselines[0] d.SetId(aws.StringValue(baseline.BaselineId)) d.Set("name", baseline.BaselineName) diff --git a/aws/data_source_aws_ssm_patch_baseline_test.go b/aws/data_source_aws_ssm_patch_baseline_test.go index 292922d06bc0..a2d661a0997f 100644 --- a/aws/data_source_aws_ssm_patch_baseline_test.go +++ b/aws/data_source_aws_ssm_patch_baseline_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ssm" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccAWSSsmPatchBaselineDataSource_existingBaseline(t *testing.T) { resourceName := "data.aws_ssm_patch_baseline.test_existing" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccCheckAwsSsmPatchBaselineDataSourceConfig_existingBaseline(), @@ -32,6 +34,7 @@ func TestAccAWSSsmPatchBaselineDataSource_newBaseline(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSSSMPatchBaselineDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_ssoadmin_instances_test.go b/aws/data_source_aws_ssoadmin_instances_test.go index a4c4eb48fb9b..f2c2a9158964 100644 --- a/aws/data_source_aws_ssoadmin_instances_test.go +++ b/aws/data_source_aws_ssoadmin_instances_test.go @@ -39,8 +39,9 @@ func TestAccDataSourceAWSSSOAdminInstances_basic(t *testing.T) { dataSourceName := "data.aws_ssoadmin_instances.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, + ErrorCheck: testAccErrorCheck(t, ssoadmin.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSSOAdminInstancesConfigBasic, diff --git a/aws/data_source_aws_ssoadmin_permission_set_test.go b/aws/data_source_aws_ssoadmin_permission_set_test.go index d83cfc949309..c11864d0379d 100644 --- a/aws/data_source_aws_ssoadmin_permission_set_test.go +++ b/aws/data_source_aws_ssoadmin_permission_set_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ssoadmin" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAWSSSOAdminPermissionSet_arn(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, + ErrorCheck: testAccErrorCheck(t, ssoadmin.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSSOPermissionSetByArnConfig(rName), @@ -39,8 +41,9 @@ func TestAccDataSourceAWSSSOAdminPermissionSet_name(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, + ErrorCheck: testAccErrorCheck(t, ssoadmin.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSSOPermissionSetByNameConfig(rName), @@ -60,8 +63,9 @@ func TestAccDataSourceAWSSSOAdminPermissionSet_name(t *testing.T) { func TestAccDataSourceAWSSSOAdminPermissionSet_nonExistent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSSSOAdminInstances(t) }, + ErrorCheck: testAccErrorCheck(t, ssoadmin.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAWSSSOPermissionSetByNameConfig_nonExistent, diff --git a/aws/data_source_aws_storagegateway_local_disk.go b/aws/data_source_aws_storagegateway_local_disk.go index 61ea759a4530..247d83c6684a 100644 --- a/aws/data_source_aws_storagegateway_local_disk.go +++ b/aws/data_source_aws_storagegateway_local_disk.go @@ -22,10 +22,12 @@ func dataSourceAwsStorageGatewayLocalDisk() *schema.Resource { "disk_node": { Type: schema.TypeString, Optional: true, + Computed: true, }, "disk_path": { Type: schema.TypeString, Optional: true, + Computed: true, }, "gateway_arn": { Type: schema.TypeString, diff --git a/aws/data_source_aws_storagegateway_local_disk_test.go b/aws/data_source_aws_storagegateway_local_disk_test.go index 2c37af7da9cf..6ca557cf330c 100644 --- a/aws/data_source_aws_storagegateway_local_disk_test.go +++ b/aws/data_source_aws_storagegateway_local_disk_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/storagegateway" "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" @@ -16,6 +17,7 @@ func TestAccAWSStorageGatewayLocalDiskDataSource_DiskNode(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, storagegateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSStorageGatewayGatewayDestroy, Steps: []resource.TestStep{ @@ -27,7 +29,9 @@ func TestAccAWSStorageGatewayLocalDiskDataSource_DiskNode(t *testing.T) { Config: testAccAWSStorageGatewayLocalDiskDataSourceConfig_DiskNode(rName), Check: resource.ComposeTestCheckFunc( testAccAWSStorageGatewayLocalDiskDataSourceExists(dataSourceName), - resource.TestCheckResourceAttrSet(dataSourceName, "disk_id"), + resource.TestMatchResourceAttr(dataSourceName, "disk_id", regexp.MustCompile(`.+`)), + resource.TestMatchResourceAttr(dataSourceName, "disk_node", regexp.MustCompile(`.+`)), + resource.TestMatchResourceAttr(dataSourceName, "disk_path", regexp.MustCompile(`.+`)), ), }, }, @@ -40,6 +44,7 @@ func TestAccAWSStorageGatewayLocalDiskDataSource_DiskPath(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, storagegateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSStorageGatewayGatewayDestroy, Steps: []resource.TestStep{ @@ -51,7 +56,9 @@ func TestAccAWSStorageGatewayLocalDiskDataSource_DiskPath(t *testing.T) { Config: testAccAWSStorageGatewayLocalDiskDataSourceConfig_DiskPath(rName), Check: resource.ComposeTestCheckFunc( testAccAWSStorageGatewayLocalDiskDataSourceExists(dataSourceName), - resource.TestCheckResourceAttrSet(dataSourceName, "disk_id"), + resource.TestMatchResourceAttr(dataSourceName, "disk_id", regexp.MustCompile(`.+`)), + resource.TestMatchResourceAttr(dataSourceName, "disk_node", regexp.MustCompile(`.+`)), + resource.TestMatchResourceAttr(dataSourceName, "disk_path", regexp.MustCompile(`.+`)), ), }, }, diff --git a/aws/data_source_aws_subnet.go b/aws/data_source_aws_subnet.go index cf0ee0f056e9..8ef4c3424c08 100644 --- a/aws/data_source_aws_subnet.go +++ b/aws/data_source_aws_subnet.go @@ -15,95 +15,82 @@ func dataSourceAwsSubnet() *schema.Resource { Read: dataSourceAwsSubnetRead, Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "assign_ipv6_address_on_creation": { + Type: schema.TypeBool, + Computed: true, + }, "availability_zone": { Type: schema.TypeString, Optional: true, Computed: true, }, - "availability_zone_id": { Type: schema.TypeString, Optional: true, Computed: true, }, - - "cidr_block": { - Type: schema.TypeString, - Optional: true, + "available_ip_address_count": { + Type: schema.TypeInt, Computed: true, }, - - "ipv6_cidr_block": { + "cidr_block": { Type: schema.TypeString, Optional: true, Computed: true, }, - - "outpost_arn": { + "customer_owned_ipv4_pool": { Type: schema.TypeString, Computed: true, }, - "default_for_az": { Type: schema.TypeBool, Optional: true, Computed: true, }, - "filter": ec2CustomFiltersSchema(), - "id": { Type: schema.TypeString, Optional: true, Computed: true, }, - - "state": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - - "tags": tagsSchemaComputed(), - - "vpc_id": { + "ipv6_cidr_block": { Type: schema.TypeString, Optional: true, Computed: true, }, - - "assign_ipv6_address_on_creation": { - Type: schema.TypeBool, - Computed: true, - }, - - "customer_owned_ipv4_pool": { + "ipv6_cidr_block_association_id": { Type: schema.TypeString, Computed: true, }, - "map_customer_owned_ip_on_launch": { Type: schema.TypeBool, Computed: true, }, - "map_public_ip_on_launch": { Type: schema.TypeBool, Computed: true, }, - - "ipv6_cidr_block_association_id": { + "outpost_arn": { Type: schema.TypeString, Computed: true, }, - - "arn": { + "owner_id": { Type: schema.TypeString, Computed: true, }, - - "owner_id": { + "state": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "tags": tagsSchemaComputed(), + "vpc_id": { Type: schema.TypeString, + Optional: true, Computed: true, }, }, @@ -177,32 +164,34 @@ func dataSourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error { subnet := resp.Subnets[0] d.SetId(aws.StringValue(subnet.SubnetId)) - d.Set("vpc_id", subnet.VpcId) - d.Set("availability_zone", subnet.AvailabilityZone) - d.Set("availability_zone_id", subnet.AvailabilityZoneId) - d.Set("cidr_block", subnet.CidrBlock) - d.Set("default_for_az", subnet.DefaultForAz) - d.Set("state", subnet.State) - d.Set("outpost_arn", subnet.OutpostArn) - - if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(subnet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } + d.Set("arn", subnet.SubnetArn) d.Set("assign_ipv6_address_on_creation", subnet.AssignIpv6AddressOnCreation) + d.Set("availability_zone_id", subnet.AvailabilityZoneId) + d.Set("availability_zone", subnet.AvailabilityZone) + d.Set("available_ip_address_count", subnet.AvailableIpAddressCount) + d.Set("cidr_block", subnet.CidrBlock) d.Set("customer_owned_ipv4_pool", subnet.CustomerOwnedIpv4Pool) - d.Set("map_customer_owned_ip_on_launch", subnet.MapCustomerOwnedIpOnLaunch) - d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch) + d.Set("default_for_az", subnet.DefaultForAz) for _, a := range subnet.Ipv6CidrBlockAssociationSet { - if *a.Ipv6CidrBlockState.State == "associated" { //we can only ever have 1 IPv6 block associated at once + if a.Ipv6CidrBlockState != nil && aws.StringValue(a.Ipv6CidrBlockState.State) == ec2.VpcCidrBlockStateCodeAssociated { //we can only ever have 1 IPv6 block associated at once d.Set("ipv6_cidr_block_association_id", a.AssociationId) d.Set("ipv6_cidr_block", a.Ipv6CidrBlock) } } - d.Set("arn", subnet.SubnetArn) + d.Set("map_customer_owned_ip_on_launch", subnet.MapCustomerOwnedIpOnLaunch) + d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch) + d.Set("outpost_arn", subnet.OutpostArn) d.Set("owner_id", subnet.OwnerId) + d.Set("state", subnet.State) + + if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(subnet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + d.Set("vpc_id", subnet.VpcId) return nil } diff --git a/aws/data_source_aws_subnet_ids_test.go b/aws/data_source_aws_subnet_ids_test.go index 8b4615ba89e6..afa25a12111e 100644 --- a/aws/data_source_aws_subnet_ids_test.go +++ b/aws/data_source_aws_subnet_ids_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,6 +13,7 @@ func TestAccDataSourceAwsSubnetIDs_basic(t *testing.T) { rInt := acctest.RandIntRange(0, 256) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -35,6 +37,7 @@ func TestAccDataSourceAwsSubnetIDs_filter(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ diff --git a/aws/data_source_aws_subnet_test.go b/aws/data_source_aws_subnet_test.go index 053309b85036..da7307be7a9e 100644 --- a/aws/data_source_aws_subnet_test.go +++ b/aws/data_source_aws_subnet_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -24,6 +25,7 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ @@ -34,6 +36,7 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) { resource.TestCheckResourceAttrPair(ds1ResourceName, "owner_id", snResourceName, "owner_id"), resource.TestCheckResourceAttrPair(ds1ResourceName, "availability_zone", snResourceName, "availability_zone"), resource.TestCheckResourceAttrPair(ds1ResourceName, "availability_zone_id", snResourceName, "availability_zone_id"), + resource.TestCheckResourceAttrSet(ds1ResourceName, "available_ip_address_count"), resource.TestCheckResourceAttrPair(ds1ResourceName, "vpc_id", vpcResourceName, "id"), resource.TestCheckResourceAttr(ds1ResourceName, "cidr_block", cidr), resource.TestCheckResourceAttr(ds1ResourceName, "tags.Name", tag), @@ -46,6 +49,7 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) { resource.TestCheckResourceAttrPair(ds2ResourceName, "owner_id", snResourceName, "owner_id"), resource.TestCheckResourceAttrPair(ds2ResourceName, "availability_zone", snResourceName, "availability_zone"), resource.TestCheckResourceAttrPair(ds2ResourceName, "availability_zone_id", snResourceName, "availability_zone_id"), + resource.TestCheckResourceAttrSet(ds2ResourceName, "available_ip_address_count"), resource.TestCheckResourceAttrPair(ds2ResourceName, "vpc_id", vpcResourceName, "id"), resource.TestCheckResourceAttr(ds2ResourceName, "cidr_block", cidr), resource.TestCheckResourceAttr(ds2ResourceName, "tags.Name", tag), @@ -58,6 +62,7 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) { resource.TestCheckResourceAttrPair(ds3ResourceName, "owner_id", snResourceName, "owner_id"), resource.TestCheckResourceAttrPair(ds3ResourceName, "availability_zone", snResourceName, "availability_zone"), resource.TestCheckResourceAttrPair(ds3ResourceName, "availability_zone_id", snResourceName, "availability_zone_id"), + resource.TestCheckResourceAttrSet(ds3ResourceName, "available_ip_address_count"), resource.TestCheckResourceAttrPair(ds3ResourceName, "vpc_id", vpcResourceName, "id"), resource.TestCheckResourceAttr(ds3ResourceName, "cidr_block", cidr), resource.TestCheckResourceAttr(ds3ResourceName, "tags.Name", tag), @@ -110,8 +115,9 @@ func TestAccDataSourceAwsSubnet_basic(t *testing.T) { func TestAccDataSourceAwsSubnet_ipv6ByIpv6Filter(t *testing.T) { rInt := acctest.RandIntRange(0, 256) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSubnetConfigIpv6(rInt), @@ -130,8 +136,9 @@ func TestAccDataSourceAwsSubnet_ipv6ByIpv6Filter(t *testing.T) { func TestAccDataSourceAwsSubnet_ipv6ByIpv6CidrBlock(t *testing.T) { rInt := acctest.RandIntRange(0, 256) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsSubnetConfigIpv6(rInt), diff --git a/aws/data_source_aws_subnets.go b/aws/data_source_aws_subnets.go new file mode 100644 index 000000000000..053d24ef6012 --- /dev/null +++ b/aws/data_source_aws_subnets.go @@ -0,0 +1,69 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsSubnets() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsSubnetsRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "tags": tagsSchemaComputed(), + + "ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceAwsSubnetsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + input := &ec2.DescribeSubnetsInput{} + + if tags, tagsOk := d.GetOk("tags"); tagsOk { + input.Filters = append(input.Filters, buildEC2TagFilterList( + keyvaluetags.New(tags.(map[string]interface{})).Ec2Tags(), + )...) + } + + if filters, filtersOk := d.GetOk("filter"); filtersOk { + input.Filters = append(input.Filters, + buildAwsDataSourceFilters(filters.(*schema.Set))...) + } + + if len(input.Filters) == 0 { + input.Filters = nil + } + + var subnetIDs []*string + err := conn.DescribeSubnetsPages(input, func(page *ec2.DescribeSubnetsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, subnet := range page.Subnets { + subnetIDs = append(subnetIDs, subnet.SubnetId) + } + + return !lastPage + }) + + if err != nil { + return fmt.Errorf("error reading Subnets: %w", err) + } + + d.SetId(meta.(*AWSClient).region) + d.Set("ids", aws.StringValueSlice(subnetIDs)) + + return nil +} diff --git a/aws/data_source_aws_subnets_test.go b/aws/data_source_aws_subnets_test.go new file mode 100644 index 000000000000..baff9a134334 --- /dev/null +++ b/aws/data_source_aws_subnets_test.go @@ -0,0 +1,198 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsSubnets_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsSubnetsConfig(rName), + }, + { + Config: testAccDataSourceAwsSubnetsConfigWithDataSource(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.aws_subnets.selected", "ids.#", "4"), + resource.TestCheckResourceAttr("data.aws_subnets.private", "ids.#", "2"), + testCheckResourceAttrGreaterThanValue("data.aws_subnets.all", "ids.#", "0"), + resource.TestCheckResourceAttr("data.aws_subnets.none", "ids.#", "0"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsSubnets_filter(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsSubnets_filter(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.aws_subnets.test", "ids.#", "2"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsSubnetsConfig(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "172.16.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test_public_a" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.123.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-public-a" + Tier = "Public" + } +} + +resource "aws_subnet" "test_public_b" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.124.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-public-b" + Tier = "Public" + } +} + +resource "aws_subnet" "test_private_a" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.125.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-private-a" + Tier = "Private" + } +} + +resource "aws_subnet" "test_private_b" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.126.0/24" + availability_zone = data.aws_availability_zones.available.names[1] + + tags = { + Name = "%[1]s-private-b" + Tier = "Private" + } +} +`, rName)) +} + +func testAccDataSourceAwsSubnetsConfigWithDataSource(rName string) string { + return composeConfig(testAccDataSourceAwsSubnetsConfig(rName), ` +data "aws_subnets" "selected" { + filter { + name = "vpc-id" + values = [aws_vpc.test.id] + } +} + +data "aws_subnets" "private" { + filter { + name = "vpc-id" + values = [aws_vpc.test.id] + } + + tags = { + Tier = "Private" + } +} + +data "aws_subnets" "all" {} + +data "aws_subnets" "none" { + filter { + name = "vpc-id" + values = [aws_vpc.test.id] + } + + filter { + name = "cidr-block" + values = ["172.16.127.0/24"] + } +} +`) +} + +func testAccDataSourceAwsSubnets_filter(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "172.16.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test_a_one" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-a1" + } +} + +resource "aws_subnet" "test_a_two" { + vpc_id = aws_subnet.test_b.vpc_id + cidr_block = "172.16.2.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-a2" + } +} + +resource "aws_subnet" "test_b" { + vpc_id = aws_vpc.test.id + cidr_block = "172.16.3.0/24" + availability_zone = data.aws_availability_zones.available.names[1] + + tags = { + Name = "%[1]s-b" + } +} + +data "aws_subnets" "test" { + filter { + name = "availabilityZone" + values = [aws_subnet.test_a_one.availability_zone] + } + + filter { + name = "vpc-id" + values = [aws_subnet.test_a_two.vpc_id] + } +} +`, rName)) +} diff --git a/aws/data_source_aws_transfer_server.go b/aws/data_source_aws_transfer_server.go index 0ac5364113ca..3359335cd43a 100644 --- a/aws/data_source_aws_transfer_server.go +++ b/aws/data_source_aws_transfer_server.go @@ -2,46 +2,79 @@ package aws import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/transfer" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/finder" ) func dataSourceAwsTransferServer() *schema.Resource { return &schema.Resource{ - Read: dataSourceAwsTransferServerRead, Schema: map[string]*schema.Schema{ - "server_id": { + "arn": { Type: schema.TypeString, - Required: true, + Computed: true, }, - "arn": { + + "certificate": { Type: schema.TypeString, Computed: true, }, - "endpoint": { + "domain": { Type: schema.TypeString, Computed: true, }, - "invocation_role": { + + "endpoint": { Type: schema.TypeString, Computed: true, }, - "url": { + + "endpoint_type": { Type: schema.TypeString, Computed: true, }, + "identity_provider_type": { Type: schema.TypeString, Computed: true, }, + + "invocation_role": { + Type: schema.TypeString, + Computed: true, + }, + "logging_role": { Type: schema.TypeString, Computed: true, }, + + "protocols": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "security_policy_name": { + Type: schema.TypeString, + Computed: true, + }, + + "server_id": { + Type: schema.TypeString, + Required: true, + }, + + "url": { + Type: schema.TypeString, + Computed: true, + }, }, + + Read: dataSourceAwsTransferServerRead, } } @@ -49,28 +82,33 @@ func dataSourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) e conn := meta.(*AWSClient).transferconn serverID := d.Get("server_id").(string) - input := &transfer.DescribeServerInput{ - ServerId: aws.String(serverID), - } - log.Printf("[DEBUG] Describe Transfer Server Option: %#v", input) + output, err := finder.ServerByID(conn, serverID) - resp, err := conn.DescribeServer(input) if err != nil { - return fmt.Errorf("error describing Transfer Server (%s): %w", serverID, err) + return fmt.Errorf("error reading Transfer Server (%s): %w", serverID, err) } - endpoint := meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", serverID)) - - d.SetId(serverID) - d.Set("arn", resp.Server.Arn) - d.Set("endpoint", endpoint) - if resp.Server.IdentityProviderDetails != nil { - d.Set("invocation_role", aws.StringValue(resp.Server.IdentityProviderDetails.InvocationRole)) - d.Set("url", aws.StringValue(resp.Server.IdentityProviderDetails.Url)) + d.SetId(aws.StringValue(output.ServerId)) + d.Set("arn", output.Arn) + d.Set("certificate", output.Certificate) + d.Set("domain", output.Domain) + d.Set("endpoint", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", serverID))) + d.Set("endpoint_type", output.EndpointType) + d.Set("identity_provider_type", output.IdentityProviderType) + if output.IdentityProviderDetails != nil { + d.Set("invocation_role", output.IdentityProviderDetails.InvocationRole) + } else { + d.Set("invocation_role", "") + } + d.Set("logging_role", output.LoggingRole) + d.Set("protocols", aws.StringValueSlice(output.Protocols)) + d.Set("security_policy_name", output.SecurityPolicyName) + if output.IdentityProviderDetails != nil { + d.Set("url", output.IdentityProviderDetails.Url) + } else { + d.Set("url", "") } - d.Set("identity_provider_type", resp.Server.IdentityProviderType) - d.Set("logging_role", resp.Server.LoggingRole) return nil } diff --git a/aws/data_source_aws_transfer_server_test.go b/aws/data_source_aws_transfer_server_test.go index e624772be42e..b70552453010 100644 --- a/aws/data_source_aws_transfer_server_test.go +++ b/aws/data_source_aws_transfer_server_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/transfer" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,13 +14,15 @@ func TestAccDataSourceAwsTransferServer_basic(t *testing.T) { datasourceName := "data.aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, + ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsTransferServerConfig_basic, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "domain", resourceName, "domain"), resource.TestCheckResourceAttrPair(datasourceName, "endpoint", resourceName, "endpoint"), resource.TestCheckResourceAttrPair(datasourceName, "identity_provider_type", resourceName, "identity_provider_type"), resource.TestCheckResourceAttrPair(datasourceName, "logging_role", resourceName, "logging_role"), @@ -35,16 +38,23 @@ func TestAccDataSourceAwsTransferServer_service_managed(t *testing.T) { datasourceName := "data.aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, + ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsTransferServerConfig_service_managed(rName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "certificate", resourceName, "certificate"), resource.TestCheckResourceAttrPair(datasourceName, "endpoint", resourceName, "endpoint"), + resource.TestCheckResourceAttrPair(datasourceName, "endpoint_type", resourceName, "endpoint_type"), resource.TestCheckResourceAttrPair(datasourceName, "identity_provider_type", resourceName, "identity_provider_type"), + resource.TestCheckResourceAttrPair(datasourceName, "invocation_role", resourceName, "invocation_role"), resource.TestCheckResourceAttrPair(datasourceName, "logging_role", resourceName, "logging_role"), + resource.TestCheckResourceAttrPair(datasourceName, "protocols.#", resourceName, "protocols.#"), + resource.TestCheckResourceAttrPair(datasourceName, "security_policy_name", resourceName, "security_policy_name"), + resource.TestCheckResourceAttrPair(datasourceName, "url", resourceName, "url"), ), }, }, @@ -57,8 +67,9 @@ func TestAccDataSourceAwsTransferServer_apigateway(t *testing.T) { datasourceName := "data.aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsTransferServerConfig_apigateway(rName), diff --git a/aws/data_source_aws_vpc.go b/aws/data_source_aws_vpc.go index 713ca7b35d03..430fcd62b916 100644 --- a/aws/data_source_aws_vpc.go +++ b/aws/data_source_aws_vpc.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" ) func dataSourceAwsVpc() *schema.Resource { @@ -191,9 +192,9 @@ func dataSourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error { arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, - AccountID: meta.(*AWSClient).accountid, + AccountID: aws.StringValue(vpc.OwnerId), Resource: fmt.Sprintf("vpc/%s", d.Id()), }.String() d.Set("arn", arn) @@ -216,17 +217,21 @@ func dataSourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error { d.Set("ipv6_cidr_block", vpc.Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock) } - attResp, err := awsVpcDescribeVpcAttribute("enableDnsSupport", aws.StringValue(vpc.VpcId), conn) + enableDnsHostnames, err := finder.VpcAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames) + if err != nil { - return err + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsHostnames, err) } - d.Set("enable_dns_support", attResp.EnableDnsSupport.Value) - attResp, err = awsVpcDescribeVpcAttribute("enableDnsHostnames", aws.StringValue(vpc.VpcId), conn) + d.Set("enable_dns_hostnames", enableDnsHostnames) + + enableDnsSupport, err := finder.VpcAttribute(conn, aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport) + if err != nil { - return err + return fmt.Errorf("error reading EC2 VPC (%s) Attribute (%s): %w", aws.StringValue(vpc.VpcId), ec2.VpcAttributeNameEnableDnsSupport, err) } - d.Set("enable_dns_hostnames", attResp.EnableDnsHostnames.Value) + + d.Set("enable_dns_support", enableDnsSupport) routeTableId, err := resourceAwsVpcSetMainRouteTable(conn, aws.StringValue(vpc.VpcId)) if err != nil { diff --git a/aws/data_source_aws_vpc_dhcp_options.go b/aws/data_source_aws_vpc_dhcp_options.go index 2ba6914a1702..576239ed1b68 100644 --- a/aws/data_source_aws_vpc_dhcp_options.go +++ b/aws/data_source_aws_vpc_dhcp_options.go @@ -111,7 +111,7 @@ func dataSourceAwsVpcDhcpOptionsRead(d *schema.ResourceData, meta interface{}) e switch key { case "domain-name": - d.Set(tfKey, aws.StringValue(dhcpConfiguration.Values[0].Value)) + d.Set(tfKey, dhcpConfiguration.Values[0].Value) case "domain-name-servers": if err := d.Set(tfKey, flattenEc2AttributeValues(dhcpConfiguration.Values)); err != nil { return fmt.Errorf("error setting %s: %w", tfKey, err) @@ -121,7 +121,7 @@ func dataSourceAwsVpcDhcpOptionsRead(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("error setting %s: %w", tfKey, err) } case "netbios-node-type": - d.Set(tfKey, aws.StringValue(dhcpConfiguration.Values[0].Value)) + d.Set(tfKey, dhcpConfiguration.Values[0].Value) case "ntp-servers": if err := d.Set(tfKey, flattenEc2AttributeValues(dhcpConfiguration.Values)); err != nil { return fmt.Errorf("error setting %s: %w", tfKey, err) @@ -136,9 +136,9 @@ func dataSourceAwsVpcDhcpOptionsRead(d *schema.ResourceData, meta interface{}) e arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, - AccountID: meta.(*AWSClient).accountid, + AccountID: aws.StringValue(output.DhcpOptions[0].OwnerId), Resource: fmt.Sprintf("dhcp-options/%s", d.Id()), }.String() diff --git a/aws/data_source_aws_vpc_dhcp_options_test.go b/aws/data_source_aws_vpc_dhcp_options_test.go index 2e00e4fba2bc..bf42a68fa240 100644 --- a/aws/data_source_aws_vpc_dhcp_options_test.go +++ b/aws/data_source_aws_vpc_dhcp_options_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsVpcDhcpOptions_basic(t *testing.T) { datasourceName := "data.aws_vpc_dhcp_options.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcDhcpOptionsConfig_Missing, @@ -50,8 +52,9 @@ func TestAccDataSourceAwsVpcDhcpOptions_Filter(t *testing.T) { datasourceName := "data.aws_vpc_dhcp_options.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcDhcpOptionsConfig_Filter(rInt, 1), diff --git a/aws/data_source_aws_vpc_endpoint.go b/aws/data_source_aws_vpc_endpoint.go index 59ddd1b1f231..3dee22f20b01 100644 --- a/aws/data_source_aws_vpc_endpoint.go +++ b/aws/data_source_aws_vpc_endpoint.go @@ -161,9 +161,9 @@ func dataSourceAwsVpcEndpointRead(d *schema.ResourceData, meta interface{}) erro arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, - AccountID: meta.(*AWSClient).accountid, + AccountID: aws.StringValue(vpce.OwnerId), Resource: fmt.Sprintf("vpc-endpoint/%s", d.Id()), }.String() d.Set("arn", arn) diff --git a/aws/data_source_aws_vpc_endpoint_service.go b/aws/data_source_aws_vpc_endpoint_service.go index 337534077cab..15626866d16c 100644 --- a/aws/data_source_aws_vpc_endpoint_service.go +++ b/aws/data_source_aws_vpc_endpoint_service.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) @@ -66,9 +67,10 @@ func dataSourceAwsVpcEndpointService() *schema.Resource { ConflictsWith: []string{"service"}, }, "service_type": { - Type: schema.TypeString, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(ec2.ServiceType_Values(), false), }, "tags": tagsSchemaComputed(), "vpc_endpoint_policy_supported": { @@ -104,6 +106,14 @@ func dataSourceAwsVpcEndpointServiceRead(d *schema.ResourceData, meta interface{ if serviceNameOk { req.ServiceNames = aws.StringSlice([]string{serviceName}) } + + if v, ok := d.GetOk("service_type"); ok { + req.Filters = append(req.Filters, &ec2.Filter{ + Name: aws.String("service-type"), + Values: aws.StringSlice([]string{v.(string)}), + }) + } + if tagsOk { req.Filters = append(req.Filters, ec2TagFiltersFromMap(tags.(map[string]interface{}))...) } @@ -134,29 +144,11 @@ func dataSourceAwsVpcEndpointServiceRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("no matching VPC Endpoint Service found") } - var serviceDetails []*ec2.ServiceDetail - - // Client-side filtering. When the EC2 API supports this functionality - // server-side it should be moved. - for _, serviceDetail := range resp.ServiceDetails { - if serviceDetail == nil { - continue - } - - if v, ok := d.GetOk("service_type"); ok { - if len(serviceDetail.ServiceType) > 0 && serviceDetail.ServiceType[0] != nil && v.(string) != aws.StringValue(serviceDetail.ServiceType[0].ServiceType) { - continue - } - } - - serviceDetails = append(serviceDetails, serviceDetail) - } - - if len(serviceDetails) > 1 { + if len(resp.ServiceDetails) > 1 { return fmt.Errorf("multiple VPC Endpoint Services matched; use additional constraints to reduce matches to a single VPC Endpoint Service") } - sd := serviceDetails[0] + sd := resp.ServiceDetails[0] serviceId := aws.StringValue(sd.ServiceId) serviceName = aws.StringValue(sd.ServiceName) @@ -164,7 +156,7 @@ func dataSourceAwsVpcEndpointServiceRead(d *schema.ResourceData, meta interface{ arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, AccountID: meta.(*AWSClient).accountid, Resource: fmt.Sprintf("vpc-endpoint-service/%s", serviceId), diff --git a/aws/data_source_aws_vpc_endpoint_service_test.go b/aws/data_source_aws_vpc_endpoint_service_test.go index 755edaba5198..ee05a1a11d7c 100644 --- a/aws/data_source_aws_vpc_endpoint_service_test.go +++ b/aws/data_source_aws_vpc_endpoint_service_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -13,8 +14,9 @@ func TestAccDataSourceAwsVpcEndpointService_gateway(t *testing.T) { datasourceName := "data.aws_vpc_endpoint_service.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceGatewayConfig, @@ -40,8 +42,9 @@ func TestAccDataSourceAwsVpcEndpointService_interface(t *testing.T) { datasourceName := "data.aws_vpc_endpoint_service.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceInterfaceConfig, @@ -67,8 +70,9 @@ func TestAccDataSourceAwsVpcEndpointService_custom(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceCustomConfig(rName), @@ -93,8 +97,9 @@ func TestAccDataSourceAwsVpcEndpointService_custom_filter(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceCustomConfigFilter(rName), @@ -119,8 +124,9 @@ func TestAccDataSourceAwsVpcEndpointService_custom_filter_tags(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceCustomConfigFilterTags(rName), @@ -144,8 +150,9 @@ func TestAccDataSourceAwsVpcEndpointService_ServiceType_Gateway(t *testing.T) { datasourceName := "data.aws_vpc_endpoint_service.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceConfig_ServiceType("s3", "Gateway"), @@ -162,8 +169,9 @@ func TestAccDataSourceAwsVpcEndpointService_ServiceType_Interface(t *testing.T) datasourceName := "data.aws_vpc_endpoint_service.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointServiceConfig_ServiceType("ec2", "Interface"), diff --git a/aws/data_source_aws_vpc_endpoint_test.go b/aws/data_source_aws_vpc_endpoint_test.go index bc6c7827a360..2d8ec584f786 100644 --- a/aws/data_source_aws_vpc_endpoint_test.go +++ b/aws/data_source_aws_vpc_endpoint_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -14,8 +15,9 @@ func TestAccDataSourceAwsVpcEndpoint_gatewayBasic(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_gatewayBasic(rName), @@ -43,8 +45,9 @@ func TestAccDataSourceAwsVpcEndpoint_byId(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_byId(rName), @@ -72,8 +75,9 @@ func TestAccDataSourceAwsVpcEndpoint_byFilter(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_byFilter(rName), @@ -101,8 +105,9 @@ func TestAccDataSourceAwsVpcEndpoint_byTags(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_byTags(rName), @@ -130,8 +135,9 @@ func TestAccDataSourceAwsVpcEndpoint_gatewayWithRouteTableAndTags(t *testing.T) rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_gatewayWithRouteTableAndTags(rName), @@ -160,8 +166,9 @@ func TestAccDataSourceAwsVpcEndpoint_interface(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcEndpointConfig_interface(rName), diff --git a/aws/data_source_aws_vpc_peering_connection_test.go b/aws/data_source_aws_vpc_peering_connection_test.go index ab4bff7c2fd5..b8f387ba6383 100644 --- a/aws/data_source_aws_vpc_peering_connection_test.go +++ b/aws/data_source_aws_vpc_peering_connection_test.go @@ -3,6 +3,7 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -12,8 +13,9 @@ func TestAccDataSourceAwsVpcPeeringConnection_CidrBlock(t *testing.T) { requesterVpcResourceName := "aws_vpc.requester" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionConfigCidrBlock(), @@ -33,8 +35,9 @@ func TestAccDataSourceAwsVpcPeeringConnection_Id(t *testing.T) { requesterVpcResourceName := "aws_vpc.requester" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionConfigId(), @@ -70,8 +73,9 @@ func TestAccDataSourceAwsVpcPeeringConnection_PeerCidrBlock(t *testing.T) { accepterVpcResourceName := "aws_vpc.accepter" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionConfigPeerCidrBlock(), @@ -89,8 +93,9 @@ func TestAccDataSourceAwsVpcPeeringConnection_PeerVpcId(t *testing.T) { resourceName := "aws_vpc_peering_connection.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionConfigPeerVpcId(), @@ -108,8 +113,9 @@ func TestAccDataSourceAwsVpcPeeringConnection_VpcId(t *testing.T) { resourceName := "aws_vpc_peering_connection.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionConfigVpcId(), diff --git a/aws/data_source_aws_vpc_peering_connections_test.go b/aws/data_source_aws_vpc_peering_connections_test.go index 6fe61df74892..51fc55ca0628 100644 --- a/aws/data_source_aws_vpc_peering_connections_test.go +++ b/aws/data_source_aws_vpc_peering_connections_test.go @@ -3,13 +3,15 @@ package aws import ( "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsVpcPeeringConnections_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcPeeringConnectionsConfig, diff --git a/aws/data_source_aws_vpc_test.go b/aws/data_source_aws_vpc_test.go index cce9e7b0254d..019945f7a483 100644 --- a/aws/data_source_aws_vpc_test.go +++ b/aws/data_source_aws_vpc_test.go @@ -6,13 +6,14 @@ import ( "testing" "time" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccDataSourceAwsVpc_basic(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) - rInt := rand.Intn(16) - cidr := fmt.Sprintf("172.%d.0.0/16", rInt) + rInt := rand.Intn(254) + cidr := fmt.Sprintf("10.%d.0.0/16", rInt+1) // Prevent common 10.0.0.0/16 cidr_block matches tag := fmt.Sprintf("terraform-testacc-vpc-data-source-basic-%d", rInt) vpcResourceName := "aws_vpc.test" @@ -22,8 +23,9 @@ func TestAccDataSourceAwsVpc_basic(t *testing.T) { ds4ResourceName := "data.aws_vpc.by_filter" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcConfig(cidr, tag), @@ -79,16 +81,17 @@ func TestAccDataSourceAwsVpc_basic(t *testing.T) { func TestAccDataSourceAwsVpc_ipv6Associated(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) - rInt := rand.Intn(16) - cidr := fmt.Sprintf("172.%d.0.0/16", rInt) + rInt := rand.Intn(255) + cidr := fmt.Sprintf("10.%d.0.0/16", rInt) tag := fmt.Sprintf("terraform-testacc-vpc-data-source-ipv6-associated-%d", rInt) vpcResourceName := "aws_vpc.test" ds1ResourceName := "data.aws_vpc.by_id" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcConfigIpv6(cidr, tag), @@ -111,19 +114,19 @@ func TestAccDataSourceAwsVpc_ipv6Associated(t *testing.T) { }) } -func TestAccDataSourceAwsVpc_multipleCidr(t *testing.T) { - rInt := rand.Intn(16) - rName := "data.aws_vpc.test" +func TestAccDataSourceAwsVpc_CidrBlockAssociations_Multiple(t *testing.T) { + dataSourceName := "data.aws_vpc.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckVpcDestroy, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsVpcConfigMultipleCidr(rInt), + Config: testAccDataSourceAwsVpcConfigCidrBlockAssociationsMultiple(), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(rName, "cidr_block_associations.#", "2"), + resource.TestCheckResourceAttr(dataSourceName, "cidr_block_associations.#", "2"), ), }, }, @@ -173,29 +176,26 @@ data "aws_vpc" "by_tag" { data "aws_vpc" "by_filter" { filter { - name = "cidr" - values = [aws_vpc.test.cidr_block] + name = "vpc-id" + values = [aws_vpc.test.id] } } `, cidr, tag) } -func testAccDataSourceAwsVpcConfigMultipleCidr(octet int) string { - return fmt.Sprintf(` +func testAccDataSourceAwsVpcConfigCidrBlockAssociationsMultiple() string { + return ` resource "aws_vpc" "test" { - cidr_block = "10.%d.0.0/16" + cidr_block = "10.0.0.0/16" } resource "aws_vpc_ipv4_cidr_block_association" "test" { vpc_id = aws_vpc.test.id - cidr_block = "172.%d.0.0/16" + cidr_block = "172.0.0.0/16" } data "aws_vpc" "test" { - filter { - name = "cidr-block-association.cidr-block" - values = [aws_vpc_ipv4_cidr_block_association.test.cidr_block] - } + id = aws_vpc_ipv4_cidr_block_association.test.vpc_id } -`, octet, octet) +` } diff --git a/aws/data_source_aws_vpcs_test.go b/aws/data_source_aws_vpcs_test.go index 85658fca81f0..97eedab56b64 100644 --- a/aws/data_source_aws_vpcs_test.go +++ b/aws/data_source_aws_vpcs_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "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" @@ -11,8 +12,9 @@ import ( func TestAccDataSourceAwsVpcs_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcsConfig(), @@ -27,8 +29,9 @@ func TestAccDataSourceAwsVpcs_basic(t *testing.T) { func TestAccDataSourceAwsVpcs_tags(t *testing.T) { rName := acctest.RandString(5) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcsConfig_tags(rName), @@ -44,8 +47,9 @@ func TestAccDataSourceAwsVpcs_tags(t *testing.T) { func TestAccDataSourceAwsVpcs_filters(t *testing.T) { rName := acctest.RandString(5) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpcsConfig_filters(rName), diff --git a/aws/data_source_aws_vpn_gateway.go b/aws/data_source_aws_vpn_gateway.go index f7f13016454d..72146f520e93 100644 --- a/aws/data_source_aws_vpn_gateway.go +++ b/aws/data_source_aws_vpn_gateway.go @@ -126,7 +126,7 @@ func dataSourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error arn := arn.ARN{ Partition: meta.(*AWSClient).partition, - Service: "ec2", + Service: ec2.ServiceName, Region: meta.(*AWSClient).region, AccountID: meta.(*AWSClient).accountid, Resource: fmt.Sprintf("vpn-gateway/%s", d.Id()), diff --git a/aws/data_source_aws_vpn_gateway_test.go b/aws/data_source_aws_vpn_gateway_test.go index 6c8c1f752e77..738b2ce31ab1 100644 --- a/aws/data_source_aws_vpn_gateway_test.go +++ b/aws/data_source_aws_vpn_gateway_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -17,8 +18,9 @@ func TestAccDataSourceAwsVpnGateway_unattached(t *testing.T) { resourceName := "aws_vpn_gateway.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpnGatewayUnattachedConfig(rInt), @@ -42,8 +44,9 @@ func TestAccDataSourceAwsVpnGateway_attached(t *testing.T) { dataSourceName := "data.aws_vpn_gateway.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsVpnGatewayAttachedConfig(rInt), diff --git a/aws/data_source_aws_waf_ipset_test.go b/aws/data_source_aws_waf_ipset_test.go index 813acb19318c..9a7fefb45969 100644 --- a/aws/data_source_aws_waf_ipset_test.go +++ b/aws/data_source_aws_waf_ipset_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafIPSet_basic(t *testing.T) { datasourceName := "data.aws_waf_ipset.ipset" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, waf.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafIPSet_NonExistent, diff --git a/aws/data_source_aws_waf_rate_based_rule_test.go b/aws/data_source_aws_waf_rate_based_rule_test.go index bf86b5c14094..3cfa56c67454 100644 --- a/aws/data_source_aws_waf_rate_based_rule_test.go +++ b/aws/data_source_aws_waf_rate_based_rule_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRateBasedRule_basic(t *testing.T) { datasourceName := "data.aws_waf_rate_based_rule.wafrule" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, waf.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRateBasedRuleConfig_NonExistent, diff --git a/aws/data_source_aws_waf_rule_test.go b/aws/data_source_aws_waf_rule_test.go index 5d28f6f420d0..3dfe62431f75 100644 --- a/aws/data_source_aws_waf_rule_test.go +++ b/aws/data_source_aws_waf_rule_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRule_basic(t *testing.T) { datasourceName := "data.aws_waf_rule.wafrule" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, waf.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRuleConfig_NonExistent, diff --git a/aws/data_source_aws_waf_web_acl_test.go b/aws/data_source_aws_waf_web_acl_test.go index 604fb14019c5..ca5204767835 100644 --- a/aws/data_source_aws_waf_web_acl_test.go +++ b/aws/data_source_aws_waf_web_acl_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafWebAcl_basic(t *testing.T) { datasourceName := "data.aws_waf_web_acl.web_acl" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(waf.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, waf.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafWebAclConfig_NonExistent, diff --git a/aws/data_source_aws_wafregional_ipset_test.go b/aws/data_source_aws_wafregional_ipset_test.go index e4ab2fec9681..443b3015e7a9 100644 --- a/aws/data_source_aws_wafregional_ipset_test.go +++ b/aws/data_source_aws_wafregional_ipset_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRegionalIPSet_basic(t *testing.T) { datasourceName := "data.aws_wafregional_ipset.ipset" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, wafregional.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRegionalIPSet_NonExistent, diff --git a/aws/data_source_aws_wafregional_rate_based_rule_test.go b/aws/data_source_aws_wafregional_rate_based_rule_test.go index 515881661934..f83d0425f305 100644 --- a/aws/data_source_aws_wafregional_rate_based_rule_test.go +++ b/aws/data_source_aws_wafregional_rate_based_rule_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRegionalRateBasedRule_basic(t *testing.T) { datasourceName := "data.aws_wafregional_rate_based_rule.wafrule" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, wafregional.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRegionalRateBasedRuleConfig_NonExistent, diff --git a/aws/data_source_aws_wafregional_rule_test.go b/aws/data_source_aws_wafregional_rule_test.go index 1563833169cb..ad330a337370 100644 --- a/aws/data_source_aws_wafregional_rule_test.go +++ b/aws/data_source_aws_wafregional_rule_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRegionalRule_basic(t *testing.T) { datasourceName := "data.aws_wafregional_rule.wafrule" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, wafregional.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRegionalRuleConfig_NonExistent, diff --git a/aws/data_source_aws_wafregional_web_acl_test.go b/aws/data_source_aws_wafregional_web_acl_test.go index 2b3a874a967d..2e22a4e1d4fd 100644 --- a/aws/data_source_aws_wafregional_web_acl_test.go +++ b/aws/data_source_aws_wafregional_web_acl_test.go @@ -16,8 +16,9 @@ func TestAccDataSourceAwsWafRegionalWebAcl_basic(t *testing.T) { datasourceName := "data.aws_wafregional_web_acl.web_acl" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(wafregional.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, wafregional.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafRegionalWebAclConfig_NonExistent, diff --git a/aws/data_source_aws_wafv2_ip_set.go b/aws/data_source_aws_wafv2_ip_set.go index 4f65b23e33a6..3c5700543575 100644 --- a/aws/data_source_aws_wafv2_ip_set.go +++ b/aws/data_source_aws_wafv2_ip_set.go @@ -99,9 +99,9 @@ func dataSourceAwsWafv2IPSetRead(d *schema.ResourceData, meta interface{}) error } d.SetId(aws.StringValue(resp.IPSet.Id)) - d.Set("arn", aws.StringValue(resp.IPSet.ARN)) - d.Set("description", aws.StringValue(resp.IPSet.Description)) - d.Set("ip_address_version", aws.StringValue(resp.IPSet.IPAddressVersion)) + d.Set("arn", resp.IPSet.ARN) + d.Set("description", resp.IPSet.Description) + d.Set("ip_address_version", resp.IPSet.IPAddressVersion) if err := d.Set("addresses", flattenStringList(resp.IPSet.Addresses)); err != nil { return fmt.Errorf("error setting addresses: %w", err) diff --git a/aws/data_source_aws_wafv2_ip_set_test.go b/aws/data_source_aws_wafv2_ip_set_test.go index 593de0250a8d..5078f954865c 100644 --- a/aws/data_source_aws_wafv2_ip_set_test.go +++ b/aws/data_source_aws_wafv2_ip_set_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/wafv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsWafv2IPSet_basic(t *testing.T) { datasourceName := "data.aws_wafv2_ip_set.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, + ErrorCheck: testAccErrorCheck(t, wafv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafv2IPSet_NonExistent(name), diff --git a/aws/data_source_aws_wafv2_regex_pattern_set.go b/aws/data_source_aws_wafv2_regex_pattern_set.go index d72020f1571c..c40e96cacb2b 100644 --- a/aws/data_source_aws_wafv2_regex_pattern_set.go +++ b/aws/data_source_aws_wafv2_regex_pattern_set.go @@ -102,8 +102,8 @@ func dataSourceAwsWafv2RegexPatternSetRead(d *schema.ResourceData, meta interfac } d.SetId(aws.StringValue(resp.RegexPatternSet.Id)) - d.Set("arn", aws.StringValue(resp.RegexPatternSet.ARN)) - d.Set("description", aws.StringValue(resp.RegexPatternSet.Description)) + d.Set("arn", resp.RegexPatternSet.ARN) + d.Set("description", resp.RegexPatternSet.Description) if err := d.Set("regular_expression", flattenWafv2RegexPatternSet(resp.RegexPatternSet.RegularExpressionList)); err != nil { return fmt.Errorf("Error setting regular_expression: %w", err) diff --git a/aws/data_source_aws_wafv2_regex_pattern_set_test.go b/aws/data_source_aws_wafv2_regex_pattern_set_test.go index 89f7ed917ca4..a1a4c9621247 100644 --- a/aws/data_source_aws_wafv2_regex_pattern_set_test.go +++ b/aws/data_source_aws_wafv2_regex_pattern_set_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/wafv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsWafv2RegexPatternSet_basic(t *testing.T) { datasourceName := "data.aws_wafv2_regex_pattern_set.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, + ErrorCheck: testAccErrorCheck(t, wafv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafv2RegexPatternSet_NonExistent(name), @@ -30,7 +32,7 @@ func TestAccDataSourceAwsWafv2RegexPatternSet_basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), resource.TestCheckResourceAttrPair(datasourceName, "id", resourceName, "id"), resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), - resource.TestCheckResourceAttrPair(datasourceName, "regular_expression_list", resourceName, "regular_expression_list"), + resource.TestCheckResourceAttrPair(datasourceName, "regular_expression", resourceName, "regular_expression"), resource.TestCheckResourceAttrPair(datasourceName, "scope", resourceName, "scope"), ), }, diff --git a/aws/data_source_aws_wafv2_rule_group.go b/aws/data_source_aws_wafv2_rule_group.go index 8f8f96adf323..fe49ade86f28 100644 --- a/aws/data_source_aws_wafv2_rule_group.go +++ b/aws/data_source_aws_wafv2_rule_group.go @@ -76,8 +76,8 @@ func dataSourceAwsWafv2RuleGroupRead(d *schema.ResourceData, meta interface{}) e } d.SetId(aws.StringValue(foundRuleGroup.Id)) - d.Set("arn", aws.StringValue(foundRuleGroup.ARN)) - d.Set("description", aws.StringValue(foundRuleGroup.Description)) + d.Set("arn", foundRuleGroup.ARN) + d.Set("description", foundRuleGroup.Description) return nil } diff --git a/aws/data_source_aws_wafv2_rule_group_test.go b/aws/data_source_aws_wafv2_rule_group_test.go index 1a3756ca020c..afcac7e1a687 100644 --- a/aws/data_source_aws_wafv2_rule_group_test.go +++ b/aws/data_source_aws_wafv2_rule_group_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/wafv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsWafv2RuleGroup_basic(t *testing.T) { datasourceName := "data.aws_wafv2_rule_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, + ErrorCheck: testAccErrorCheck(t, wafv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafv2RuleGroup_NonExistent(name), diff --git a/aws/data_source_aws_wafv2_web_acl.go b/aws/data_source_aws_wafv2_web_acl.go index 858eb6f9daef..01a6a8e0690f 100644 --- a/aws/data_source_aws_wafv2_web_acl.go +++ b/aws/data_source_aws_wafv2_web_acl.go @@ -76,8 +76,8 @@ func dataSourceAwsWafv2WebACLRead(d *schema.ResourceData, meta interface{}) erro } d.SetId(aws.StringValue(foundWebACL.Id)) - d.Set("arn", aws.StringValue(foundWebACL.ARN)) - d.Set("description", aws.StringValue(foundWebACL.Description)) + d.Set("arn", foundWebACL.ARN) + d.Set("description", foundWebACL.Description) return nil } diff --git a/aws/data_source_aws_wafv2_web_acl_test.go b/aws/data_source_aws_wafv2_web_acl_test.go index 90f3a74843fd..adcb45528c0d 100644 --- a/aws/data_source_aws_wafv2_web_acl_test.go +++ b/aws/data_source_aws_wafv2_web_acl_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/wafv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -15,8 +16,9 @@ func TestAccDataSourceAwsWafv2WebACL_basic(t *testing.T) { datasourceName := "data.aws_wafv2_web_acl.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSWafv2ScopeRegional(t) }, + ErrorCheck: testAccErrorCheck(t, wafv2.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWafv2WebACL_NonExistent(name), diff --git a/aws/data_source_aws_workspaces_bundle.go b/aws/data_source_aws_workspaces_bundle.go index 75fffe8fcd19..4278ceaf4849 100644 --- a/aws/data_source_aws_workspaces_bundle.go +++ b/aws/data_source_aws_workspaces_bundle.go @@ -124,10 +124,10 @@ func dataSourceAwsWorkspaceBundleRead(d *schema.ResourceData, meta interface{}) } d.SetId(aws.StringValue(bundle.BundleId)) - d.Set("bundle_id", aws.StringValue(bundle.BundleId)) - d.Set("description", aws.StringValue(bundle.Description)) - d.Set("name", aws.StringValue(bundle.Name)) - d.Set("owner", aws.StringValue(bundle.Owner)) + d.Set("bundle_id", bundle.BundleId) + d.Set("description", bundle.Description) + d.Set("name", bundle.Name) + d.Set("owner", bundle.Owner) computeType := make([]map[string]interface{}, 1) if bundle.ComputeType != nil { diff --git a/aws/data_source_aws_workspaces_bundle_test.go b/aws/data_source_aws_workspaces_bundle_test.go index 8fd09e92b27b..ec5e0d805a10 100644 --- a/aws/data_source_aws_workspaces_bundle_test.go +++ b/aws/data_source_aws_workspaces_bundle_test.go @@ -6,15 +6,17 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/workspaces" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccDataSourceAwsWorkspaceBundle_basic(t *testing.T) { +func testAccDataSourceAwsWorkspaceBundle_basic(t *testing.T) { dataSourceName := "data.aws_workspaces_bundle.test" - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspaceBundleConfig("wsb-b0s22j3d7"), @@ -35,12 +37,13 @@ func TestAccDataSourceAwsWorkspaceBundle_basic(t *testing.T) { }) } -func TestAccDataSourceAwsWorkspaceBundle_byOwnerName(t *testing.T) { +func testAccDataSourceAwsWorkspaceBundle_byOwnerName(t *testing.T) { dataSourceName := "data.aws_workspaces_bundle.test" - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspaceBundleConfig_byOwnerName("AMAZON", "Value with Windows 10 and Office 2016"), @@ -61,10 +64,11 @@ func TestAccDataSourceAwsWorkspaceBundle_byOwnerName(t *testing.T) { }) } -func TestAccDataSourceAwsWorkspaceBundle_bundleIDAndNameConflict(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, +func testAccDataSourceAwsWorkspaceBundle_bundleIDAndNameConflict(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspaceBundleConfig_bundleIDAndOwnerNameConflict("wsb-df76rqys9", "AMAZON", "Value with Windows 10 and Office 2016"), @@ -74,16 +78,17 @@ func TestAccDataSourceAwsWorkspaceBundle_bundleIDAndNameConflict(t *testing.T) { }) } -func TestAccDataSourceAwsWorkspaceBundle_privateOwner(t *testing.T) { +func testAccDataSourceAwsWorkspaceBundle_privateOwner(t *testing.T) { dataSourceName := "data.aws_workspaces_bundle.test" bundleName := os.Getenv("AWS_WORKSPACES_BUNDLE_NAME") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccWorkspacesBundlePreCheck(t) }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspaceBundleConfig_privateOwner(bundleName), diff --git a/aws/data_source_aws_workspaces_directory.go b/aws/data_source_aws_workspaces_directory.go index 548235d38c1b..cdd21a497af5 100644 --- a/aws/data_source_aws_workspaces_directory.go +++ b/aws/data_source_aws_workspaces_directory.go @@ -103,6 +103,10 @@ func dataSourceAwsWorkspacesDirectory() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "device_type_linux": { + Type: schema.TypeString, + Computed: true, + }, "device_type_osx": { Type: schema.TypeString, Computed: true, diff --git a/aws/data_source_aws_workspaces_directory_test.go b/aws/data_source_aws_workspaces_directory_test.go index 68eb7e95d12e..8596c7f6c5f6 100644 --- a/aws/data_source_aws_workspaces_directory_test.go +++ b/aws/data_source_aws_workspaces_directory_test.go @@ -4,27 +4,30 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/workspaces" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccDataSourceAwsWorkspacesDirectory_basic(t *testing.T) { +func testAccDataSourceAwsWorkspacesDirectory_basic(t *testing.T) { rName := acctest.RandString(8) + domain := testAccRandomDomainName() resourceName := "aws_workspaces_directory.test" dataSourceName := "data.aws_workspaces_directory.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccPreCheckWorkspacesDirectory(t) testAccPreCheckAWSDirectoryServiceSimpleDirectory(t) testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceAwsWorkspacesDirectoryConfig(rName), + Config: testAccDataSourceAwsWorkspacesDirectoryConfig(rName, domain), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "alias", resourceName, "alias"), resource.TestCheckResourceAttrPair(dataSourceName, "directory_id", resourceName, "directory_id"), @@ -44,6 +47,7 @@ func TestAccDataSourceAwsWorkspacesDirectory_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_android", resourceName, "workspace_access_properties.0.device_type_android"), resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_chromeos", resourceName, "workspace_access_properties.0.device_type_chromeos"), resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_ios", resourceName, "workspace_access_properties.0.device_type_ios"), + resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_linux", resourceName, "workspace_access_properties.0.device_type_linux"), resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_osx", resourceName, "workspace_access_properties.0.device_type_osx"), resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_web", resourceName, "workspace_access_properties.0.device_type_web"), resource.TestCheckResourceAttrPair(dataSourceName, "workspace_access_properties.0.device_type_windows", resourceName, "workspace_access_properties.0.device_type_windows"), @@ -63,9 +67,9 @@ func TestAccDataSourceAwsWorkspacesDirectory_basic(t *testing.T) { }) } -func testAccDataSourceAwsWorkspacesDirectoryConfig(rName string) string { +func testAccDataSourceAwsWorkspacesDirectoryConfig(rName, domain string) string { return composeConfig( - testAccAwsWorkspacesDirectoryConfig_Prerequisites(rName), + testAccAwsWorkspacesDirectoryConfig_Prerequisites(rName, domain), fmt.Sprintf(` resource "aws_security_group" "test" { name = "tf-testacc-workspaces-directory-%[1]s" @@ -91,6 +95,7 @@ resource "aws_workspaces_directory" "test" { device_type_android = "ALLOW" device_type_chromeos = "ALLOW" device_type_ios = "ALLOW" + device_type_linux = "DENY" device_type_osx = "ALLOW" device_type_web = "DENY" device_type_windows = "DENY" diff --git a/aws/data_source_aws_workspaces_image_test.go b/aws/data_source_aws_workspaces_image_test.go index a3678870d949..9834e2008b98 100644 --- a/aws/data_source_aws_workspaces_image_test.go +++ b/aws/data_source_aws_workspaces_image_test.go @@ -11,17 +11,18 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccDataSourceAwsWorkspacesImage_basic(t *testing.T) { +func testAccDataSourceAwsWorkspacesImage_basic(t *testing.T) { var image workspaces.WorkspaceImage imageID := os.Getenv("AWS_WORKSPACES_IMAGE_ID") dataSourceName := "data.aws_workspaces_image.test" - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccWorkspacesImagePreCheck(t) }, - Providers: testAccProviders, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspacesImageConfig(imageID), @@ -64,10 +65,10 @@ func testAccCheckWorkspacesImageExists(n string, image *workspaces.WorkspaceImag if err != nil { return fmt.Errorf("Failed describe workspaces images: %w", err) } - if len(resp.Images) == 0 { + if resp == nil || len(resp.Images) == 0 || resp.Images[0] == nil { return fmt.Errorf("Workspace image %s was not found", rs.Primary.ID) } - if *resp.Images[0].ImageId != rs.Primary.ID { + if aws.StringValue(resp.Images[0].ImageId) != rs.Primary.ID { return fmt.Errorf("Workspace image ID mismatch - existing: %q, state: %q", *resp.Images[0].ImageId, rs.Primary.ID) } diff --git a/aws/data_source_aws_workspaces_test.go b/aws/data_source_aws_workspaces_test.go new file mode 100644 index 000000000000..7429be9e9ee8 --- /dev/null +++ b/aws/data_source_aws_workspaces_test.go @@ -0,0 +1,39 @@ +package aws + +import ( + "testing" +) + +func TestAccDataSourceAwsWorkspaces_serial(t *testing.T) { + testCases := map[string]map[string]func(t *testing.T){ + "Bundle": { + "basic": testAccDataSourceAwsWorkspaceBundle_basic, + "bundleIDAndNameConflict": testAccDataSourceAwsWorkspaceBundle_bundleIDAndNameConflict, + "byOwnerName": testAccDataSourceAwsWorkspaceBundle_byOwnerName, + "privateOwner": testAccDataSourceAwsWorkspaceBundle_privateOwner, + }, + "Directory": { + "basic": testAccDataSourceAwsWorkspacesDirectory_basic, + }, + "Image": { + "basic": testAccDataSourceAwsWorkspacesImage_basic, + }, + "Workspace": { + "byWorkspaceID": testAccDataSourceAwsWorkspacesWorkspace_byWorkspaceID, + "byDirectoryID_userName": testAccDataSourceAwsWorkspacesWorkspace_byDirectoryID_userName, + "workspaceIDAndDirectoryIDConflict": testAccDataSourceAwsWorkspacesWorkspace_workspaceIDAndDirectoryIDConflict, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } + }) + } +} diff --git a/aws/data_source_aws_workspaces_workspace.go b/aws/data_source_aws_workspaces_workspace.go index 34c52a4f0d43..7ab19466a44c 100644 --- a/aws/data_source_aws_workspaces_workspace.go +++ b/aws/data_source_aws_workspaces_workspace.go @@ -142,15 +142,15 @@ func dataSourceAwsWorkspacesWorkspaceRead(d *schema.ResourceData, meta interface } d.SetId(aws.StringValue(workspace.WorkspaceId)) - d.Set("bundle_id", aws.StringValue(workspace.BundleId)) - d.Set("directory_id", aws.StringValue(workspace.DirectoryId)) - d.Set("ip_address", aws.StringValue(workspace.IpAddress)) - d.Set("computer_name", aws.StringValue(workspace.ComputerName)) - d.Set("state", aws.StringValue(workspace.State)) - d.Set("root_volume_encryption_enabled", aws.BoolValue(workspace.RootVolumeEncryptionEnabled)) - d.Set("user_name", aws.StringValue(workspace.UserName)) - d.Set("user_volume_encryption_enabled", aws.BoolValue(workspace.UserVolumeEncryptionEnabled)) - d.Set("volume_encryption_key", aws.StringValue(workspace.VolumeEncryptionKey)) + d.Set("bundle_id", workspace.BundleId) + d.Set("directory_id", workspace.DirectoryId) + d.Set("ip_address", workspace.IpAddress) + d.Set("computer_name", workspace.ComputerName) + d.Set("state", workspace.State) + d.Set("root_volume_encryption_enabled", workspace.RootVolumeEncryptionEnabled) + d.Set("user_name", workspace.UserName) + d.Set("user_volume_encryption_enabled", workspace.UserVolumeEncryptionEnabled) + d.Set("volume_encryption_key", workspace.VolumeEncryptionKey) if err := d.Set("workspace_properties", flattenWorkspaceProperties(workspace.WorkspaceProperties)); err != nil { return fmt.Errorf("error setting workspace properties: %w", err) } diff --git a/aws/data_source_aws_workspaces_workspace_test.go b/aws/data_source_aws_workspaces_workspace_test.go index f60a85e8b18c..53c3e855cf84 100644 --- a/aws/data_source_aws_workspaces_workspace_test.go +++ b/aws/data_source_aws_workspaces_workspace_test.go @@ -4,21 +4,25 @@ import ( "regexp" "testing" + "github.com/aws/aws-sdk-go/service/workspaces" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccDataSourceAwsWorkspacesWorkspace_byWorkspaceID(t *testing.T) { +func testAccDataSourceAwsWorkspacesWorkspace_byWorkspaceID(t *testing.T) { rName := acctest.RandString(8) + domain := testAccRandomDomainName() + dataSourceName := "data.aws_workspaces_workspace.test" resourceName := "aws_workspaces_workspace.test" - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, - Providers: testAccProviders, + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceWorkspacesWorkspaceConfig_byWorkspaceID(rName), + Config: testAccDataSourceWorkspacesWorkspaceConfig_byWorkspaceID(rName, domain), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "directory_id", resourceName, "directory_id"), resource.TestCheckResourceAttrPair(dataSourceName, "bundle_id", resourceName, "bundle_id"), @@ -40,17 +44,20 @@ func TestAccDataSourceAwsWorkspacesWorkspace_byWorkspaceID(t *testing.T) { }) } -func TestAccDataSourceAwsWorkspacesWorkspace_byDirectoryID_userName(t *testing.T) { +func testAccDataSourceAwsWorkspacesWorkspace_byDirectoryID_userName(t *testing.T) { rName := acctest.RandString(8) + domain := testAccRandomDomainName() + dataSourceName := "data.aws_workspaces_workspace.test" resourceName := "aws_workspaces_workspace.test" - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, - Providers: testAccProviders, + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccDataSourceWorkspacesWorkspaceConfig_byDirectoryID_userName(rName), + Config: testAccDataSourceWorkspacesWorkspaceConfig_byDirectoryID_userName(rName, domain), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "directory_id", resourceName, "directory_id"), resource.TestCheckResourceAttrPair(dataSourceName, "bundle_id", resourceName, "bundle_id"), @@ -72,10 +79,11 @@ func TestAccDataSourceAwsWorkspacesWorkspace_byDirectoryID_userName(t *testing.T }) } -func TestAccDataSourceAwsWorkspacesWorkspace_workspaceIDAndDirectoryIDConflict(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, - Providers: testAccProviders, +func testAccDataSourceAwsWorkspacesWorkspace_workspaceIDAndDirectoryIDConflict(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckHasIAMRole(t, "workspaces_DefaultRole") }, + ErrorCheck: testAccErrorCheck(t, workspaces.EndpointsID), + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testAccDataSourceAwsWorkspacesWorkspaceConfig_workspaceIDAndDirectoryIDConflict(), @@ -85,9 +93,9 @@ func TestAccDataSourceAwsWorkspacesWorkspace_workspaceIDAndDirectoryIDConflict(t }) } -func testAccDataSourceWorkspacesWorkspaceConfig_byWorkspaceID(rName string) string { +func testAccDataSourceWorkspacesWorkspaceConfig_byWorkspaceID(rName, domain string) string { return composeConfig( - testAccAwsWorkspacesWorkspaceConfig_Prerequisites(rName), + testAccAwsWorkspacesWorkspaceConfig_Prerequisites(rName, domain), ` resource "aws_workspaces_workspace" "test" { bundle_id = data.aws_workspaces_bundle.test.id @@ -113,9 +121,9 @@ data "aws_workspaces_workspace" "test" { `) } -func testAccDataSourceWorkspacesWorkspaceConfig_byDirectoryID_userName(rName string) string { +func testAccDataSourceWorkspacesWorkspaceConfig_byDirectoryID_userName(rName, domain string) string { return composeConfig( - testAccAwsWorkspacesWorkspaceConfig_Prerequisites(rName), + testAccAwsWorkspacesWorkspaceConfig_Prerequisites(rName, domain), ` resource "aws_workspaces_workspace" "test" { bundle_id = data.aws_workspaces_bundle.test.id diff --git a/aws/datasync.go b/aws/datasync.go index 75dc47d4e671..ec7e10887b4e 100644 --- a/aws/datasync.go +++ b/aws/datasync.go @@ -1,23 +1,11 @@ package aws import ( - "net/url" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/datasync" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func dataSyncParseLocationURI(uri string) (string, error) { - parsedURL, err := url.ParseRequestURI(uri) - - if err != nil { - return "", err - } - - return parsedURL.Path, nil -} - func expandDataSyncEc2Config(l []interface{}) *datasync.Ec2Config { if len(l) == 0 || l[0] == nil { return nil @@ -71,10 +59,14 @@ func expandDataSyncOptions(l []interface{}) *datasync.Options { options := &datasync.Options{ Atime: aws.String(m["atime"].(string)), Gid: aws.String(m["gid"].(string)), + LogLevel: aws.String(m["log_level"].(string)), Mtime: aws.String(m["mtime"].(string)), + OverwriteMode: aws.String(m["overwrite_mode"].(string)), PreserveDeletedFiles: aws.String(m["preserve_deleted_files"].(string)), PreserveDevices: aws.String(m["preserve_devices"].(string)), PosixPermissions: aws.String(m["posix_permissions"].(string)), + TaskQueueing: aws.String(m["task_queueing"].(string)), + TransferMode: aws.String(m["transfer_mode"].(string)), Uid: aws.String(m["uid"].(string)), VerifyMode: aws.String(m["verify_mode"].(string)), } @@ -146,10 +138,14 @@ func flattenDataSyncOptions(options *datasync.Options) []interface{} { "atime": aws.StringValue(options.Atime), "bytes_per_second": aws.Int64Value(options.BytesPerSecond), "gid": aws.StringValue(options.Gid), + "log_level": aws.StringValue(options.LogLevel), "mtime": aws.StringValue(options.Mtime), + "overwrite_mode": aws.StringValue(options.OverwriteMode), "posix_permissions": aws.StringValue(options.PosixPermissions), "preserve_deleted_files": aws.StringValue(options.PreserveDeletedFiles), "preserve_devices": aws.StringValue(options.PreserveDevices), + "task_queueing": aws.StringValue(options.TaskQueueing), + "transfer_mode": aws.StringValue(options.TransferMode), "uid": aws.StringValue(options.Uid), "verify_mode": aws.StringValue(options.VerifyMode), } diff --git a/aws/datasync_test.go b/aws/datasync_test.go deleted file mode 100644 index f56cf7484fc5..000000000000 --- a/aws/datasync_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package aws - -import ( - "testing" -) - -func TestDataSyncParseLocationURI(t *testing.T) { - testCases := []struct { - LocationURI string - Subdirectory string - }{ - { - LocationURI: "efs://us-east-2.fs-abcd1234/", // lintignore:AWSAT003 - Subdirectory: "/", - }, - { - LocationURI: "efs://us-east-2.fs-abcd1234/path", // lintignore:AWSAT003 - Subdirectory: "/path", - }, - { - LocationURI: "nfs://example.com/", - Subdirectory: "/", - }, - { - LocationURI: "nfs://example.com/path", - Subdirectory: "/path", - }, - { - LocationURI: "s3://myBucket/", - Subdirectory: "/", - }, - { - LocationURI: "s3://myBucket/path", - Subdirectory: "/path", - }, - } - - for i, tc := range testCases { - subdirectory, err := dataSyncParseLocationURI(tc.LocationURI) - if err != nil { - t.Fatalf("%d: received error parsing (%s): %s", i, tc.LocationURI, err) - } - if subdirectory != tc.Subdirectory { - t.Fatalf("%d: expected subdirectory (%s), received: %s", i, tc.Subdirectory, subdirectory) - } - } -} diff --git a/aws/diff_suppress_funcs.go b/aws/diff_suppress_funcs.go index 15e2e2e60115..fc3676bff300 100644 --- a/aws/diff_suppress_funcs.go +++ b/aws/diff_suppress_funcs.go @@ -5,10 +5,11 @@ import ( "encoding/json" "log" "net/url" - "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" awspolicy "github.com/jen20/awspolicyequivalence" + tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net" ) func suppressEquivalentAwsPolicyDiffs(k, old, new string, d *schema.ResourceData) bool { @@ -41,32 +42,6 @@ func suppressMissingOptionalConfigurationBlock(k, old, new string, d *schema.Res return old == "1" && new == "0" } -// Suppresses minor version changes to the db_instance engine_version attribute -func suppressAwsDbEngineVersionDiffs(k, old, new string, d *schema.ResourceData) bool { - // First check if the old/new values are nil. - // If both are nil, we have no state to compare the values with, so register a diff. - // This populates the attribute field during a plan/apply with fresh state, allowing - // the attribute to still be used in future resources. - // See https://github.com/hashicorp/terraform/issues/11881 - if old == "" && new == "" { - return false - } - - if v, ok := d.GetOk("auto_minor_version_upgrade"); ok { - if v.(bool) { - // If we're set to auto upgrade minor versions - // ignore a minor version diff between versions - if strings.HasPrefix(old, new) { - log.Printf("[DEBUG] Ignoring minor version diff") - return true - } - } - } - - // Throw a diff by default - return false -} - func suppressEquivalentJsonDiffs(k, old, new string, d *schema.ResourceData) bool { ob := bytes.NewBufferString("") if err := json.Compact(ob, []byte(old)); err != nil { @@ -81,6 +56,18 @@ func suppressEquivalentJsonDiffs(k, old, new string, d *schema.ResourceData) boo return jsonBytesEqual(ob.Bytes(), nb.Bytes()) } +func suppressEquivalentJsonEmptyNilDiffs(k, old, new string, d *schema.ResourceData) bool { + if old == "[]" && new == "" { + return true + } + + if old == "" && new == "[]" { + return true + } + + return suppressEquivalentJsonDiffs(k, old, new, d) +} + func suppressOpenIdURL(k, old, new string, d *schema.ResourceData) bool { oldUrl, err := url.Parse(old) if err != nil { @@ -118,5 +105,21 @@ func suppressEquivalentJsonOrYamlDiffs(k, old, new string, d *schema.ResourceDat // suppressEqualCIDRBlockDiffs provides custom difference suppression for CIDR blocks // that have different string values but represent the same CIDR. func suppressEqualCIDRBlockDiffs(k, old, new string, d *schema.ResourceData) bool { - return cidrBlocksEqual(old, new) + return tfnet.CIDRBlocksEqual(old, new) +} + +// suppressEquivalentTime suppresses differences for time values that represent the same +// instant in different timezones. +func suppressEquivalentTime(k, old, new string, d *schema.ResourceData) bool { + oldTime, err := time.Parse(time.RFC3339, old) + if err != nil { + return false + } + + newTime, err := time.Parse(time.RFC3339, new) + if err != nil { + return false + } + + return oldTime.Equal(newTime) } diff --git a/aws/dx_vif.go b/aws/dx_vif.go index b12997e5d93b..4b2aea8edbc6 100644 --- a/aws/dx_vif.go +++ b/aws/dx_vif.go @@ -41,8 +41,8 @@ func dxVirtualInterfaceUpdate(d *schema.ResourceData, meta interface{}) error { } arn := d.Get("arn").(string) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.DirectconnectUpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating Direct Connect virtual interface (%s) tags: %s", arn, err) diff --git a/aws/ec2_transit_gateway.go b/aws/ec2_transit_gateway.go index aa8bb57714c4..9bae8a12723f 100644 --- a/aws/ec2_transit_gateway.go +++ b/aws/ec2_transit_gateway.go @@ -9,6 +9,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" ) func decodeEc2TransitGatewayRouteID(id string) (string, string, error) { @@ -109,8 +111,8 @@ func ec2DescribeTransitGatewayRoute(conn *ec2.EC2, transitGatewayRouteTableID, d if route == nil { continue } - if cidrBlocksEqual(aws.StringValue(route.DestinationCidrBlock), destination) { - cidrString := canonicalCidrBlock(aws.StringValue(route.DestinationCidrBlock)) + if tfnet.CIDRBlocksEqual(aws.StringValue(route.DestinationCidrBlock), destination) { + cidrString := tfnet.CanonicalCIDRBlock(aws.StringValue(route.DestinationCidrBlock)) route.DestinationCidrBlock = aws.String(cidrString) return route, nil } @@ -184,34 +186,6 @@ func ec2DescribeTransitGatewayRouteTableAssociation(conn *ec2.EC2, transitGatewa return output.Associations[0], nil } -func ec2DescribeTransitGatewayRouteTablePropagation(conn *ec2.EC2, transitGatewayRouteTableID, transitGatewayAttachmentID string) (*ec2.TransitGatewayRouteTablePropagation, error) { - if transitGatewayRouteTableID == "" { - return nil, nil - } - - input := &ec2.GetTransitGatewayRouteTablePropagationsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("transit-gateway-attachment-id"), - Values: []*string{aws.String(transitGatewayAttachmentID)}, - }, - }, - TransitGatewayRouteTableId: aws.String(transitGatewayRouteTableID), - } - - output, err := conn.GetTransitGatewayRouteTablePropagations(input) - - if err != nil { - return nil, err - } - - if output == nil || len(output.TransitGatewayRouteTablePropagations) == 0 { - return nil, nil - } - - return output.TransitGatewayRouteTablePropagations[0], nil -} - func ec2DescribeTransitGatewayPeeringAttachment(conn *ec2.EC2, transitGatewayAttachmentID string) (*ec2.TransitGatewayPeeringAttachment, error) { input := &ec2.DescribeTransitGatewayPeeringAttachmentsInput{ TransitGatewayAttachmentIds: []*string{aws.String(transitGatewayAttachmentID)}, @@ -324,7 +298,7 @@ func ec2TransitGatewayRouteTableAssociationUpdate(conn *ec2.EC2, transitGatewayR } func ec2TransitGatewayRouteTablePropagationUpdate(conn *ec2.EC2, transitGatewayRouteTableID, transitGatewayAttachmentID string, enablePropagation bool) error { - transitGatewayRouteTablePropagation, err := ec2DescribeTransitGatewayRouteTablePropagation(conn, transitGatewayRouteTableID, transitGatewayAttachmentID) + transitGatewayRouteTablePropagation, err := finder.TransitGatewayRouteTablePropagation(conn, transitGatewayRouteTableID, transitGatewayAttachmentID) if err != nil { return fmt.Errorf("error determining EC2 Transit Gateway Attachment (%s) propagation to Route Table (%s): %s", transitGatewayAttachmentID, transitGatewayRouteTableID, err) } diff --git a/aws/elasticache_validation.go b/aws/elasticache_validation.go new file mode 100644 index 000000000000..e5acc5603101 --- /dev/null +++ b/aws/elasticache_validation.go @@ -0,0 +1,156 @@ +package aws + +import ( + "context" + "errors" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/service/elasticache" + multierror "github.com/hashicorp/go-multierror" + gversion "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache" +) + +const ( + redisVersionPreV6RegexpRaw = `[1-5](\.[[:digit:]]+){2}` + redisVersionPostV6RegexpRaw = `([6-9]|[[:digit:]]{2})\.x` + + redisVersionRegexpRaw = redisVersionPreV6RegexpRaw + "|" + redisVersionPostV6RegexpRaw +) + +const ( + redisVersionRegexpPattern = "^" + redisVersionRegexpRaw + "$" + redisVersionPostV6RegexpPattern = "^" + redisVersionPostV6RegexpRaw + "$" +) + +var ( + redisVersionRegexp = regexp.MustCompile(redisVersionRegexpPattern) + redisVersionPostV6Regexp = regexp.MustCompile(redisVersionPostV6RegexpPattern) +) + +func ValidateElastiCacheRedisVersionString(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if !redisVersionRegexp.MatchString(value) { + errors = append(errors, fmt.Errorf("%s: Redis versions must match .x when using version 6 or higher, or ..", k)) + } + + return +} + +// NormalizeElastiCacheEngineVersion returns a github.com/hashicorp/go-version Version +// that can handle a regular 1.2.3 version number or a 6.x version number used for +// ElastiCache Redis version 6 and higher +func NormalizeElastiCacheEngineVersion(version string) (*gversion.Version, error) { + if matches := redisVersionPostV6Regexp.FindStringSubmatch(version); matches != nil { + version = matches[1] + } + return gversion.NewVersion(version) +} + +// CustomizeDiffElastiCacheEngineVersion causes re-creation of the resource if the version is being downgraded +func CustomizeDiffElastiCacheEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if diff.Id() == "" || !diff.HasChange("engine_version") { + return nil + } + + o, n := diff.GetChange("engine_version") + oVersion, err := NormalizeElastiCacheEngineVersion(o.(string)) + if err != nil { + return fmt.Errorf("error parsing old engine_version: %w", err) + } + nVersion, err := NormalizeElastiCacheEngineVersion(n.(string)) + if err != nil { + return fmt.Errorf("error parsing new engine_version: %w", err) + } + + if nVersion.GreaterThan(oVersion) { + return nil + } + + return diff.ForceNew("engine_version") +} + +// CustomizeDiffValidateClusterAZMode validates that `num_cache_nodes` is greater than 1 when `az_mode` is "cross-az" +func CustomizeDiffValidateClusterAZMode(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v, ok := diff.GetOk("az_mode"); !ok || v.(string) != elasticache.AZModeCrossAz { + return nil + } + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) != 1 { + return nil + } + + return errors.New(`az_mode "cross-az" is not supported with num_cache_nodes = 1`) +} + +// CustomizeDiffValidateClusterEngineVersion validates the correct format for `engine_version`, based on `engine` +func CustomizeDiffValidateClusterEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // Memcached: Versions in format .. + // Redis: Starting with version 6, must match .x, prior to version 6, .. + engineVersion, ok := diff.GetOk("engine_version") + if !ok { + return nil + } + + var validator schema.SchemaValidateFunc + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached { + validator = validateVersionString + } else { + validator = ValidateElastiCacheRedisVersionString + } + + _, errs := validator(engineVersion, "engine_version") + + var err *multierror.Error + err = multierror.Append(err, errs...) + return err.ErrorOrNil() +} + +// CustomizeDiffValidateClusterNumCacheNodes validates that `num_cache_nodes` is 1 when `engine` is "redis" +func CustomizeDiffValidateClusterNumCacheNodes(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached { + return nil + } + + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) == 1 { + return nil + } + return errors.New(`engine "redis" does not support num_cache_nodes > 1`) +} + +// CustomizeDiffClusterMemcachedNodeType causes re-creation when `node_type` is changed and `engine` is "memcached" +func CustomizeDiffClusterMemcachedNodeType(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // Engine memcached does not currently support vertical scaling + // https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/Scaling.html#Scaling.Memcached.Vertically + if diff.Id() == "" || !diff.HasChange("node_type") { + return nil + } + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis { + return nil + } + return diff.ForceNew("node_type") +} + +// CustomizeDiffValidateClusterMemcachedSnapshotIdentifier validates that `final_snapshot_identifier` is not set when `engine` is "memcached" +func CustomizeDiffValidateClusterMemcachedSnapshotIdentifier(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis { + return nil + } + if _, ok := diff.GetOk("final_snapshot_identifier"); !ok { + return nil + } + return errors.New(`engine "memcached" does not support final_snapshot_identifier`) +} + +// CustomizeDiffValidateReplicationGroupAutomaticFailover validates that `automatic_failover_enabled` is set when `multi_az_enabled` is true +func CustomizeDiffValidateReplicationGroupAutomaticFailover(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v := diff.Get("multi_az_enabled").(bool); !v { + return nil + } + if v := diff.Get("automatic_failover_enabled").(bool); !v { + return errors.New(`automatic_failover_enabled must be true if multi_az_enabled is true`) + } + return nil +} diff --git a/aws/elasticsearch_domain_structure.go b/aws/elasticsearch_domain_structure.go index 4762b4654561..e6114eceb350 100644 --- a/aws/elasticsearch_domain_structure.go +++ b/aws/elasticsearch_domain_structure.go @@ -44,6 +44,61 @@ func expandAdvancedSecurityOptions(m []interface{}) *elasticsearch.AdvancedSecur return &config } +func expandESSAMLOptions(data []interface{}) *elasticsearch.SAMLOptionsInput { + if len(data) == 0 { + return nil + } + + if data[0] == nil { + return &elasticsearch.SAMLOptionsInput{} + } + + options := elasticsearch.SAMLOptionsInput{} + group := data[0].(map[string]interface{}) + + if SAMLEnabled, ok := group["enabled"]; ok { + options.Enabled = aws.Bool(SAMLEnabled.(bool)) + + if SAMLEnabled.(bool) { + options.Idp = expandSAMLOptionsIdp(group["idp"].([]interface{})) + if v, ok := group["master_backend_role"].(string); ok && v != "" { + options.MasterBackendRole = aws.String(v) + } + if v, ok := group["master_user_name"].(string); ok && v != "" { + options.MasterUserName = aws.String(v) + } + if v, ok := group["roles_key"].(string); ok { + options.RolesKey = aws.String(v) + } + if v, ok := group["session_timeout_minutes"].(int); ok { + options.SessionTimeoutMinutes = aws.Int64(int64(v)) + } + if v, ok := group["subject_key"].(string); ok { + options.SubjectKey = aws.String(v) + } + } + } + + return &options +} + +func expandSAMLOptionsIdp(l []interface{}) *elasticsearch.SAMLIdp { + if len(l) == 0 { + return nil + } + + if l[0] == nil { + return &elasticsearch.SAMLIdp{} + } + + m := l[0].(map[string]interface{}) + + return &elasticsearch.SAMLIdp{ + EntityId: aws.String(m["entity_id"].(string)), + MetadataContent: aws.String(m["metadata_content"].(string)), + } +} + func flattenAdvancedSecurityOptions(advancedSecurityOptions *elasticsearch.AdvancedSecurityOptions) []map[string]interface{} { if advancedSecurityOptions == nil { return []map[string]interface{}{} @@ -58,6 +113,43 @@ func flattenAdvancedSecurityOptions(advancedSecurityOptions *elasticsearch.Advan return []map[string]interface{}{m} } +func flattenESSAMLOptions(d *schema.ResourceData, samlOptions *elasticsearch.SAMLOptionsOutput) []interface{} { + if samlOptions == nil { + return nil + } + + m := map[string]interface{}{ + "enabled": aws.BoolValue(samlOptions.Enabled), + "idp": flattenESSAMLIdpOptions(samlOptions.Idp), + } + + m["roles_key"] = aws.StringValue(samlOptions.RolesKey) + m["session_timeout_minutes"] = aws.Int64Value(samlOptions.SessionTimeoutMinutes) + m["subject_key"] = aws.StringValue(samlOptions.SubjectKey) + + // samlOptions.master_backend_role and samlOptions.master_user_name will be added to the + // all_access role in kibana's security manager. These values cannot be read or + // modified by the elasticsearch API. So, we ignore it on read and let persist + // the value already in the state. + m["master_backend_role"] = d.Get("saml_options.0.master_backend_role").(string) + m["master_user_name"] = d.Get("saml_options.0.master_user_name").(string) + + return []interface{}{m} +} + +func flattenESSAMLIdpOptions(SAMLIdp *elasticsearch.SAMLIdp) []interface{} { + if SAMLIdp == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "entity_id": aws.StringValue(SAMLIdp.EntityId), + "metadata_content": aws.StringValue(SAMLIdp.MetadataContent), + } + + return []interface{}{m} +} + func getMasterUserOptions(d *schema.ResourceData) []interface{} { if v, ok := d.GetOk("advanced_security_options"); ok { options := v.([]interface{}) diff --git a/aws/fsx.go b/aws/fsx.go deleted file mode 100644 index 73afe902a41d..000000000000 --- a/aws/fsx.go +++ /dev/null @@ -1,141 +0,0 @@ -package aws - -import ( - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/fsx" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func describeFsxFileSystem(conn *fsx.FSx, id string) (*fsx.FileSystem, error) { - input := &fsx.DescribeFileSystemsInput{ - FileSystemIds: []*string{aws.String(id)}, - } - var filesystem *fsx.FileSystem - - err := conn.DescribeFileSystemsPages(input, func(page *fsx.DescribeFileSystemsOutput, lastPage bool) bool { - for _, fs := range page.FileSystems { - if aws.StringValue(fs.FileSystemId) == id { - filesystem = fs - return false - } - } - - return !lastPage - }) - - return filesystem, err -} - -func refreshFsxFileSystemLifecycle(conn *fsx.FSx, id string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - filesystem, err := describeFsxFileSystem(conn, id) - - if isAWSErr(err, fsx.ErrCodeFileSystemNotFound, "") { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - if filesystem == nil { - return nil, "", nil - } - - return filesystem, aws.StringValue(filesystem.Lifecycle), nil - } -} - -func refreshFsxFileSystemAdministrativeActionsStatusFileSystemUpdate(conn *fsx.FSx, id string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - filesystem, err := describeFsxFileSystem(conn, id) - - if isAWSErr(err, fsx.ErrCodeFileSystemNotFound, "") { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - if filesystem == nil { - return nil, "", nil - } - - for _, administrativeAction := range filesystem.AdministrativeActions { - if administrativeAction == nil { - continue - } - - if aws.StringValue(administrativeAction.AdministrativeActionType) == fsx.AdministrativeActionTypeFileSystemUpdate { - return filesystem, aws.StringValue(administrativeAction.Status), nil - } - } - - return filesystem, fsx.StatusCompleted, nil - } -} - -func waitForFsxFileSystemCreation(conn *fsx.FSx, id string, timeout time.Duration) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{fsx.FileSystemLifecycleCreating}, - Target: []string{fsx.FileSystemLifecycleAvailable}, - Refresh: refreshFsxFileSystemLifecycle(conn, id), - Timeout: timeout, - Delay: 30 * time.Second, - } - - _, err := stateConf.WaitForState() - - return err -} - -func waitForFsxFileSystemDeletion(conn *fsx.FSx, id string, timeout time.Duration) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{fsx.FileSystemLifecycleAvailable, fsx.FileSystemLifecycleDeleting}, - Target: []string{}, - Refresh: refreshFsxFileSystemLifecycle(conn, id), - Timeout: timeout, - Delay: 30 * time.Second, - } - - _, err := stateConf.WaitForState() - - return err -} - -func waitForFsxFileSystemUpdate(conn *fsx.FSx, id string, timeout time.Duration) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{fsx.FileSystemLifecycleUpdating}, - Target: []string{fsx.FileSystemLifecycleAvailable}, - Refresh: refreshFsxFileSystemLifecycle(conn, id), - Timeout: timeout, - Delay: 30 * time.Second, - } - - _, err := stateConf.WaitForState() - - return err -} - -func waitForFsxFileSystemUpdateAdministrativeActionsStatusFileSystemUpdate(conn *fsx.FSx, id string, timeout time.Duration) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - fsx.StatusInProgress, - fsx.StatusPending, - }, - Target: []string{ - fsx.StatusCompleted, - fsx.StatusUpdatedOptimizing, - }, - Refresh: refreshFsxFileSystemAdministrativeActionsStatusFileSystemUpdate(conn, id), - Timeout: timeout, - Delay: 30 * time.Second, - } - - _, err := stateConf.WaitForState() - - return err -} diff --git a/aws/hosted_zones.go b/aws/hosted_zones.go index a45ae249dcc1..722f22f747f2 100644 --- a/aws/hosted_zones.go +++ b/aws/hosted_zones.go @@ -17,7 +17,7 @@ var hostedZoneIDsMap = map[string]string{ endpoints.ApEast1RegionID: "ZNB98KWMFR0R6", endpoints.ApNortheast1RegionID: "Z2M4EHUR26P7ZW", endpoints.ApNortheast2RegionID: "Z3W03O7B5YMIYP", - "ap-northeast-3": "Z2YQB5RD63NC85", //lintignore:AWSAT003 // https://github.com/aws/aws-sdk-go/issues/1863 + endpoints.ApNortheast3RegionID: "Z2YQB5RD63NC85", endpoints.ApSouth1RegionID: "Z11RGJOFQNVJUP", endpoints.ApSoutheast1RegionID: "Z3O0J2DXBE1FTB", endpoints.ApSoutheast2RegionID: "Z1WCIGYICN2BYD", diff --git a/aws/internal/attrmap/attrmap.go b/aws/internal/attrmap/attrmap.go new file mode 100644 index 000000000000..094f0e7ff45f --- /dev/null +++ b/aws/internal/attrmap/attrmap.go @@ -0,0 +1,141 @@ +package attrmap + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// AttributeMap represents a map of Terraform resource attribute name to AWS API attribute name. +// Useful for SQS Queue or SNS Topic attribute handling. +type attributeInfo struct { + apiAttributeName string + tfType schema.ValueType + tfOptionalComputed bool +} + +type AttributeMap map[string]attributeInfo + +// New returns a new AttributeMap from the specified Terraform resource attribute name to AWS API attribute name map and resource schema. +func New(attrMap map[string]string, schemaMap map[string]*schema.Schema) AttributeMap { + attributeMap := make(AttributeMap) + + for tfAttributeName, apiAttributeName := range attrMap { + if s, ok := schemaMap[tfAttributeName]; ok { + attributeInfo := attributeInfo{ + apiAttributeName: apiAttributeName, + tfType: s.Type, + } + + if s.Optional && s.Computed { + attributeInfo.tfOptionalComputed = true + } + + attributeMap[tfAttributeName] = attributeInfo + } else { + log.Printf("[ERROR] Unknown attribute: %s", tfAttributeName) + } + } + + return attributeMap +} + +// ApiAttributesToResourceData sets Terraform ResourceData from a map of AWS API attributes. +func (m AttributeMap) ApiAttributesToResourceData(apiAttributes map[string]string, d *schema.ResourceData) error { + for tfAttributeName, attributeInfo := range m { + if v, ok := apiAttributes[attributeInfo.apiAttributeName]; ok { + var err error + var tfAttributeValue interface{} + + switch t := attributeInfo.tfType; t { + case schema.TypeBool: + tfAttributeValue, err = strconv.ParseBool(v) + + if err != nil { + return fmt.Errorf("error parsing %s value (%s) into boolean: %w", tfAttributeName, v, err) + } + case schema.TypeInt: + tfAttributeValue, err = strconv.Atoi(v) + + if err != nil { + return fmt.Errorf("error parsing %s value (%s) into integer: %w", tfAttributeName, v, err) + } + case schema.TypeString: + tfAttributeValue = v + default: + return fmt.Errorf("attribute %s is of unsupported type: %d", tfAttributeName, t) + } + + if err := d.Set(tfAttributeName, tfAttributeValue); err != nil { + return fmt.Errorf("error setting %s: %w", tfAttributeName, err) + } + } else { + d.Set(tfAttributeName, nil) + } + } + + return nil +} + +// ResourceDataToApiAttributesCreate returns a map of AWS API attributes from Terraform ResourceData. +// The API attributes map is suitable for resource create. +func (m AttributeMap) ResourceDataToApiAttributesCreate(d *schema.ResourceData) (map[string]string, error) { + apiAttributes := map[string]string{} + + for tfAttributeName, attributeInfo := range m { + var apiAttributeValue string + + switch v, t := d.Get(tfAttributeName), attributeInfo.tfType; t { + case schema.TypeBool: + if v := v.(bool); v { + apiAttributeValue = strconv.FormatBool(v) + } + case schema.TypeInt: + // On creation don't specify any zero Optional/Computed attribute integer values. + if v := v.(int); !attributeInfo.tfOptionalComputed || v != 0 { + apiAttributeValue = strconv.Itoa(v) + } + case schema.TypeString: + apiAttributeValue = v.(string) + default: + return nil, fmt.Errorf("attribute %s is of unsupported type: %d", tfAttributeName, t) + } + + if apiAttributeValue != "" { + apiAttributes[attributeInfo.apiAttributeName] = apiAttributeValue + } + } + + return apiAttributes, nil +} + +// ResourceDataToApiAttributesUpdate returns a map of AWS API attributes from Terraform ResourceData. +// The API attributes map is suitable for resource update. +func (m AttributeMap) ResourceDataToApiAttributesUpdate(d *schema.ResourceData) (map[string]string, error) { + apiAttributes := map[string]string{} + + for tfAttributeName, attributeInfo := range m { + if d.HasChange(tfAttributeName) { + v := d.Get(tfAttributeName) + + var apiAttributeValue string + + switch t := attributeInfo.tfType; t { + case schema.TypeBool: + apiAttributeValue = strconv.FormatBool(v.(bool)) + case schema.TypeInt: + apiAttributeValue = strconv.Itoa(v.(int)) + case schema.TypeString: + apiAttributeValue = v.(string) + default: + return nil, fmt.Errorf("attribute %s is of unsupported type: %d", tfAttributeName, t) + } + + apiAttributes[attributeInfo.apiAttributeName] = apiAttributeValue + } + } + + return apiAttributes, nil +} diff --git a/aws/internal/envvar/consts.go b/aws/internal/envvar/consts.go index 9cbf802ef6c6..ccc7fff6e3da 100644 --- a/aws/internal/envvar/consts.go +++ b/aws/internal/envvar/consts.go @@ -30,7 +30,7 @@ const ( AwsAlternateAccessKeyId = "AWS_ALTERNATE_ACCESS_KEY_ID" // For tests using an alternate AWS account, the equivalent of AWS_PROFILE for that account - AwsAlternateProfile = "AWS_PROFILE" + AwsAlternateProfile = "AWS_ALTERNATE_PROFILE" // For tests using an alternate AWS region, the equivalent of AWS_DEFAULT_REGION for that account AwsAlternateRegion = "AWS_ALTERNATE_REGION" @@ -48,3 +48,19 @@ const ( // An inline assume role policy is then used to deny actions for the test TfAccAssumeRoleArn = "TF_ACC_ASSUME_ROLE_ARN" ) + +// Custom environment variables used for assuming a role with resource sweepers +const ( + // The ARN of the IAM Role to assume + TfAwsAssumeRoleARN = "TF_AWS_ASSUME_ROLE_ARN" + + // The duration in seconds the IAM role will be assumed. + // Defaults to 1 hour (3600) instead of the SDK default of 15 minutes. + TfAwsAssumeRoleDuration = "TF_AWS_ASSUME_ROLE_DURATION" + + // An External ID to pass to the assumed role + TfAwsAssumeRoleExternalID = "TF_AWS_ASSUME_ROLE_EXTERNAL_ID" + + // A session name for the assumed role + TfAwsAssumeRoleSessionName = "TF_AWS_ASSUME_ROLE_SESSION_NAME" +) diff --git a/aws/internal/envvar/funcs.go b/aws/internal/envvar/funcs.go index de23c2e08a73..73f5ac019729 100644 --- a/aws/internal/envvar/funcs.go +++ b/aws/internal/envvar/funcs.go @@ -1,6 +1,7 @@ package envvar import ( + "fmt" "os" ) @@ -14,3 +15,29 @@ func GetWithDefault(variable string, defaultValue string) string { return value } + +// RequireOneOf verifies that at least one environment variable is non-empty or returns an error. +// +// If at lease one environment variable is non-empty, returns the first name and value. +func RequireOneOf(names []string, usageMessage string) (string, string, error) { + for _, variable := range names { + value := os.Getenv(variable) + + if value != "" { + return variable, value, nil + } + } + + return "", "", fmt.Errorf("at least one environment variable of %v must be set. Usage: %s", names, usageMessage) +} + +// Require verifies that an environment variable is non-empty or returns an error. +func Require(name string, usageMessage string) (string, error) { + value := os.Getenv(name) + + if value == "" { + return "", fmt.Errorf("environment variable %s must be set. Usage: %s", name, usageMessage) + } + + return value, nil +} diff --git a/aws/internal/envvar/funcs_test.go b/aws/internal/envvar/funcs_test.go index 1a232627283d..d2bc27b6b246 100644 --- a/aws/internal/envvar/funcs_test.go +++ b/aws/internal/envvar/funcs_test.go @@ -48,3 +48,119 @@ func TestGetWithDefault(t *testing.T) { } }) } + +func TestRequireOneOf(t *testing.T) { + envVar1 := "TESTENVVAR_REQUIREONEOF1" + envVar2 := "TESTENVVAR_REQUIREONEOF2" + envVars := []string{envVar1, envVar2} + + t.Run("missing", func(t *testing.T) { + for _, envVar := range envVars { + os.Unsetenv(envVar) + } + + _, _, err := envvar.RequireOneOf(envVars, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("all empty", func(t *testing.T) { + os.Setenv(envVar1, "") + os.Setenv(envVar2, "") + defer unsetEnvVars(envVars) + + _, _, err := envvar.RequireOneOf(envVars, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("some empty", func(t *testing.T) { + wantValue := "pickme" + + os.Setenv(envVar1, "") + os.Setenv(envVar2, wantValue) + defer unsetEnvVars(envVars) + + gotName, gotValue, err := envvar.RequireOneOf(envVars, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if gotName != envVar2 { + t.Fatalf("expected name: %s, got: %s", envVar2, gotName) + } + + if gotValue != wantValue { + t.Fatalf("expected value: %s, got: %s", wantValue, gotValue) + } + }) + + t.Run("all not empty", func(t *testing.T) { + wantValue := "pickme" + + os.Setenv(envVar1, wantValue) + os.Setenv(envVar2, "other") + defer unsetEnvVars(envVars) + + gotName, gotValue, err := envvar.RequireOneOf(envVars, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if gotName != envVar1 { + t.Fatalf("expected name: %s, got: %s", envVar1, gotName) + } + + if gotValue != wantValue { + t.Fatalf("expected value: %s, got: %s", wantValue, gotValue) + } + }) +} + +func TestRequire(t *testing.T) { + envVar := "TESTENVVAR_REQUIRE" + + t.Run("missing", func(t *testing.T) { + os.Unsetenv(envVar) + + _, err := envvar.Require(envVar, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("empty", func(t *testing.T) { + os.Setenv(envVar, "") + defer os.Unsetenv(envVar) + + _, err := envvar.Require(envVar, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("not empty", func(t *testing.T) { + want := "notempty" + + os.Setenv(envVar, want) + defer os.Unsetenv(envVar) + + got, err := envvar.Require(envVar, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if got != want { + t.Fatalf("expected value: %s, got: %s", want, got) + } + }) +} diff --git a/aws/internal/envvar/testing_funcs.go b/aws/internal/envvar/testing_funcs.go index ff01b950a02f..ce81e250a606 100644 --- a/aws/internal/envvar/testing_funcs.go +++ b/aws/internal/envvar/testing_funcs.go @@ -12,17 +12,13 @@ import ( func TestFailIfAllEmpty(t testing.T, names []string, usageMessage string) (string, string) { t.Helper() - for _, variable := range names { - value := os.Getenv(variable) - - if value != "" { - return variable, value - } + name, value, err := RequireOneOf(names, usageMessage) + if err != nil { + t.Fatal(err) + return "", "" } - t.Fatalf("at least one environment variable of %v must be set. Usage: %s", names, usageMessage) - - return "", "" + return name, value } // TestFailIfEmpty verifies that an environment variable is non-empty or fails the test. @@ -54,3 +50,18 @@ func TestSkipIfEmpty(t testing.T, name string, usageMessage string) string { return value } + +// TestSkipIfAllEmpty verifies that at least one environment variable is non-empty or skips the test. +// +// If at lease one environment variable is non-empty, returns the first name and value. +func TestSkipIfAllEmpty(t testing.T, names []string, usageMessage string) (string, string) { + t.Helper() + + name, value, err := RequireOneOf(names, usageMessage) + if err != nil { + t.Skipf("skipping test because %s.", err) + return "", "" + } + + return name, value +} diff --git a/aws/internal/experimental/nullable/bool.go b/aws/internal/experimental/nullable/bool.go new file mode 100644 index 000000000000..29819a81bab4 --- /dev/null +++ b/aws/internal/experimental/nullable/bool.go @@ -0,0 +1,66 @@ +package nullable + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + TypeNullableBool = schema.TypeString +) + +type Bool string + +func (b Bool) IsNull() bool { + return b == "" +} + +func (b Bool) Value() (bool, bool, error) { + if b.IsNull() { + return false, true, nil + } + + value, err := strconv.ParseBool(string(b)) + if err != nil { + return false, false, err + } + return value, false, nil +} + +func NewBool(v bool) Bool { + return Bool(strconv.FormatBool(v)) +} + +// ValidateTypeStringNullableBool provides custom error messaging for TypeString booleans +// Some arguments require a boolean value or unspecified, empty field. +func ValidateTypeStringNullableBool(v interface{}, k string) (ws []string, es []error) { + value, ok := v.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if value == "" { + return + } + + if _, err := strconv.ParseBool(value); err != nil { + es = append(es, fmt.Errorf("%s: cannot parse '%s' as boolean: %w", k, value, err)) + } + + return +} + +// DiffSuppressNullableBoolFalseAsNull allows false to be treated equivalently to null. +// This can be used to allow a practitioner to set false when the API requires a null value, +// as a convenience. +func DiffSuppressNullableBoolFalseAsNull(k, o, n string, d *schema.ResourceData) bool { + ov, onull, _ := Bool(o).Value() + nv, nnull, _ := Bool(n).Value() + if !ov && nnull || onull && !nv { + return true + } + return false +} diff --git a/aws/internal/experimental/nullable/bool_test.go b/aws/internal/experimental/nullable/bool_test.go new file mode 100644 index 000000000000..39f4d05d7328 --- /dev/null +++ b/aws/internal/experimental/nullable/bool_test.go @@ -0,0 +1,82 @@ +package nullable + +import ( + "errors" + "regexp" + "strconv" + "testing" +) + +func TestNullableBool(t *testing.T) { + cases := []struct { + val string + expectNull bool + expectedValue bool + expectedErr error + }{ + { + val: "true", + expectNull: false, + expectedValue: true, + }, + { + val: "false", + expectNull: false, + expectedValue: false, + }, + { + val: "", + expectNull: true, + expectedValue: false, + }, + { + val: "A", + expectNull: false, + expectedValue: false, + expectedErr: strconv.ErrSyntax, + }, + } + + for i, tc := range cases { + v := Bool(tc.val) + + if null := v.IsNull(); null != tc.expectNull { + t.Fatalf("expected test case %d IsNull to return %t, got %t", i, null, tc.expectNull) + } + + value, null, err := v.Value() + if value != tc.expectedValue { + t.Fatalf("expected test case %d Value to be %t, got %t", i, tc.expectedValue, value) + } + if null != tc.expectNull { + t.Fatalf("expected test case %d Value null flag to be %t, got %t", i, tc.expectNull, null) + } + if tc.expectedErr == nil && err != nil { + t.Fatalf("expected test case %d to succeed, got error %s", i, err) + } + if tc.expectedErr != nil { + if !errors.Is(err, tc.expectedErr) { + t.Fatalf("expected test case %d to have error matching \"%s\", got %s", i, tc.expectedErr, err) + } + } + } +} + +func TestValidationBool(t *testing.T) { + runTestCases(t, []testCase{ + { + val: "true", + f: ValidateTypeStringNullableBool, + }, + { + val: "A", + f: ValidateTypeStringNullableBool, + expectedErr: regexp.MustCompile(`[\w]+: cannot parse 'A' as boolean: .*`), + }, + { + val: 1, + f: ValidateTypeStringNullableBool, + expectedErr: regexp.MustCompile(`expected type of [\w]+ to be string`), + }, + }) +} diff --git a/aws/internal/experimental/nullable/int.go b/aws/internal/experimental/nullable/int.go index 539816034853..25feb8112bb3 100644 --- a/aws/internal/experimental/nullable/int.go +++ b/aws/internal/experimental/nullable/int.go @@ -76,3 +76,31 @@ func ValidateTypeStringNullableIntAtLeast(min int) schema.SchemaValidateFunc { return } } + +// ValidateTypeStringNullableIntBetween provides custom error messaging for TypeString ints +// Some arguments require an int value or unspecified, empty field. +func ValidateTypeStringNullableIntBetween(min int, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + value, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if value == "" { + return + } + + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + es = append(es, fmt.Errorf("%s: cannot parse '%s' as int: %w", k, value, err)) + return + } + + if v < int64(min) || v > int64(max) { + es = append(es, fmt.Errorf("expected %s to be at between (%d) and (%d), got %d", k, min, max, v)) + } + + return + } +} diff --git a/aws/internal/experimental/nullable/int_test.go b/aws/internal/experimental/nullable/int_test.go index cc1c61e554ec..7db8ee5917a3 100644 --- a/aws/internal/experimental/nullable/int_test.go +++ b/aws/internal/experimental/nullable/int_test.go @@ -10,23 +10,23 @@ import ( func TestNullableInt(t *testing.T) { cases := []struct { val string - expectedNull bool + expectNull bool expectedValue int64 expectedErr error }{ { val: "1", - expectedNull: false, + expectNull: false, expectedValue: 1, }, { val: "", - expectedNull: true, + expectNull: true, expectedValue: 0, }, { val: "A", - expectedNull: false, + expectNull: false, expectedValue: 0, expectedErr: strconv.ErrSyntax, }, @@ -35,16 +35,16 @@ func TestNullableInt(t *testing.T) { for i, tc := range cases { v := Int(tc.val) - if null := v.IsNull(); null != tc.expectedNull { - t.Fatalf("expected test case %d IsNull to return %t, got %t", i, null, tc.expectedNull) + if null := v.IsNull(); null != tc.expectNull { + t.Fatalf("expected test case %d IsNull to return %t, got %t", i, null, tc.expectNull) } value, null, err := v.Value() if value != tc.expectedValue { t.Fatalf("expected test case %d Value to be %d, got %d", i, tc.expectedValue, value) } - if null != tc.expectedNull { - t.Fatalf("expected test case %d Value null flag to be %t, got %t", i, tc.expectedNull, null) + if null != tc.expectNull { + t.Fatalf("expected test case %d Value null flag to be %t, got %t", i, tc.expectNull, null) } if tc.expectedErr == nil && err != nil { t.Fatalf("expected test case %d to succeed, got error %s", i, err) diff --git a/aws/internal/experimental/nullable/testing.go b/aws/internal/experimental/nullable/testing.go index 9921ac5375f6..c94cba9ba720 100644 --- a/aws/internal/experimental/nullable/testing.go +++ b/aws/internal/experimental/nullable/testing.go @@ -2,9 +2,9 @@ package nullable import ( "regexp" + "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - testing "github.com/mitchellh/go-testing-interface" ) type testCase struct { @@ -13,7 +13,7 @@ type testCase struct { expectedErr *regexp.Regexp } -func runTestCases(t testing.T, cases []testCase) { +func runTestCases(t *testing.T, cases []testCase) { t.Helper() matchErr := func(errs []error, r *regexp.Regexp) bool { diff --git a/aws/internal/generators/listpages/main.go b/aws/internal/generators/listpages/main.go index 8d74a8eb9e90..5f3f6bb530a1 100644 --- a/aws/internal/generators/listpages/main.go +++ b/aws/internal/generators/listpages/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -9,7 +10,6 @@ import ( "go/ast" "go/format" "html/template" - "io/ioutil" "log" "os" "sort" @@ -72,7 +72,7 @@ func main() { src := g.format() - err := ioutil.WriteFile(outputName, src, 0644) + err := os.WriteFile(outputName, src, 0644) if err != nil { log.Fatalf("error writing output: %s", err) } diff --git a/aws/internal/json/equivalence.go b/aws/internal/json/equivalence.go new file mode 100644 index 000000000000..4cc8dd249ae1 --- /dev/null +++ b/aws/internal/json/equivalence.go @@ -0,0 +1,37 @@ +package json + +import ( + "bytes" + "encoding/json" + "reflect" +) + +// BytesEqual compares two arrays of JSON bytes and returns true if the unmarshaled objects represented by the bytes +// are equal according to `reflect.DeepEqual`. +func BytesEqual(b1, b2 []byte) bool { + var o1 interface{} + if err := json.Unmarshal(b1, &o1); err != nil { + return false + } + + var o2 interface{} + if err := json.Unmarshal(b2, &o2); err != nil { + return false + } + + return reflect.DeepEqual(o1, o2) +} + +func StringsEquivalent(s1, s2 string) bool { + b1 := bytes.NewBufferString("") + if err := json.Compact(b1, []byte(s1)); err != nil { + return false + } + + b2 := bytes.NewBufferString("") + if err := json.Compact(b2, []byte(s2)); err != nil { + return false + } + + return BytesEqual(b1.Bytes(), b2.Bytes()) +} diff --git a/aws/internal/json/equivalence_test.go b/aws/internal/json/equivalence_test.go new file mode 100644 index 000000000000..15a863ee8b39 --- /dev/null +++ b/aws/internal/json/equivalence_test.go @@ -0,0 +1,65 @@ +package json + +import ( + "testing" +) + +func TestBytesEqualQuotedAndUnquoted(t *testing.T) { + unquoted := `{"test": "test"}` + quoted := "{\"test\": \"test\"}" + + if !BytesEqual([]byte(unquoted), []byte(quoted)) { + t.Errorf("Expected BytesEqual to return true for %s == %s", unquoted, quoted) + } + + unquotedDiff := `{"test": "test"}` + quotedDiff := "{\"test\": \"tested\"}" + + if BytesEqual([]byte(unquotedDiff), []byte(quotedDiff)) { + t.Errorf("Expected BytesEqual to return false for %s == %s", unquotedDiff, quotedDiff) + } +} + +func TestBytesEqualWhitespaceAndNoWhitespace(t *testing.T) { + noWhitespace := `{"test":"test"}` + whitespace := ` +{ + "test": "test" +}` + + if !BytesEqual([]byte(noWhitespace), []byte(whitespace)) { + t.Errorf("Expected BytesEqual to return true for %s == %s", noWhitespace, whitespace) + } + + noWhitespaceDiff := `{"test":"test"}` + whitespaceDiff := ` +{ + "test": "tested" +}` + + if BytesEqual([]byte(noWhitespaceDiff), []byte(whitespaceDiff)) { + t.Errorf("Expected BytesEqual to return false for %s == %s", noWhitespaceDiff, whitespaceDiff) + } +} + +func TestStringsEquivalentWhitespaceAndNoWhitespace(t *testing.T) { + noWhitespace := `{"test":"test"}` + whitespace := ` +{ + "test": "test" +}` + + if !StringsEquivalent(noWhitespace, whitespace) { + t.Errorf("Expected StringsEquivalent to return true for %s == %s", noWhitespace, whitespace) + } + + noWhitespaceDiff := `{"test":"test"}` + whitespaceDiff := ` +{ + "test": "tested" +}` + + if StringsEquivalent(noWhitespaceDiff, whitespaceDiff) { + t.Errorf("Expected StringsEquivalent to return false for %s == %s", noWhitespaceDiff, whitespaceDiff) + } +} diff --git a/aws/internal/keyvaluetags/create_tags_gen.go b/aws/internal/keyvaluetags/create_tags_gen.go index 096f55925f2a..1c8592e61ee0 100644 --- a/aws/internal/keyvaluetags/create_tags_gen.go +++ b/aws/internal/keyvaluetags/create_tags_gen.go @@ -25,24 +25,19 @@ func Ec2CreateTags(conn *ec2.EC2, identifier string, tagsMap interface{}) error Tags: tags.IgnoreAws().Ec2Tags(), } - err := resource.Retry(EventualConsistencyTimeout, func() *resource.RetryError { - _, err := conn.CreateTags(input) + _, err := tfresource.RetryWhenNotFound(EventualConsistencyTimeout, func() (interface{}, error) { + output, err := conn.CreateTags(input) if tfawserr.ErrCodeContains(err, ".NotFound") { - return resource.RetryableError(err) + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } } - if err != nil { - return resource.NonRetryableError(err) - } - - return nil + return output, err }) - if tfresource.TimedOut(err) { - _, err = conn.CreateTags(input) - } - if err != nil { return fmt.Errorf("error tagging resource (%s): %w", identifier, err) } diff --git a/aws/internal/keyvaluetags/generators/createtags/main.go b/aws/internal/keyvaluetags/generators/createtags/main.go index 0e5f8b92f08e..4da3d449390d 100644 --- a/aws/internal/keyvaluetags/generators/createtags/main.go +++ b/aws/internal/keyvaluetags/generators/createtags/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -32,20 +33,19 @@ func main() { ServiceNames: serviceNames, } templateFuncMap := template.FuncMap{ - "ClientType": keyvaluetags.ServiceClientType, - "ResourceNotFoundErrorCode": keyvaluetags.ServiceResourceNotFoundErrorCode, - "ResourceNotFoundErrorCodeContains": keyvaluetags.ServiceResourceNotFoundErrorCodeContains, - "RetryCreationOnResourceNotFound": keyvaluetags.ServiceRetryCreationOnResourceNotFound, - "TagFunction": keyvaluetags.ServiceTagFunction, - "TagFunctionBatchSize": keyvaluetags.ServiceTagFunctionBatchSize, - "TagInputCustomValue": keyvaluetags.ServiceTagInputCustomValue, - "TagInputIdentifierField": keyvaluetags.ServiceTagInputIdentifierField, - "TagInputIdentifierRequiresSlice": keyvaluetags.ServiceTagInputIdentifierRequiresSlice, - "TagInputTagsField": keyvaluetags.ServiceTagInputTagsField, - "TagPackage": keyvaluetags.ServiceTagPackage, - "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, - "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, - "Title": strings.Title, + "ClientType": keyvaluetags.ServiceClientType, + "ParentResourceNotFoundError": keyvaluetags.ServiceParentResourceNotFoundError, + "RetryCreationOnResourceNotFound": keyvaluetags.ServiceRetryCreationOnResourceNotFound, + "TagFunction": keyvaluetags.ServiceTagFunction, + "TagFunctionBatchSize": keyvaluetags.ServiceTagFunctionBatchSize, + "TagInputCustomValue": keyvaluetags.ServiceTagInputCustomValue, + "TagInputIdentifierField": keyvaluetags.ServiceTagInputIdentifierField, + "TagInputIdentifierRequiresSlice": keyvaluetags.ServiceTagInputIdentifierRequiresSlice, + "TagInputTagsField": keyvaluetags.ServiceTagInputTagsField, + "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, + "Title": strings.Title, } tmpl, err := template.New("createtags").Funcs(templateFuncMap).Parse(templateBody) @@ -133,31 +133,13 @@ func {{ . | Title }}CreateTags(conn {{ . | ClientType }}, identifier string{{ if {{- if . | RetryCreationOnResourceNotFound }} - err := resource.Retry(EventualConsistencyTimeout, func() *resource.RetryError { - _, err := conn.{{ . | TagFunction }}(input) + _, err := tfresource.RetryWhenNotFound(EventualConsistencyTimeout, func() (interface{}, error) { + output, err := conn.{{ . | TagFunction }}(input) - {{- if . | ResourceNotFoundErrorCodeContains }} + {{ . | ParentResourceNotFoundError }} - if tfawserr.ErrCodeContains(err, "{{ . | ResourceNotFoundErrorCodeContains }}") { - - {{- else }} - - if tfawserr.ErrCodeEquals(err, {{ . | ResourceNotFoundErrorCode }}) { - - {{- end }} - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } - - return nil + return output, err }) - - if tfresource.TimedOut(err) { - _, err = conn.{{ . | TagFunction }}(input) - } {{- else }} _, err := conn.{{ . | TagFunction }}(input) {{- end }} diff --git a/aws/internal/keyvaluetags/generators/gettag/main.go b/aws/internal/keyvaluetags/generators/gettag/main.go index 123e291c964d..6a025cd7b697 100644 --- a/aws/internal/keyvaluetags/generators/gettag/main.go +++ b/aws/internal/keyvaluetags/generators/gettag/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -92,19 +93,20 @@ import ( {{- range .ServiceNames }} "github.com/aws/aws-sdk-go/service/{{ . }}" {{- end }} + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) {{- range .ServiceNames }} // {{ . | Title }}GetTag fetches an individual {{ . }} service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over {{ . | Title }}ListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. {{- if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} -func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (bool, *TagData, error) { +func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (*TagData, error) { {{- else }} -func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (bool, *string, error) { +func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (*string, error) { {{- end }} {{- if . | ListTagsInputFilterIdentifierName }} input := &{{ . | TagPackage }}.{{ . | ListTagsFunction }}Input{ @@ -123,7 +125,7 @@ func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | output, err := conn.{{ . | ListTagsFunction }}(input) if err != nil { - return false, nil, err + return nil, err } listTags := {{ . | Title }}KeyValueTags(output.{{ . | ListTagsOutputTagsField }}{{ if . | TagTypeIdentifierField }}, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}{{ end }}) @@ -131,14 +133,18 @@ func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | listTags, err := {{ . | Title }}ListTags(conn, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}) if err != nil { - return false, nil, err + return nil, err } {{- end }} + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) + } + {{ if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} - return listTags.KeyExists(key), listTags.KeyTagData(key), nil + return listTags.KeyTagData(key), nil {{- else }} - return listTags.KeyExists(key), listTags.KeyValue(key), nil + return listTags.KeyValue(key), nil {{- end }} } {{- end }} diff --git a/aws/internal/keyvaluetags/generators/listtags/main.go b/aws/internal/keyvaluetags/generators/listtags/main.go index 4ffe44d98ec5..8aad67ebfe2a 100644 --- a/aws/internal/keyvaluetags/generators/listtags/main.go +++ b/aws/internal/keyvaluetags/generators/listtags/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -22,7 +23,9 @@ var serviceNames = []string{ "acmpca", "amplify", "apigatewayv2", + "appconfig", "appmesh", + "apprunner", "appstream", "appsync", "athena", @@ -103,11 +106,14 @@ var serviceNames = []string{ "rds", "resourcegroups", "route53", + "route53recoveryreadiness", "route53resolver", "sagemaker", "securityhub", "servicediscovery", + "schemas", "sfn", + "shield", "signer", "sns", "sqs", @@ -115,6 +121,7 @@ var serviceNames = []string{ "ssoadmin", "storagegateway", "swf", + "timestreamwrite", "transfer", "waf", "wafregional", @@ -142,6 +149,7 @@ func main() { "ListTagsInputIdentifierField": keyvaluetags.ServiceListTagsInputIdentifierField, "ListTagsInputIdentifierRequiresSlice": keyvaluetags.ServiceListTagsInputIdentifierRequiresSlice, "ListTagsOutputTagsField": keyvaluetags.ServiceListTagsOutputTagsField, + "ParentResourceNotFoundError": keyvaluetags.ServiceParentResourceNotFoundError, "TagPackage": keyvaluetags.ServiceTagPackage, "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, @@ -192,6 +200,8 @@ import ( {{- range .ServiceNames }} "github.com/aws/aws-sdk-go/service/{{ . }}" {{- end }} + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) {{ range .ServiceNames }} @@ -221,6 +231,8 @@ func {{ . | Title }}ListTags(conn {{ . | ClientType }}, identifier string{{ if . output, err := conn.{{ . | ListTagsFunction }}(input) + {{ . | ParentResourceNotFoundError }} + if err != nil { return New(nil), err } diff --git a/aws/internal/keyvaluetags/generators/servicetags/main.go b/aws/internal/keyvaluetags/generators/servicetags/main.go index 057627155924..e3a931fec4af 100644 --- a/aws/internal/keyvaluetags/generators/servicetags/main.go +++ b/aws/internal/keyvaluetags/generators/servicetags/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -21,6 +22,7 @@ var sliceServiceNames = []string{ "acm", "acmpca", "appmesh", + "apprunner", "athena", "autoscaling", "cloud9", @@ -91,11 +93,13 @@ var sliceServiceNames = []string{ "servicecatalog", "servicediscovery", "sfn", + "shield", "sns", "ssm", "ssoadmin", "storagegateway", "swf", + "timestreamwrite", "transfer", "waf", "wafregional", @@ -109,6 +113,7 @@ var mapServiceNames = []string{ "amplify", "apigateway", "apigatewayv2", + "appconfig", "appstream", "appsync", "backup", @@ -118,6 +123,7 @@ var mapServiceNames = []string{ "codestarnotifications", "cognitoidentity", "cognitoidentityprovider", + "connect", "dataexchange", "dlm", "eks", @@ -129,16 +135,20 @@ var mapServiceNames = []string{ "kinesisvideo", "imagebuilder", "lambda", + "macie2", "mediaconnect", "mediaconvert", "medialive", "mediapackage", "mq", + "mwaa", "opsworks", "qldb", "pinpoint", "resourcegroups", + "route53recoveryreadiness", "securityhub", + "schemas", "signer", "sqs", "synthetics", diff --git a/aws/internal/keyvaluetags/generators/updatetags/main.go b/aws/internal/keyvaluetags/generators/updatetags/main.go index 5d9b9cf73eb4..8b48a382b15e 100644 --- a/aws/internal/keyvaluetags/generators/updatetags/main.go +++ b/aws/internal/keyvaluetags/generators/updatetags/main.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -23,7 +24,9 @@ var serviceNames = []string{ "amplify", "apigateway", "apigatewayv2", + "appconfig", "appmesh", + "apprunner", "appstream", "appsync", "athena", @@ -46,6 +49,7 @@ var serviceNames = []string{ "cognitoidentity", "cognitoidentityprovider", "configservice", + "connect", "databasemigrationservice", "dataexchange", "datapipeline", @@ -95,6 +99,7 @@ var serviceNames = []string{ "mediapackage", "mediastore", "mq", + "mwaa", "neptune", "networkfirewall", "networkmanager", @@ -108,12 +113,15 @@ var serviceNames = []string{ "redshift", "resourcegroups", "route53", + "route53recoveryreadiness", "route53resolver", "sagemaker", "secretsmanager", "securityhub", "servicediscovery", + "schemas", "sfn", + "shield", "signer", "sns", "sqs", @@ -122,6 +130,7 @@ var serviceNames = []string{ "storagegateway", "swf", "synthetics", + "timestreamwrite", "transfer", "waf", "wafregional", diff --git a/aws/internal/keyvaluetags/get_tag_gen.go b/aws/internal/keyvaluetags/get_tag_gen.go index 0486f616acc0..67ef0210ac4b 100644 --- a/aws/internal/keyvaluetags/get_tag_gen.go +++ b/aws/internal/keyvaluetags/get_tag_gen.go @@ -10,14 +10,15 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/route53resolver" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // AutoscalingGetTag fetches an individual autoscaling service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over AutoscalingListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func AutoscalingGetTag(conn *autoscaling.AutoScaling, identifier string, resourceType string, key string) (bool, *TagData, error) { +func AutoscalingGetTag(conn *autoscaling.AutoScaling, identifier string, resourceType string, key string) (*TagData, error) { input := &autoscaling.DescribeTagsInput{ Filters: []*autoscaling.Filter{ { @@ -34,50 +35,62 @@ func AutoscalingGetTag(conn *autoscaling.AutoScaling, identifier string, resourc output, err := conn.DescribeTags(input) if err != nil { - return false, nil, err + return nil, err } listTags := AutoscalingKeyValueTags(output.Tags, identifier, resourceType) - return listTags.KeyExists(key), listTags.KeyTagData(key), nil + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) + } + + return listTags.KeyTagData(key), nil } // BatchGetTag fetches an individual batch service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over BatchListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func BatchGetTag(conn *batch.Batch, identifier string, key string) (bool, *string, error) { +func BatchGetTag(conn *batch.Batch, identifier string, key string) (*string, error) { listTags, err := BatchListTags(conn, identifier) if err != nil { - return false, nil, err + return nil, err + } + + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) } - return listTags.KeyExists(key), listTags.KeyValue(key), nil + return listTags.KeyValue(key), nil } // DynamodbGetTag fetches an individual dynamodb service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over DynamodbListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func DynamodbGetTag(conn *dynamodb.DynamoDB, identifier string, key string) (bool, *string, error) { +func DynamodbGetTag(conn *dynamodb.DynamoDB, identifier string, key string) (*string, error) { listTags, err := DynamodbListTags(conn, identifier) if err != nil { - return false, nil, err + return nil, err + } + + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) } - return listTags.KeyExists(key), listTags.KeyValue(key), nil + return listTags.KeyValue(key), nil } // Ec2GetTag fetches an individual ec2 service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over Ec2ListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func Ec2GetTag(conn *ec2.EC2, identifier string, key string) (bool, *string, error) { +func Ec2GetTag(conn *ec2.EC2, identifier string, key string) (*string, error) { input := &ec2.DescribeTagsInput{ Filters: []*ec2.Filter{ { @@ -94,40 +107,52 @@ func Ec2GetTag(conn *ec2.EC2, identifier string, key string) (bool, *string, err output, err := conn.DescribeTags(input) if err != nil { - return false, nil, err + return nil, err } listTags := Ec2KeyValueTags(output.Tags) - return listTags.KeyExists(key), listTags.KeyValue(key), nil + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) + } + + return listTags.KeyValue(key), nil } // EcsGetTag fetches an individual ecs service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over EcsListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func EcsGetTag(conn *ecs.ECS, identifier string, key string) (bool, *string, error) { +func EcsGetTag(conn *ecs.ECS, identifier string, key string) (*string, error) { listTags, err := EcsListTags(conn, identifier) if err != nil { - return false, nil, err + return nil, err + } + + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) } - return listTags.KeyExists(key), listTags.KeyValue(key), nil + return listTags.KeyValue(key), nil } // Route53resolverGetTag fetches an individual route53resolver service tag for a resource. -// Returns whether the key exists, the key value, and any errors. +// Returns whether the key value and any errors. A NotFoundError is used to signal that no value was found. // This function will optimise the handling over Route53resolverListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func Route53resolverGetTag(conn *route53resolver.Route53Resolver, identifier string, key string) (bool, *string, error) { +func Route53resolverGetTag(conn *route53resolver.Route53Resolver, identifier string, key string) (*string, error) { listTags, err := Route53resolverListTags(conn, identifier) if err != nil { - return false, nil, err + return nil, err + } + + if !listTags.KeyExists(key) { + return nil, tfresource.NewEmptyResultError(nil) } - return listTags.KeyExists(key), listTags.KeyValue(key), nil + return listTags.KeyValue(key), nil } diff --git a/aws/internal/keyvaluetags/iam_tags.go b/aws/internal/keyvaluetags/iam_tags.go index 1059708d2806..897a4f3dd3ee 100644 --- a/aws/internal/keyvaluetags/iam_tags.go +++ b/aws/internal/keyvaluetags/iam_tags.go @@ -1,3 +1,4 @@ +//go:build !generate // +build !generate package keyvaluetags @@ -80,3 +81,178 @@ func IamUserUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, return nil } + +// IamInstanceProfileUpdateTags updates IAM Instance Profile tags. +// The identifier is the Instance Profile name. +func IamInstanceProfileUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagInstanceProfileInput{ + InstanceProfileName: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagInstanceProfile(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagInstanceProfileInput{ + InstanceProfileName: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagInstanceProfile(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// IamOpenIDConnectProviderUpdateTags updates IAM OpenID Connect Provider tags. +// The identifier is the OpenID Connect Provider ARN. +func IamOpenIDConnectProviderUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagOpenIDConnectProviderInput{ + OpenIDConnectProviderArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagOpenIDConnectProvider(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagOpenIDConnectProviderInput{ + OpenIDConnectProviderArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagOpenIDConnectProvider(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// IamPolicyUpdateTags updates IAM Policy tags. +// The identifier is the Policy ARN. +func IamPolicyUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagPolicyInput{ + PolicyArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagPolicy(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagPolicyInput{ + PolicyArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagPolicy(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// IamSAMLProviderUpdateTags updates IAM SAML Provider tags. +// The identifier is the SAML Provider ARN. +func IamSAMLProviderUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagSAMLProviderInput{ + SAMLProviderArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagSAMLProvider(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagSAMLProviderInput{ + SAMLProviderArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagSAMLProvider(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// IamServerCertificateUpdateTags updates IAM Server Certificate tags. +// The identifier is the Server Certificate name. +func IamServerCertificateUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagServerCertificateInput{ + ServerCertificateName: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagServerCertificate(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagServerCertificateInput{ + ServerCertificateName: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagServerCertificate(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} diff --git a/aws/internal/keyvaluetags/inspector_tags.go b/aws/internal/keyvaluetags/inspector_tags.go index d390b9d3e823..f7043c922fbb 100644 --- a/aws/internal/keyvaluetags/inspector_tags.go +++ b/aws/internal/keyvaluetags/inspector_tags.go @@ -1,3 +1,4 @@ +//go:build !generate // +build !generate package keyvaluetags diff --git a/aws/internal/keyvaluetags/key_value_tags.go b/aws/internal/keyvaluetags/key_value_tags.go index f9af201a7c07..31d4834c54f9 100644 --- a/aws/internal/keyvaluetags/key_value_tags.go +++ b/aws/internal/keyvaluetags/key_value_tags.go @@ -25,6 +25,11 @@ const ( ServerlessApplicationRepositoryTagKeyPrefix = `serverlessrepo:` ) +// DefaultConfig contains tags to default across all resources. +type DefaultConfig struct { + Tags KeyValueTags +} + // IgnoreConfig contains various options for removing resource tags. type IgnoreConfig struct { Keys KeyValueTags @@ -50,6 +55,45 @@ func (tags KeyValueTags) IgnoreAws() KeyValueTags { return result } +// GetTags is convenience method that returns the DefaultConfig's Tags, if any +func (dc *DefaultConfig) GetTags() KeyValueTags { + if dc == nil { + return nil + } + + return dc.Tags +} + +// MergeTags returns the result of keyvaluetags.Merge() on the given +// DefaultConfig.Tags with KeyValueTags provided as an argument, +// overriding the value of any tag with a matching key. +func (dc *DefaultConfig) MergeTags(tags KeyValueTags) KeyValueTags { + if dc == nil || dc.Tags == nil { + return tags + } + + return dc.Tags.Merge(tags) +} + +// TagsEqual returns true if the given configuration's Tags +// are equal to those passed in as an argument; +// otherwise returns false +func (dc *DefaultConfig) TagsEqual(tags KeyValueTags) bool { + if dc == nil || dc.Tags == nil { + return tags == nil + } + + if tags == nil { + return false + } + + if len(tags) == 0 { + return len(dc.Tags) == 0 + } + + return dc.Tags.ContainsAll(tags) +} + // IgnoreConfig returns any tags not removed by a given configuration. func (tags KeyValueTags) IgnoreConfig(config *IgnoreConfig) KeyValueTags { if config == nil { @@ -384,6 +428,34 @@ func (tags KeyValueTags) ContainsAll(target KeyValueTags) bool { return true } +// Equal returns whether or two sets of key-value tags are equal. +func (tags KeyValueTags) Equal(other KeyValueTags) bool { + if tags == nil && other == nil { + return true + } + + if tags == nil || other == nil { + return false + } + + if len(tags) != len(other) { + return false + } + + for k, v := range tags { + o, ok := other[k] + if !ok { + return false + } + + if !v.Equal(o) { + return false + } + } + + return true +} + // Hash returns a stable hash value. // The returned value may be negative (i.e. not suitable for a 'Set' function). func (tags KeyValueTags) Hash() int { @@ -401,6 +473,27 @@ func (tags KeyValueTags) Hash() int { return hash } +// RemoveDefaultConfig returns tags not present in a DefaultConfig object +// in addition to tags with key/value pairs that override those in a DefaultConfig; +// however, if all tags present in the DefaultConfig object are equivalent to those +// in the given KeyValueTags, then the KeyValueTags are returned, effectively +// bypassing the need to remove differing tags. +func (tags KeyValueTags) RemoveDefaultConfig(dc *DefaultConfig) KeyValueTags { + if dc == nil || dc.Tags == nil { + return tags + } + + result := make(KeyValueTags) + + for k, v := range tags { + if defaultVal, ok := dc.Tags[k]; !ok || !v.Equal(defaultVal) { + result[k] = v + } + } + + return result +} + // String returns the default string representation of the KeyValueTags. func (tags KeyValueTags) String() string { var builder strings.Builder @@ -435,20 +528,42 @@ func (tags KeyValueTags) UrlEncode() string { return values.Encode() } -// New creates KeyValueTags from common Terraform Provider SDK types. -// Supports map[string]string, map[string]*string, map[string]interface{}, and []interface{}. +// UrlQueryString returns the KeyValueTags formatted as URL Query parameters without encoding. +func (tags KeyValueTags) UrlQueryString() string { + keys := make([]string, 0, len(tags)) + for k, v := range tags { + if v == nil || v.Value == nil { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + + var buf strings.Builder + for _, k := range keys { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(k) + buf.WriteByte('=') + buf.WriteString(*tags[k].Value) + } + + return buf.String() +} + +// New creates KeyValueTags from common types or returns an empty KeyValueTags. +// +// Supports various Terraform Plugin SDK types including map[string]string, +// map[string]*string, map[string]interface{}, and []interface{}. // When passed []interface{}, all elements are treated as keys and assigned nil values. +// When passed KeyValueTags or its underlying type implementation, returns itself. func New(i interface{}) KeyValueTags { switch value := i.(type) { + case KeyValueTags: + return make(KeyValueTags).Merge(value) case map[string]*TagData: - kvtm := make(KeyValueTags, len(value)) - - for k, v := range value { - tagData := v - kvtm[k] = tagData - } - - return kvtm + return make(KeyValueTags).Merge(KeyValueTags(value)) case map[string]string: kvtm := make(KeyValueTags, len(value)) @@ -477,8 +592,13 @@ func New(i interface{}) KeyValueTags { kvtm := make(KeyValueTags, len(value)) for k, v := range value { - str := v.(string) - kvtm[k] = &TagData{Value: &str} + kvtm[k] = &TagData{} + + str, ok := v.(string) + + if ok { + kvtm[k].Value = &str + } } return kvtm diff --git a/aws/internal/keyvaluetags/key_value_tags_test.go b/aws/internal/keyvaluetags/key_value_tags_test.go index 08a9376a2bc4..9e3a4b20e4cd 100644 --- a/aws/internal/keyvaluetags/key_value_tags_test.go +++ b/aws/internal/keyvaluetags/key_value_tags_test.go @@ -4,6 +4,293 @@ import ( "testing" ) +func TestKeyValueTagsDefaultConfigGetTags(t *testing.T) { + testCases := []struct { + name string + defaultConfig *DefaultConfig + want KeyValueTags + }{ + { + name: "empty config", + defaultConfig: &DefaultConfig{}, + want: KeyValueTags{}, + }, + { + name: "nil config", + defaultConfig: nil, + want: nil, + }, + { + name: "with Tags config", + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + }), + }, + want: New(map[string]string{ + "key1": "value1", + "key2": "value2", + }), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.defaultConfig.GetTags() + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want.Map()) + }) + } +} + +func TestKeyValueTagsDefaultConfigMergeTags(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want map[string]string + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some overridden", + tags: New(map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys none matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key6": "value6", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.defaultConfig.MergeTags(testCase.tags) + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + }) + } +} + +func TestKeyValueTagsDefaultConfigTagsEqual(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want bool + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: false, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: false, + }, + { + name: "empty tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: false, + }, + { + name: "no tags", + tags: nil, + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: false, + }, + { + name: "empty config and no tags", + tags: nil, + defaultConfig: &DefaultConfig{}, + want: true, + }, + { + name: "no config and tags", + tags: nil, + defaultConfig: nil, + want: true, + }, + { + name: "keys and values all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: true, + }, + { + name: "only keys matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value0", + "key2": "value1", + "key3": "value2", + }), + }, + want: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.defaultConfig.TagsEqual(testCase.tags) + + if got != testCase.want { + t.Errorf("got %t; want %t", got, testCase.want) + } + }) + } +} + func TestKeyValueTagsIgnoreAws(t *testing.T) { testCases := []struct { name string @@ -1537,39 +1824,165 @@ func TestKeyValueTagsContainsAll(t *testing.T) { } } -func TestKeyValueTagsHash(t *testing.T) { +func TestKeyValueTagsEqual(t *testing.T) { testCases := []struct { - name string - tags KeyValueTags - zero bool + name string + source KeyValueTags + target KeyValueTags + want bool }{ { - name: "empty", - tags: New(map[string]string{}), - zero: true, + name: "nil", + source: nil, + target: nil, + want: true, }, { - name: "nil value", - tags: New(map[string]*string{ - "key1": nil, + name: "empty", + source: New(map[string]string{}), + target: New(map[string]string{}), + want: true, + }, + { + name: "source_nil", + source: nil, + target: New(map[string]string{ + "key1": "value1", }), - zero: false, + want: false, }, { - name: "not_empty", - tags: New(map[string]string{ + name: "source_empty", + source: New(map[string]string{}), + target: New(map[string]string{ "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", }), - zero: false, + want: false, }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - got := testCase.tags.Hash() + { + name: "target_nil", + source: New(map[string]string{ + "key1": "value1", + }), + target: nil, + want: false, + }, + { + name: "target_empty", + source: New(map[string]string{ + "key1": "value1", + }), + target: New(map[string]string{}), + want: false, + }, + { + name: "nil value matches", + source: New(map[string]*string{ + "key1": nil, + }), + target: New(map[string]*string{ + "key1": nil, + }), + want: true, + }, + { + name: "exact_match", + source: New(map[string]string{ + "key1": "value1", + "key2": "value2", + }), + target: New(map[string]string{ + "key1": "value1", + "key2": "value2", + }), + want: true, + }, + { + name: "source_contains_all", + source: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + target: New(map[string]string{ + "key1": "value1", + "key3": "value3", + }), + want: false, + }, + { + name: "source_does_not_contain_all", + source: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + target: New(map[string]string{ + "key1": "value1", + "key4": "value4", + }), + want: false, + }, + { + name: "target_value_neq", + source: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + target: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value4", + }), + want: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.source.Equal(testCase.target) + + if got != testCase.want { + t.Errorf("unexpected Equal: %t", got) + } + }) + } +} + +func TestKeyValueTagsHash(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + zero bool + }{ + { + name: "empty", + tags: New(map[string]string{}), + zero: true, + }, + { + name: "nil value", + tags: New(map[string]*string{ + "key1": nil, + }), + zero: false, + }, + { + name: "not_empty", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + }), + zero: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.Hash() if (got == 0 && !testCase.zero) || (got != 0 && testCase.zero) { t.Errorf("unexpected hash code: %d", got) @@ -1578,6 +1991,135 @@ func TestKeyValueTagsHash(t *testing.T) { } } +func TestKeyValueTagsRemoveDefaultConfig(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want map[string]string + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{}, + }, + { + name: "keys all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{}, + }, + { + name: "keys some matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some overridden", + tags: New(map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys none matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.RemoveDefaultConfig(testCase.defaultConfig) + + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + }) + } +} + func TestKeyValueTagsUrlEncode(t *testing.T) { testCases := []struct { name string @@ -1634,6 +2176,216 @@ func TestKeyValueTagsUrlEncode(t *testing.T) { } } +func TestKeyValueTagsUrlQueryString(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + want string + }{ + { + name: "empty", + tags: New(map[string]string{}), + want: "", + }, + { + name: "nil value", + tags: New(map[string]*string{ + "key1": nil, + }), + want: "", + }, + { + name: "single", + tags: New(map[string]string{ + "key1": "value1", + }), + want: "key1=value1", + }, + { + name: "multiple", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + want: "key1=value1&key2=value2&key3=value3", + }, + { + name: "multiple_with_encoded", + tags: New(map[string]string{ + "key1": "value 1", + "key@2": "value+:2", + "key3": "value3", + }), + want: "key1=value 1&key3=value3&key@2=value+:2", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.UrlQueryString() + + if got != testCase.want { + t.Errorf("unexpected query string value: %q", got) + } + }) + } +} + +func TestNew(t *testing.T) { + testCases := []struct { + name string + source interface{} + want map[string]string + }{ + { + name: "empty_KeyValueTags", + source: KeyValueTags{}, + want: map[string]string{}, + }, + { + name: "empty_map_string_TagDataPointer", + source: map[string]*TagData{}, + want: map[string]string{}, + }, + { + name: "empty_map_string_interface", + source: map[string]interface{}{}, + want: map[string]string{}, + }, + { + name: "empty_map_string_string", + source: map[string]string{}, + want: map[string]string{}, + }, + { + name: "empty_map_string_stringPointer", + source: map[string]*string{}, + want: map[string]string{}, + }, + { + name: "empty_slice_interface", + source: []interface{}{}, + want: map[string]string{}, + }, + { + name: "non_empty_KeyValueTags", + source: KeyValueTags{ + "key1": &TagData{ + Value: nil, + }, + "key2": &TagData{ + Value: testStringPtr(""), + }, + "key3": &TagData{ + Value: testStringPtr("value3"), + }, + }, + want: map[string]string{ + "key1": "", + "key2": "", + "key3": "value3", + }, + }, + { + name: "non_empty_map_string_TagDataPointer", + source: map[string]*TagData{ + "key1": { + Value: nil, + }, + "key2": { + Value: testStringPtr(""), + }, + "key3": { + Value: testStringPtr("value3"), + }, + }, + want: map[string]string{ + "key1": "", + "key2": "", + "key3": "value3", + }, + }, + { + name: "non_empty_map_string_interface", + source: map[string]interface{}{ + "key1": nil, + "key2": "", + "key3": "value3", + }, + want: map[string]string{ + "key1": "", + "key2": "", + "key3": "value3", + }, + }, + { + name: "non_empty_map_string_string", + source: map[string]string{ + "key1": "", + "key2": "value2", + }, + want: map[string]string{ + "key1": "", + "key2": "value2", + }, + }, + { + name: "non_empty_map_string_stringPointer", + source: map[string]*string{ + "key1": nil, + "key2": testStringPtr(""), + "key3": testStringPtr("value3"), + }, + want: map[string]string{ + "key1": "", + "key2": "", + "key3": "value3", + }, + }, + { + name: "non_empty_slice_interface", + source: []interface{}{ + "key1", + "key2", + }, + want: map[string]string{ + "key1": "", + "key2": "", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := New(testCase.source) + + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + + // Verify that any source KeyTagValues types are copied + // Unfortunately must be done for each separate type + switch src := testCase.source.(type) { + case KeyValueTags: + src.Merge(New(map[string]string{"mergekey": "mergevalue"})) + + _, ok := got.Map()["mergekey"] + + if ok { + t.Fatal("expected source to be copied, got source modification") + } + case map[string]*TagData: + src["mergekey"] = &TagData{Value: testStringPtr("mergevalue")} + + _, ok := got.Map()["mergekey"] + + if ok { + t.Fatal("expected source to be copied, got source modification") + } + } + }) + } +} + func TestTagDataEqual(t *testing.T) { testCases := []struct { name string diff --git a/aws/internal/keyvaluetags/list_tags_gen.go b/aws/internal/keyvaluetags/list_tags_gen.go index 5cbec75b054b..c274df4734c9 100644 --- a/aws/internal/keyvaluetags/list_tags_gen.go +++ b/aws/internal/keyvaluetags/list_tags_gen.go @@ -9,7 +9,9 @@ import ( "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/amplify" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/aws/aws-sdk-go/service/appconfig" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/aws/aws-sdk-go/service/apprunner" "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" @@ -90,11 +92,14 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/resourcegroups" "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53recoveryreadiness" "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/aws/aws-sdk-go/service/schemas" "github.com/aws/aws-sdk-go/service/securityhub" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/aws/aws-sdk-go/service/sfn" + "github.com/aws/aws-sdk-go/service/shield" "github.com/aws/aws-sdk-go/service/signer" "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" @@ -102,6 +107,7 @@ import ( "github.com/aws/aws-sdk-go/service/ssoadmin" "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/aws/aws-sdk-go/service/swf" + "github.com/aws/aws-sdk-go/service/timestreamwrite" "github.com/aws/aws-sdk-go/service/transfer" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafregional" @@ -109,6 +115,8 @@ import ( "github.com/aws/aws-sdk-go/service/worklink" "github.com/aws/aws-sdk-go/service/workspaces" "github.com/aws/aws-sdk-go/service/xray" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) // AccessanalyzerListTags lists accessanalyzer service tags. @@ -121,6 +129,13 @@ func AccessanalyzerListTags(conn *accessanalyzer.AccessAnalyzer, identifier stri output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -138,6 +153,13 @@ func AcmListTags(conn *acm.ACM, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForCertificate(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -155,6 +177,13 @@ func AcmpcaListTags(conn *acmpca.ACMPCA, identifier string) (KeyValueTags, error output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -172,6 +201,13 @@ func AmplifyListTags(conn *amplify.Amplify, identifier string) (KeyValueTags, er output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -189,6 +225,13 @@ func Apigatewayv2ListTags(conn *apigatewayv2.ApiGatewayV2, identifier string) (K output, err := conn.GetTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -196,6 +239,30 @@ func Apigatewayv2ListTags(conn *apigatewayv2.ApiGatewayV2, identifier string) (K return Apigatewayv2KeyValueTags(output.Tags), nil } +// AppconfigListTags lists appconfig service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func AppconfigListTags(conn *appconfig.AppConfig, identifier string) (KeyValueTags, error) { + input := &appconfig.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return AppconfigKeyValueTags(output.Tags), nil +} + // AppmeshListTags lists appmesh service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -206,6 +273,13 @@ func AppmeshListTags(conn *appmesh.AppMesh, identifier string) (KeyValueTags, er output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -213,6 +287,30 @@ func AppmeshListTags(conn *appmesh.AppMesh, identifier string) (KeyValueTags, er return AppmeshKeyValueTags(output.Tags), nil } +// ApprunnerListTags lists apprunner service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ApprunnerListTags(conn *apprunner.AppRunner, identifier string) (KeyValueTags, error) { + input := &apprunner.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return ApprunnerKeyValueTags(output.Tags), nil +} + // AppstreamListTags lists appstream service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -223,6 +321,13 @@ func AppstreamListTags(conn *appstream.AppStream, identifier string) (KeyValueTa output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -240,6 +345,13 @@ func AppsyncListTags(conn *appsync.AppSync, identifier string) (KeyValueTags, er output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -257,6 +369,13 @@ func AthenaListTags(conn *athena.Athena, identifier string) (KeyValueTags, error output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -279,6 +398,13 @@ func AutoscalingListTags(conn *autoscaling.AutoScaling, identifier string, resou output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -296,6 +422,13 @@ func BackupListTags(conn *backup.Backup, identifier string) (KeyValueTags, error output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -313,6 +446,13 @@ func BatchListTags(conn *batch.Batch, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -330,6 +470,13 @@ func Cloud9ListTags(conn *cloud9.Cloud9, identifier string) (KeyValueTags, error output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -347,6 +494,13 @@ func CloudfrontListTags(conn *cloudfront.CloudFront, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -364,6 +518,13 @@ func Cloudhsmv2ListTags(conn *cloudhsmv2.CloudHSMV2, identifier string) (KeyValu output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -381,6 +542,13 @@ func CloudtrailListTags(conn *cloudtrail.CloudTrail, identifier string) (KeyValu output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -398,6 +566,13 @@ func CloudwatchListTags(conn *cloudwatch.CloudWatch, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -415,6 +590,13 @@ func CloudwatcheventsListTags(conn *cloudwatchevents.CloudWatchEvents, identifie output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -432,6 +614,13 @@ func CloudwatchlogsListTags(conn *cloudwatchlogs.CloudWatchLogs, identifier stri output, err := conn.ListTagsLogGroup(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -449,6 +638,13 @@ func CodeartifactListTags(conn *codeartifact.CodeArtifact, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -466,6 +662,13 @@ func CodecommitListTags(conn *codecommit.CodeCommit, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -483,6 +686,13 @@ func CodedeployListTags(conn *codedeploy.CodeDeploy, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -500,6 +710,13 @@ func CodepipelineListTags(conn *codepipeline.CodePipeline, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -517,6 +734,13 @@ func CodestarconnectionsListTags(conn *codestarconnections.CodeStarConnections, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -534,6 +758,13 @@ func CodestarnotificationsListTags(conn *codestarnotifications.CodeStarNotificat output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -551,6 +782,13 @@ func CognitoidentityListTags(conn *cognitoidentity.CognitoIdentity, identifier s output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -568,6 +806,13 @@ func CognitoidentityproviderListTags(conn *cognitoidentityprovider.CognitoIdenti output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -585,6 +830,13 @@ func ConfigserviceListTags(conn *configservice.ConfigService, identifier string) output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -602,6 +854,13 @@ func DatabasemigrationserviceListTags(conn *databasemigrationservice.DatabaseMig output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -619,6 +878,13 @@ func DataexchangeListTags(conn *dataexchange.DataExchange, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -636,6 +902,13 @@ func DatasyncListTags(conn *datasync.DataSync, identifier string) (KeyValueTags, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -653,6 +926,13 @@ func DaxListTags(conn *dax.DAX, identifier string) (KeyValueTags, error) { output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -670,6 +950,13 @@ func DevicefarmListTags(conn *devicefarm.DeviceFarm, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -687,6 +974,13 @@ func DirectconnectListTags(conn *directconnect.DirectConnect, identifier string) output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -704,6 +998,13 @@ func DirectoryserviceListTags(conn *directoryservice.DirectoryService, identifie output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -721,6 +1022,13 @@ func DlmListTags(conn *dlm.DLM, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -738,6 +1046,13 @@ func DocdbListTags(conn *docdb.DocDB, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -755,6 +1070,13 @@ func DynamodbListTags(conn *dynamodb.DynamoDB, identifier string) (KeyValueTags, output, err := conn.ListTagsOfResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -777,6 +1099,13 @@ func Ec2ListTags(conn *ec2.EC2, identifier string) (KeyValueTags, error) { output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeContains(err, ".NotFound") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -794,6 +1123,13 @@ func EcrListTags(conn *ecr.ECR, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -811,6 +1147,13 @@ func EcsListTags(conn *ecs.ECS, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrMessageContains(err, "InvalidParameterException", "The specified cluster is inactive. Specify an active cluster and try again.") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -828,6 +1171,13 @@ func EfsListTags(conn *efs.EFS, identifier string) (KeyValueTags, error) { output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -845,6 +1195,13 @@ func EksListTags(conn *eks.EKS, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -862,6 +1219,13 @@ func ElasticacheListTags(conn *elasticache.ElastiCache, identifier string) (KeyV output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -879,6 +1243,13 @@ func ElasticbeanstalkListTags(conn *elasticbeanstalk.ElasticBeanstalk, identifie output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -896,6 +1267,13 @@ func ElasticsearchserviceListTags(conn *elasticsearchservice.ElasticsearchServic output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -913,6 +1291,13 @@ func ElbListTags(conn *elb.ELB, identifier string) (KeyValueTags, error) { output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -930,6 +1315,13 @@ func Elbv2ListTags(conn *elbv2.ELBV2, identifier string) (KeyValueTags, error) { output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -947,6 +1339,13 @@ func FirehoseListTags(conn *firehose.Firehose, identifier string) (KeyValueTags, output, err := conn.ListTagsForDeliveryStream(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -964,6 +1363,13 @@ func FsxListTags(conn *fsx.FSx, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -981,6 +1387,13 @@ func GameliftListTags(conn *gamelift.GameLift, identifier string) (KeyValueTags, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -998,6 +1411,13 @@ func GlacierListTags(conn *glacier.Glacier, identifier string) (KeyValueTags, er output, err := conn.ListTagsForVault(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1015,6 +1435,13 @@ func GlobalacceleratorListTags(conn *globalaccelerator.GlobalAccelerator, identi output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1032,6 +1459,13 @@ func GlueListTags(conn *glue.Glue, identifier string) (KeyValueTags, error) { output, err := conn.GetTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1049,6 +1483,13 @@ func GreengrassListTags(conn *greengrass.Greengrass, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1066,6 +1507,13 @@ func GuarddutyListTags(conn *guardduty.GuardDuty, identifier string) (KeyValueTa output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1083,6 +1531,13 @@ func ImagebuilderListTags(conn *imagebuilder.Imagebuilder, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1100,6 +1555,13 @@ func InspectorListTags(conn *inspector.Inspector, identifier string) (KeyValueTa output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1117,6 +1579,13 @@ func IotListTags(conn *iot.IoT, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1134,6 +1603,13 @@ func IotanalyticsListTags(conn *iotanalytics.IoTAnalytics, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1151,6 +1627,13 @@ func IoteventsListTags(conn *iotevents.IoTEvents, identifier string) (KeyValueTa output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1168,6 +1651,13 @@ func KafkaListTags(conn *kafka.Kafka, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1185,6 +1675,13 @@ func KinesisListTags(conn *kinesis.Kinesis, identifier string) (KeyValueTags, er output, err := conn.ListTagsForStream(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1202,6 +1699,13 @@ func KinesisanalyticsListTags(conn *kinesisanalytics.KinesisAnalytics, identifie output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1219,6 +1723,13 @@ func Kinesisanalyticsv2ListTags(conn *kinesisanalyticsv2.KinesisAnalyticsV2, ide output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1236,6 +1747,13 @@ func KinesisvideoListTags(conn *kinesisvideo.KinesisVideo, identifier string) (K output, err := conn.ListTagsForStream(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1253,6 +1771,13 @@ func KmsListTags(conn *kms.KMS, identifier string) (KeyValueTags, error) { output, err := conn.ListResourceTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1270,6 +1795,13 @@ func LambdaListTags(conn *lambda.Lambda, identifier string) (KeyValueTags, error output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1287,6 +1819,13 @@ func LicensemanagerListTags(conn *licensemanager.LicenseManager, identifier stri output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1304,6 +1843,13 @@ func MediaconnectListTags(conn *mediaconnect.MediaConnect, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1321,6 +1867,13 @@ func MediaconvertListTags(conn *mediaconvert.MediaConvert, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1338,6 +1891,13 @@ func MedialiveListTags(conn *medialive.MediaLive, identifier string) (KeyValueTa output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1355,6 +1915,13 @@ func MediapackageListTags(conn *mediapackage.MediaPackage, identifier string) (K output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1372,6 +1939,13 @@ func MediastoreListTags(conn *mediastore.MediaStore, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1389,6 +1963,13 @@ func MqListTags(conn *mq.MQ, identifier string) (KeyValueTags, error) { output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1406,6 +1987,13 @@ func NeptuneListTags(conn *neptune.Neptune, identifier string) (KeyValueTags, er output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1423,6 +2011,13 @@ func NetworkfirewallListTags(conn *networkfirewall.NetworkFirewall, identifier s output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1440,6 +2035,13 @@ func NetworkmanagerListTags(conn *networkmanager.NetworkManager, identifier stri output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1457,6 +2059,13 @@ func OpsworksListTags(conn *opsworks.OpsWorks, identifier string) (KeyValueTags, output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1474,6 +2083,13 @@ func OrganizationsListTags(conn *organizations.Organizations, identifier string) output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1491,6 +2107,13 @@ func PinpointListTags(conn *pinpoint.Pinpoint, identifier string) (KeyValueTags, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1508,6 +2131,13 @@ func QldbListTags(conn *qldb.QLDB, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1525,6 +2155,13 @@ func QuicksightListTags(conn *quicksight.QuickSight, identifier string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1542,6 +2179,13 @@ func RdsListTags(conn *rds.RDS, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1559,6 +2203,13 @@ func ResourcegroupsListTags(conn *resourcegroups.ResourceGroups, identifier stri output, err := conn.GetTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1577,6 +2228,13 @@ func Route53ListTags(conn *route53.Route53, identifier string, resourceType stri output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1584,6 +2242,30 @@ func Route53ListTags(conn *route53.Route53, identifier string, resourceType stri return Route53KeyValueTags(output.ResourceTagSet.Tags), nil } +// Route53recoveryreadinessListTags lists route53recoveryreadiness service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func Route53recoveryreadinessListTags(conn *route53recoveryreadiness.Route53RecoveryReadiness, identifier string) (KeyValueTags, error) { + input := &route53recoveryreadiness.ListTagsForResourcesInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResources(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return Route53recoveryreadinessKeyValueTags(output.Tags), nil +} + // Route53resolverListTags lists route53resolver service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -1594,6 +2276,13 @@ func Route53resolverListTags(conn *route53resolver.Route53Resolver, identifier s output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1611,6 +2300,13 @@ func SagemakerListTags(conn *sagemaker.SageMaker, identifier string) (KeyValueTa output, err := conn.ListTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1618,6 +2314,30 @@ func SagemakerListTags(conn *sagemaker.SageMaker, identifier string) (KeyValueTa return SagemakerKeyValueTags(output.Tags), nil } +// SchemasListTags lists schemas service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func SchemasListTags(conn *schemas.Schemas, identifier string) (KeyValueTags, error) { + input := &schemas.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return SchemasKeyValueTags(output.Tags), nil +} + // SecurityhubListTags lists securityhub service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -1628,6 +2348,13 @@ func SecurityhubListTags(conn *securityhub.SecurityHub, identifier string) (KeyV output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1645,6 +2372,13 @@ func ServicediscoveryListTags(conn *servicediscovery.ServiceDiscovery, identifie output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1662,6 +2396,13 @@ func SfnListTags(conn *sfn.SFN, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1669,6 +2410,30 @@ func SfnListTags(conn *sfn.SFN, identifier string) (KeyValueTags, error) { return SfnKeyValueTags(output.Tags), nil } +// ShieldListTags lists shield service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ShieldListTags(conn *shield.Shield, identifier string) (KeyValueTags, error) { + input := &shield.ListTagsForResourceInput{ + ResourceARN: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return ShieldKeyValueTags(output.Tags), nil +} + // SignerListTags lists signer service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -1679,6 +2444,13 @@ func SignerListTags(conn *signer.Signer, identifier string) (KeyValueTags, error output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1696,6 +2468,13 @@ func SnsListTags(conn *sns.SNS, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1713,6 +2492,13 @@ func SqsListTags(conn *sqs.SQS, identifier string) (KeyValueTags, error) { output, err := conn.ListQueueTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1731,6 +2517,13 @@ func SsmListTags(conn *ssm.SSM, identifier string, resourceType string) (KeyValu output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1749,6 +2542,13 @@ func SsoadminListTags(conn *ssoadmin.SSOAdmin, identifier string, resourceType s output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1766,6 +2566,13 @@ func StoragegatewayListTags(conn *storagegateway.StorageGateway, identifier stri output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1783,6 +2590,13 @@ func SwfListTags(conn *swf.SWF, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1790,6 +2604,30 @@ func SwfListTags(conn *swf.SWF, identifier string) (KeyValueTags, error) { return SwfKeyValueTags(output.Tags), nil } +// TimestreamwriteListTags lists timestreamwrite service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func TimestreamwriteListTags(conn *timestreamwrite.TimestreamWrite, identifier string) (KeyValueTags, error) { + input := ×treamwrite.ListTagsForResourceInput{ + ResourceARN: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return New(nil), err + } + + return TimestreamwriteKeyValueTags(output.Tags), nil +} + // TransferListTags lists transfer service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -1800,6 +2638,13 @@ func TransferListTags(conn *transfer.Transfer, identifier string) (KeyValueTags, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1817,6 +2662,13 @@ func WafListTags(conn *waf.WAF, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1834,6 +2686,13 @@ func WafregionalListTags(conn *wafregional.WAFRegional, identifier string) (KeyV output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1851,6 +2710,13 @@ func Wafv2ListTags(conn *wafv2.WAFV2, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1868,6 +2734,13 @@ func WorklinkListTags(conn *worklink.WorkLink, identifier string) (KeyValueTags, output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1885,6 +2758,13 @@ func WorkspacesListTags(conn *workspaces.WorkSpaces, identifier string) (KeyValu output, err := conn.DescribeTags(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } @@ -1902,6 +2782,13 @@ func XrayListTags(conn *xray.XRay, identifier string) (KeyValueTags, error) { output, err := conn.ListTagsForResource(input) + if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return New(nil), err } diff --git a/aws/internal/keyvaluetags/s3_tags.go b/aws/internal/keyvaluetags/s3_tags.go index a858b48e51c3..34cad2c3d3f0 100644 --- a/aws/internal/keyvaluetags/s3_tags.go +++ b/aws/internal/keyvaluetags/s3_tags.go @@ -1,3 +1,4 @@ +//go:build !generate // +build !generate package keyvaluetags diff --git a/aws/internal/keyvaluetags/s3control_tags.go b/aws/internal/keyvaluetags/s3control_tags.go index 6650d7ad1110..4ef2bbfd95fe 100644 --- a/aws/internal/keyvaluetags/s3control_tags.go +++ b/aws/internal/keyvaluetags/s3control_tags.go @@ -1,3 +1,4 @@ +//go:build !generate // +build !generate package keyvaluetags diff --git a/aws/internal/keyvaluetags/service_generation_customizations.go b/aws/internal/keyvaluetags/service_generation_customizations.go index ba3797f086a1..32aa5b6693a9 100644 --- a/aws/internal/keyvaluetags/service_generation_customizations.go +++ b/aws/internal/keyvaluetags/service_generation_customizations.go @@ -12,7 +12,9 @@ import ( "github.com/aws/aws-sdk-go/service/amplify" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/aws/aws-sdk-go/service/appconfig" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/aws/aws-sdk-go/service/apprunner" "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" @@ -35,6 +37,7 @@ import ( "github.com/aws/aws-sdk-go/service/cognitoidentity" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" "github.com/aws/aws-sdk-go/service/configservice" + "github.com/aws/aws-sdk-go/service/connect" "github.com/aws/aws-sdk-go/service/databasemigrationservice" "github.com/aws/aws-sdk-go/service/dataexchange" "github.com/aws/aws-sdk-go/service/datapipeline" @@ -85,6 +88,7 @@ import ( "github.com/aws/aws-sdk-go/service/mediapackage" "github.com/aws/aws-sdk-go/service/mediastore" "github.com/aws/aws-sdk-go/service/mq" + "github.com/aws/aws-sdk-go/service/mwaa" "github.com/aws/aws-sdk-go/service/neptune" "github.com/aws/aws-sdk-go/service/networkfirewall" "github.com/aws/aws-sdk-go/service/networkmanager" @@ -99,12 +103,15 @@ import ( "github.com/aws/aws-sdk-go/service/resourcegroups" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53recoveryreadiness" "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/aws/aws-sdk-go/service/schemas" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/securityhub" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/aws/aws-sdk-go/service/sfn" + "github.com/aws/aws-sdk-go/service/shield" "github.com/aws/aws-sdk-go/service/signer" "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" @@ -113,6 +120,7 @@ import ( "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/aws/aws-sdk-go/service/swf" "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/aws/aws-sdk-go/service/timestreamwrite" "github.com/aws/aws-sdk-go/service/transfer" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafregional" @@ -141,8 +149,12 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(apigateway.New) case "apigatewayv2": funcType = reflect.TypeOf(apigatewayv2.New) + case "appconfig": + funcType = reflect.TypeOf(appconfig.New) case "appmesh": funcType = reflect.TypeOf(appmesh.New) + case "apprunner": + funcType = reflect.TypeOf(apprunner.New) case "appstream": funcType = reflect.TypeOf(appstream.New) case "appsync": @@ -187,6 +199,8 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(cognitoidentityprovider.New) case "configservice": funcType = reflect.TypeOf(configservice.New) + case "connect": + funcType = reflect.TypeOf(connect.New) case "databasemigrationservice": funcType = reflect.TypeOf(databasemigrationservice.New) case "dataexchange": @@ -287,6 +301,8 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(mediastore.New) case "mq": funcType = reflect.TypeOf(mq.New) + case "mwaa": + funcType = reflect.TypeOf(mwaa.New) case "neptune": funcType = reflect.TypeOf(neptune.New) case "networkfirewall": @@ -315,6 +331,8 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(resourcegroupstaggingapi.New) case "route53": funcType = reflect.TypeOf(route53.New) + case "route53recoveryreadiness": + funcType = reflect.TypeOf(route53recoveryreadiness.New) case "route53resolver": funcType = reflect.TypeOf(route53resolver.New) case "sagemaker": @@ -325,8 +343,12 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(securityhub.New) case "servicediscovery": funcType = reflect.TypeOf(servicediscovery.New) + case "schemas": + funcType = reflect.TypeOf(schemas.New) case "sfn": funcType = reflect.TypeOf(sfn.New) + case "shield": + funcType = reflect.TypeOf(shield.New) case "signer": funcType = reflect.TypeOf(signer.New) case "sns": @@ -343,6 +365,8 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(swf.New) case "synthetics": funcType = reflect.TypeOf(synthetics.New) + case "timestreamwrite": + funcType = reflect.TypeOf(timestreamwrite.New) case "transfer": funcType = reflect.TypeOf(transfer.New) case "waf": @@ -415,12 +439,16 @@ func ServiceListTagsFunction(serviceName string) string { return "ListTags" case "mq": return "ListTags" + case "mwaa": + return "ListTags" case "opsworks": return "ListTags" case "redshift": return "DescribeTags" case "resourcegroups": return "GetTags" + case "route53recoveryreadiness": + return "ListTagsForResources" case "sagemaker": return "ListTags" case "sqs": @@ -461,7 +489,7 @@ func ServiceListTagsInputIdentifierField(serviceName string) string { } } -// ServiceListTagInputIdentifierRequiresSlice determines if the service list tagging resource field requires a slice. +// ServiceListTagsInputIdentifierRequiresSlice determines if the service list tagging resource field requires a slice. func ServiceListTagsInputIdentifierRequiresSlice(serviceName string) string { switch serviceName { case "cloudtrail": @@ -529,22 +557,45 @@ func ServiceListTagsOutputTagsField(serviceName string) string { } } -// ServiceResourceNotFoundErrorCode determines the error code of tagable resources when not found -func ServiceResourceNotFoundErrorCode(serviceName string) string { +// ServiceParentResourceNotFoundError determines additional NotFoundError handling for missing parent resources. +// Use this to ignore errors returned by the create tags and list tags APIs for missing parent resources. +// +// This handling should be in the form of: +// if CONDITIONAL { +// err = &resource.NotFoundError{ +// LastError: err, +// LastRequest: input, +// } +// } +func ServiceParentResourceNotFoundError(serviceName string) string { switch serviceName { - default: - return "ResourceNotFoundException" + case "ec2": + return ` +if tfawserr.ErrCodeContains(err, ".NotFound") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, } } - -// ServiceResourceNotFoundErrorCode determines the common substring of error codes of tagable resources when not found -// This value takes precedence over ServiceResourceNotFoundErrorCode when defined for a service. -func ServiceResourceNotFoundErrorCodeContains(serviceName string) string { - switch serviceName { - case "ec2": - return ".NotFound" +` + case "ecs": + return ` +if tfawserr.ErrMessageContains(err, "InvalidParameterException", "The specified cluster is inactive. Specify an active cluster and try again.") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } +} +` default: - return "" + return ` +if tfawserr.ErrCodeEquals(err, "ResourceNotFoundException") { + err = &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } +} +` } } @@ -618,6 +669,8 @@ func ServiceTagFunction(serviceName string) string { return "ChangeTagsForResource" case "sagemaker": return "AddTags" + case "shield": + return "TagResource" case "sqs": return "TagQueue" case "ssm": @@ -734,12 +787,16 @@ func ServiceTagInputIdentifierField(serviceName string) string { return "SecretId" case "servicediscovery": return "ResourceARN" + case "shield": + return "ResourceARN" case "sqs": return "QueueUrl" case "ssm": return "ResourceId" case "storagegateway": return "ResourceARN" + case "timestreamwrite": + return "ResourceARN" case "transfer": return "Arn" case "waf": diff --git a/aws/internal/keyvaluetags/service_tags_gen.go b/aws/internal/keyvaluetags/service_tags_gen.go index 8e1d2f68f6d1..4f6e0fce614e 100644 --- a/aws/internal/keyvaluetags/service_tags_gen.go +++ b/aws/internal/keyvaluetags/service_tags_gen.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/aws/aws-sdk-go/service/apprunner" "github.com/aws/aws-sdk-go/service/athena" "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloud9" @@ -79,11 +80,13 @@ import ( "github.com/aws/aws-sdk-go/service/servicecatalog" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/aws/aws-sdk-go/service/sfn" + "github.com/aws/aws-sdk-go/service/shield" "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssoadmin" "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/aws/aws-sdk-go/service/swf" + "github.com/aws/aws-sdk-go/service/timestreamwrite" "github.com/aws/aws-sdk-go/service/transfer" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafv2" @@ -134,6 +137,16 @@ func Apigatewayv2KeyValueTags(tags map[string]*string) KeyValueTags { return New(tags) } +// AppconfigTags returns appconfig service tags. +func (tags KeyValueTags) AppconfigTags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// AppconfigKeyValueTags creates KeyValueTags from appconfig service tags. +func AppconfigKeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + // AppstreamTags returns appstream service tags. func (tags KeyValueTags) AppstreamTags() map[string]*string { return aws.StringMap(tags.Map()) @@ -224,6 +237,16 @@ func CognitoidentityproviderKeyValueTags(tags map[string]*string) KeyValueTags { return New(tags) } +// ConnectTags returns connect service tags. +func (tags KeyValueTags) ConnectTags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// ConnectKeyValueTags creates KeyValueTags from connect service tags. +func ConnectKeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + // DataexchangeTags returns dataexchange service tags. func (tags KeyValueTags) DataexchangeTags() map[string]*string { return aws.StringMap(tags.Map()) @@ -334,6 +357,16 @@ func LambdaKeyValueTags(tags map[string]*string) KeyValueTags { return New(tags) } +// Macie2Tags returns macie2 service tags. +func (tags KeyValueTags) Macie2Tags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// Macie2KeyValueTags creates KeyValueTags from macie2 service tags. +func Macie2KeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + // MediaconnectTags returns mediaconnect service tags. func (tags KeyValueTags) MediaconnectTags() map[string]*string { return aws.StringMap(tags.Map()) @@ -384,6 +417,16 @@ func MqKeyValueTags(tags map[string]*string) KeyValueTags { return New(tags) } +// MwaaTags returns mwaa service tags. +func (tags KeyValueTags) MwaaTags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// MwaaKeyValueTags creates KeyValueTags from mwaa service tags. +func MwaaKeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + // OpsworksTags returns opsworks service tags. func (tags KeyValueTags) OpsworksTags() map[string]*string { return aws.StringMap(tags.Map()) @@ -424,6 +467,26 @@ func ResourcegroupsKeyValueTags(tags map[string]*string) KeyValueTags { return New(tags) } +// Route53recoveryreadinessTags returns route53recoveryreadiness service tags. +func (tags KeyValueTags) Route53recoveryreadinessTags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// Route53recoveryreadinessKeyValueTags creates KeyValueTags from route53recoveryreadiness service tags. +func Route53recoveryreadinessKeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + +// SchemasTags returns schemas service tags. +func (tags KeyValueTags) SchemasTags() map[string]*string { + return aws.StringMap(tags.Map()) +} + +// SchemasKeyValueTags creates KeyValueTags from schemas service tags. +func SchemasKeyValueTags(tags map[string]*string) KeyValueTags { + return New(tags) +} + // SecurityhubTags returns securityhub service tags. func (tags KeyValueTags) SecurityhubTags() map[string]*string { return aws.StringMap(tags.Map()) @@ -557,6 +620,33 @@ func AppmeshKeyValueTags(tags []*appmesh.TagRef) KeyValueTags { return New(m) } +// ApprunnerTags returns apprunner service tags. +func (tags KeyValueTags) ApprunnerTags() []*apprunner.Tag { + result := make([]*apprunner.Tag, 0, len(tags)) + + for k, v := range tags.Map() { + tag := &apprunner.Tag{ + Key: aws.String(k), + Value: aws.String(v), + } + + result = append(result, tag) + } + + return result +} + +// ApprunnerKeyValueTags creates KeyValueTags from apprunner service tags. +func ApprunnerKeyValueTags(tags []*apprunner.Tag) KeyValueTags { + m := make(map[string]*string, len(tags)) + + for _, tag := range tags { + m[aws.StringValue(tag.Key)] = tag.Value + } + + return New(m) +} + // AthenaTags returns athena service tags. func (tags KeyValueTags) AthenaTags() []*athena.Tag { result := make([]*athena.Tag, 0, len(tags)) @@ -2607,6 +2697,33 @@ func SfnKeyValueTags(tags []*sfn.Tag) KeyValueTags { return New(m) } +// ShieldTags returns shield service tags. +func (tags KeyValueTags) ShieldTags() []*shield.Tag { + result := make([]*shield.Tag, 0, len(tags)) + + for k, v := range tags.Map() { + tag := &shield.Tag{ + Key: aws.String(k), + Value: aws.String(v), + } + + result = append(result, tag) + } + + return result +} + +// ShieldKeyValueTags creates KeyValueTags from shield service tags. +func ShieldKeyValueTags(tags []*shield.Tag) KeyValueTags { + m := make(map[string]*string, len(tags)) + + for _, tag := range tags { + m[aws.StringValue(tag.Key)] = tag.Value + } + + return New(m) +} + // SnsTags returns sns service tags. func (tags KeyValueTags) SnsTags() []*sns.Tag { result := make([]*sns.Tag, 0, len(tags)) @@ -2742,6 +2859,33 @@ func SwfKeyValueTags(tags []*swf.ResourceTag) KeyValueTags { return New(m) } +// TimestreamwriteTags returns timestreamwrite service tags. +func (tags KeyValueTags) TimestreamwriteTags() []*timestreamwrite.Tag { + result := make([]*timestreamwrite.Tag, 0, len(tags)) + + for k, v := range tags.Map() { + tag := ×treamwrite.Tag{ + Key: aws.String(k), + Value: aws.String(v), + } + + result = append(result, tag) + } + + return result +} + +// TimestreamwriteKeyValueTags creates KeyValueTags from timestreamwrite service tags. +func TimestreamwriteKeyValueTags(tags []*timestreamwrite.Tag) KeyValueTags { + m := make(map[string]*string, len(tags)) + + for _, tag := range tags { + m[aws.StringValue(tag.Key)] = tag.Value + } + + return New(m) +} + // TransferTags returns transfer service tags. func (tags KeyValueTags) TransferTags() []*transfer.Tag { result := make([]*transfer.Tag, 0, len(tags)) diff --git a/aws/internal/keyvaluetags/servicecatalog_tags.go b/aws/internal/keyvaluetags/servicecatalog_tags.go new file mode 100644 index 000000000000..ad378b622e27 --- /dev/null +++ b/aws/internal/keyvaluetags/servicecatalog_tags.go @@ -0,0 +1,73 @@ +//go:build !generate +// +build !generate + +package keyvaluetags + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" +) + +// Custom Service Catalog tag service update functions using the same format as generated code. + +func ServiceCatalogPortfolioUpdateTags(conn *servicecatalog.ServiceCatalog, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + input := &servicecatalog.UpdatePortfolioInput{ + Id: aws.String(identifier), + } + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input.RemoveTags = aws.StringSlice(removedTags.IgnoreAws().Keys()) + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input.AddTags = updatedTags.IgnoreAws().ServicecatalogTags() + } + + _, err := conn.UpdatePortfolio(input) + + if err != nil { + return fmt.Errorf("error updating tags for Service Catalog Product (%s): %w", identifier, err) + } + + return nil +} + +func ServiceCatalogProductUpdateTags(conn *servicecatalog.ServiceCatalog, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + input := &servicecatalog.UpdateProductInput{ + Id: aws.String(identifier), + } + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input.RemoveTags = aws.StringSlice(removedTags.IgnoreAws().Keys()) + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input.AddTags = updatedTags.IgnoreAws().ServicecatalogTags() + } + + _, err := conn.UpdateProduct(input) + + if err != nil { + return fmt.Errorf("error updating tags for Service Catalog Product (%s): %w", identifier, err) + } + + return nil +} + +func ServicecatalogRecordKeyValueTags(tags []*servicecatalog.RecordTag) KeyValueTags { + m := make(map[string]*string, len(tags)) + + for _, tag := range tags { + m[aws.StringValue(tag.Key)] = tag.Value + } + + return New(m) +} diff --git a/aws/internal/keyvaluetags/update_tags_gen.go b/aws/internal/keyvaluetags/update_tags_gen.go index 62ee4ea9faef..68117b26dffc 100644 --- a/aws/internal/keyvaluetags/update_tags_gen.go +++ b/aws/internal/keyvaluetags/update_tags_gen.go @@ -12,7 +12,9 @@ import ( "github.com/aws/aws-sdk-go/service/amplify" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/aws/aws-sdk-go/service/appconfig" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/aws/aws-sdk-go/service/apprunner" "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" @@ -35,6 +37,7 @@ import ( "github.com/aws/aws-sdk-go/service/cognitoidentity" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" "github.com/aws/aws-sdk-go/service/configservice" + "github.com/aws/aws-sdk-go/service/connect" "github.com/aws/aws-sdk-go/service/databasemigrationservice" "github.com/aws/aws-sdk-go/service/dataexchange" "github.com/aws/aws-sdk-go/service/datapipeline" @@ -84,6 +87,7 @@ import ( "github.com/aws/aws-sdk-go/service/mediapackage" "github.com/aws/aws-sdk-go/service/mediastore" "github.com/aws/aws-sdk-go/service/mq" + "github.com/aws/aws-sdk-go/service/mwaa" "github.com/aws/aws-sdk-go/service/neptune" "github.com/aws/aws-sdk-go/service/networkfirewall" "github.com/aws/aws-sdk-go/service/networkmanager" @@ -97,12 +101,15 @@ import ( "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/resourcegroups" "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53recoveryreadiness" "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/aws/aws-sdk-go/service/schemas" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/securityhub" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/aws/aws-sdk-go/service/sfn" + "github.com/aws/aws-sdk-go/service/shield" "github.com/aws/aws-sdk-go/service/signer" "github.com/aws/aws-sdk-go/service/sns" "github.com/aws/aws-sdk-go/service/sqs" @@ -111,6 +118,7 @@ import ( "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/aws/aws-sdk-go/service/swf" "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/aws/aws-sdk-go/service/timestreamwrite" "github.com/aws/aws-sdk-go/service/transfer" "github.com/aws/aws-sdk-go/service/waf" "github.com/aws/aws-sdk-go/service/wafregional" @@ -336,6 +344,42 @@ func Apigatewayv2UpdateTags(conn *apigatewayv2.ApiGatewayV2, identifier string, return nil } +// AppconfigUpdateTags updates appconfig service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func AppconfigUpdateTags(conn *appconfig.AppConfig, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &appconfig.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &appconfig.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().AppconfigTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // AppmeshUpdateTags updates appmesh service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -372,6 +416,42 @@ func AppmeshUpdateTags(conn *appmesh.AppMesh, identifier string, oldTagsMap inte return nil } +// ApprunnerUpdateTags updates apprunner service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ApprunnerUpdateTags(conn *apprunner.AppRunner, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &apprunner.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &apprunner.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().ApprunnerTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // AppstreamUpdateTags updates appstream service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -1162,6 +1242,42 @@ func ConfigserviceUpdateTags(conn *configservice.ConfigService, identifier strin return nil } +// ConnectUpdateTags updates connect service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ConnectUpdateTags(conn *connect.Connect, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &connect.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &connect.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().ConnectTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // DatabasemigrationserviceUpdateTags updates databasemigrationservice service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -2929,6 +3045,42 @@ func MqUpdateTags(conn *mq.MQ, identifier string, oldTagsMap interface{}, newTag return nil } +// MwaaUpdateTags updates mwaa service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func MwaaUpdateTags(conn *mwaa.MWAA, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &mwaa.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &mwaa.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().MwaaTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // NeptuneUpdateTags updates neptune service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -3397,6 +3549,42 @@ func Route53UpdateTags(conn *route53.Route53, identifier string, resourceType st return nil } +// Route53recoveryreadinessUpdateTags updates route53recoveryreadiness service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func Route53recoveryreadinessUpdateTags(conn *route53recoveryreadiness.Route53RecoveryReadiness, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &route53recoveryreadiness.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &route53recoveryreadiness.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().Route53recoveryreadinessTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // Route53resolverUpdateTags updates route53resolver service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -3469,6 +3657,42 @@ func SagemakerUpdateTags(conn *sagemaker.SageMaker, identifier string, oldTagsMa return nil } +// SchemasUpdateTags updates schemas service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func SchemasUpdateTags(conn *schemas.Schemas, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &schemas.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &schemas.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().SchemasTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // SecretsmanagerUpdateTags updates secretsmanager service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -3613,6 +3837,42 @@ func SfnUpdateTags(conn *sfn.SFN, identifier string, oldTagsMap interface{}, new return nil } +// ShieldUpdateTags updates shield service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ShieldUpdateTags(conn *shield.Shield, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &shield.UntagResourceInput{ + ResourceARN: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &shield.TagResourceInput{ + ResourceARN: aws.String(identifier), + Tags: updatedTags.IgnoreAws().ShieldTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // SignerUpdateTags updates signer service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. @@ -3905,6 +4165,42 @@ func SyntheticsUpdateTags(conn *synthetics.Synthetics, identifier string, oldTag return nil } +// TimestreamwriteUpdateTags updates timestreamwrite service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func TimestreamwriteUpdateTags(conn *timestreamwrite.TimestreamWrite, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := ×treamwrite.UntagResourceInput{ + ResourceARN: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAws().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := ×treamwrite.TagResourceInput{ + ResourceARN: aws.String(identifier), + Tags: updatedTags.IgnoreAws().TimestreamwriteTags(), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // TransferUpdateTags updates transfer service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. diff --git a/aws/internal/namevaluesfilters/README.md b/aws/internal/namevaluesfilters/README.md new file mode 100644 index 000000000000..917459e50ced --- /dev/null +++ b/aws/internal/namevaluesfilters/README.md @@ -0,0 +1,24 @@ +# namevaluesfilters + +The `namevaluesfilters` package is designed to provide a consistent interface for handling AWS resource filtering. + +This package implements a single `NameValuesFilters` type, which covers all filter handling logic, such as merging filters, via functions on the single type. The underlying implementation is compatible with Go operations such as `len()`. + +Full documentation for this package can be found on [GoDoc](https://godoc.org/github.com/terraform-providers/terraform-provider-aws/aws/internal/namevaluesfilters). + +Many AWS Go SDK services that support resource filtering have their service-specific Go type conversion functions to and from `NameValuesFilters` code generated. Converting from `NameValuesFilters` to AWS Go SDK types is done via `{SERVICE}Filters()` functions on the type. For more information about this code generation, see the [`generators/servicefilters` README](generators/servicefilters/README.md). + +Any filtering functions that cannot be generated should be hand implemented in a service-specific source file (e.g. `ec2_filters.go`) and follow the format of similar generated code wherever possible. The first line of the source file should be `// +build !generate`. This prevents the file's inclusion during the code generation phase. + +## Code Structure + +```text +aws/internal/namevaluesfilters +├── generators +│ └── servicefilters (generates service_filters_gen.go) +├── name_values_filters_test.go (unit tests for core logic) +├── name_values_filters.go (core logic) +├── service_generation_customizations.go (shared AWS Go SDK service customizations for generators) +├── service_filters_gen.go (generated AWS Go SDK service conversion functions) +└── _filters.go (any service-specific functions that cannot be generated) +``` diff --git a/aws/internal/namevaluesfilters/ec2_filters.go b/aws/internal/namevaluesfilters/ec2_filters.go new file mode 100644 index 000000000000..b6a6628ba25b --- /dev/null +++ b/aws/internal/namevaluesfilters/ec2_filters.go @@ -0,0 +1,21 @@ +//go:build !generate +// +build !generate + +package namevaluesfilters + +import ( + "fmt" +) + +// Custom EC2 filter functions. + +// Ec2Tags creates NameValuesFilters from a map of keyvalue tags. +func Ec2Tags(tags map[string]string) NameValuesFilters { + m := make(map[string]string, len(tags)) + + for k, v := range tags { + m[fmt.Sprintf("tag:%s", k)] = v + } + + return New(m) +} diff --git a/aws/internal/namevaluesfilters/ec2_filters_test.go b/aws/internal/namevaluesfilters/ec2_filters_test.go new file mode 100644 index 000000000000..4f6880c2d729 --- /dev/null +++ b/aws/internal/namevaluesfilters/ec2_filters_test.go @@ -0,0 +1,43 @@ +package namevaluesfilters + +import ( + "testing" +) + +func TestNameValuesFiltersEc2Tags(t *testing.T) { + testCases := []struct { + name string + filters NameValuesFilters + want map[string][]string + }{ + { + name: "nil", + filters: Ec2Tags(nil), + want: map[string][]string{}, + }, + { + name: "nil", + filters: Ec2Tags(map[string]string{}), + want: map[string][]string{}, + }, + { + name: "tags", + filters: Ec2Tags(map[string]string{ + "Name": "tf-acc-test", + "Purpose": "testing", + }), + want: map[string][]string{ + "tag:Name": {"tf-acc-test"}, + "tag:Purpose": {"testing"}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.filters.Map() + + testNameValuesFiltersVerifyMap(t, got, testCase.want) + }) + } +} diff --git a/aws/internal/namevaluesfilters/generators/servicefilters/README.md b/aws/internal/namevaluesfilters/generators/servicefilters/README.md new file mode 100644 index 000000000000..fc6680660a67 --- /dev/null +++ b/aws/internal/namevaluesfilters/generators/servicefilters/README.md @@ -0,0 +1,42 @@ +# servicefilters + +This package contains a code generator to consistently handle the various AWS Go SDK service implementations for converting service filter types to/from `NameValuesFilters`. Not all AWS Go SDK services that support filters are generated in this manner. + +To run this code generator, execute `go generate ./...` from the root of the repository. The general workflow for the generator is: + +- Generate Go file contents via template from local variables and functions +- Go format file contents +- Write file contents to `service_filters_gen.go` file + +## Example Output + +```go +// DocdbFilters returns docdb service filters. +func (filters NameValuesFilters) DocdbFilters() []*docdb.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*docdb.Filter, 0, len(m)) + + for k, v := range m { + filter := &docdb.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} +``` + +## Implementing a New Generated Service + +- In `main.go`: Add service name, e.g. `docdb`, to one of the implementation handlers + - Use `sliceServiceNames` if the AWS Go SDK service implements a specific Go type such as `Filter` +- Run `go generate ./...` (or `make gen`) from the root of the repository to regenerate the code +- Run `go test ./...` (or `make test`) from the root of the repository to ensure the generated code compiles diff --git a/aws/internal/namevaluesfilters/generators/servicefilters/main.go b/aws/internal/namevaluesfilters/generators/servicefilters/main.go new file mode 100644 index 000000000000..072dc9e3e289 --- /dev/null +++ b/aws/internal/namevaluesfilters/generators/servicefilters/main.go @@ -0,0 +1,129 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "bytes" + "go/format" + "log" + "os" + "sort" + "strings" + "text/template" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/namevaluesfilters" +) + +const filename = `service_filters_gen.go` + +// Representing types such as []*ec2.Filter, []*rds.Filter, ... +var sliceServiceNames = []string{ + "autoscaling", + "databasemigrationservice", + "docdb", + "ec2", + "elasticinference", + "elasticsearchservice", + "fsx", + "imagebuilder", + "licensemanager", + "neptune", + "rds", + "resourcegroupstaggingapi", + "route53resolver", +} + +type TemplateData struct { + SliceServiceNames []string +} + +func main() { + // Always sort to reduce any potential generation churn + sort.Strings(sliceServiceNames) + + templateData := TemplateData{ + SliceServiceNames: sliceServiceNames, + } + templateFuncMap := template.FuncMap{ + "FilterPackage": namevaluesfilters.ServiceFilterPackage, + "FilterType": namevaluesfilters.ServiceFilterType, + "FilterTypeNameField": namevaluesfilters.ServiceFilterTypeNameField, + "FilterTypeValuesField": namevaluesfilters.ServiceFilterTypeValuesField, + "Title": strings.Title, + } + + tmpl, err := template.New("servicefilters").Funcs(templateFuncMap).Parse(templateBody) + + if err != nil { + log.Fatalf("error parsing template: %s", err) + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateData) + + if err != nil { + log.Fatalf("error executing template: %s", err) + } + + generatedFileContents, err := format.Source(buffer.Bytes()) + + if err != nil { + log.Fatalf("error formatting generated file: %s", err) + } + + f, err := os.Create(filename) + + if err != nil { + log.Fatalf("error creating file (%s): %s", filename, err) + } + + defer f.Close() + + _, err = f.Write(generatedFileContents) + + if err != nil { + log.Fatalf("error writing to file (%s): %s", filename, err) + } +} + +var templateBody = ` +// Code generated by generators/servicefilters/main.go; DO NOT EDIT. + +package namevaluesfilters + +import ( + "github.com/aws/aws-sdk-go/aws" +{{- range .SliceServiceNames }} +{{- if eq . (. | FilterPackage) }} + "github.com/aws/aws-sdk-go/service/{{ . }}" +{{- end }} +{{- end }} +) + +// []*SERVICE.Filter handling +{{- range .SliceServiceNames }} + +// {{ . | Title }}Filters returns {{ . }} service filters. +func (filters NameValuesFilters) {{ . | Title }}Filters() []*{{ . | FilterPackage }}.{{ . | FilterType }} { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*{{ . | FilterPackage }}.{{ . | FilterType }}, 0, len(m)) + + for k, v := range m { + filter := &{{ . | FilterPackage }}.{{ . | FilterType }}{ + {{ . | FilterTypeNameField }}: aws.String(k), + {{ . | FilterTypeValuesField }}: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} +{{- end }} +` diff --git a/aws/internal/namevaluesfilters/name_values_filters.go b/aws/internal/namevaluesfilters/name_values_filters.go new file mode 100644 index 000000000000..90b8aec4b6a3 --- /dev/null +++ b/aws/internal/namevaluesfilters/name_values_filters.go @@ -0,0 +1,127 @@ +//go:generate go run -tags generate generators/servicefilters/main.go + +package namevaluesfilters + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// NameValuesFilters is a standard implementation for AWS resource filters. +// The AWS Go SDK is split into multiple service packages, each service with +// its own Go struct type representing a resource filter. To standardize logic +// across all these Go types, we convert them into this Go type. +type NameValuesFilters map[string][]string + +// Add adds missing and updates existing filters from common Terraform Provider SDK types. +// Supports map[string]string, map[string][]string, *schema.Set. +func (filters NameValuesFilters) Add(i interface{}) NameValuesFilters { + switch value := i.(type) { + case map[string]string: + for name, v := range value { + if values, ok := filters[name]; ok { + filters[name] = append(values, v) + } else { + values = []string{v} + filters[name] = values + } + } + + case map[string][]string: + // We can't use fallthrough here, so recurse. + return filters.Add(NameValuesFilters(value)) + + case NameValuesFilters: + for name, vs := range value { + if values, ok := filters[name]; ok { + filters[name] = append(values, vs...) + } else { + values = make([]string, len(vs)) + copy(values, vs) + filters[name] = values + } + } + + case *schema.Set: + // The set of filters described by Schema(). + for _, filter := range value.List() { + m := filter.(map[string]interface{}) + name := m["name"].(string) + + for _, v := range m["values"].(*schema.Set).List() { + if values, ok := filters[name]; ok { + filters[name] = append(values, v.(string)) + } else { + values = []string{v.(string)} + filters[name] = values + } + } + } + } + + return filters +} + +// Map returns filter names mapped to their values. +// Duplicate values are eliminated and empty values removed. +func (filters NameValuesFilters) Map() map[string][]string { + result := make(map[string][]string) + + for k, v := range filters { + targetValues := make([]string, 0) + + SOURCE_VALUES: + for _, sourceValue := range v { + if sourceValue == "" { + continue + } + + for _, targetValue := range targetValues { + if sourceValue == targetValue { + continue SOURCE_VALUES + } + } + + targetValues = append(targetValues, sourceValue) + } + + if len(targetValues) == 0 { + continue + } + + result[k] = targetValues + } + + return result +} + +// New creates NameValuesFilters from common Terraform Provider SDK types. +// Supports map[string]string, map[string][]string, *schema.Set. +func New(i interface{}) NameValuesFilters { + return make(NameValuesFilters).Add(i) +} + +// Schema returns a *schema.Schema that represents a set of custom filtering criteria +// that a user can specify as input to a data source. +// It is conventional for an attribute of this type to be included as a top-level attribute called "filter". +func Schema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "values": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + } +} diff --git a/aws/internal/namevaluesfilters/name_values_filters_test.go b/aws/internal/namevaluesfilters/name_values_filters_test.go new file mode 100644 index 000000000000..d0a28233a1e8 --- /dev/null +++ b/aws/internal/namevaluesfilters/name_values_filters_test.go @@ -0,0 +1,170 @@ +package namevaluesfilters + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" +) + +func TestNameValuesFiltersMap(t *testing.T) { + testCases := []struct { + name string + filters NameValuesFilters + want map[string][]string + }{ + { + name: "empty", + filters: New(map[string][]string{}), + want: map[string][]string{}, + }, + { + name: "empty_strings", + filters: New(map[string][]string{ + "name1": {""}, + "name2": {"", ""}, + }), + want: map[string][]string{}, + }, + { + name: "duplicates", + filters: New(map[string][]string{ + "name1": {"value1"}, + "name2": {"value2a", "value2b", "", "value2a", "value2c", "value2c"}, + }), + want: map[string][]string{ + "name1": {"value1"}, + "name2": {"value2a", "value2b", "value2c"}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.filters.Map() + + testNameValuesFiltersVerifyMap(t, got, testCase.want) + }) + } +} + +func TestNameValuesFiltersAdd(t *testing.T) { + testCases := []struct { + name string + filters NameValuesFilters + add interface{} + want map[string][]string + }{ + { + name: "empty", + filters: New(map[string][]string{}), + add: nil, + want: map[string][]string{}, + }, + { + name: "add_all", + filters: New(map[string]string{ + "name1": "value1", + "name2": "value2", + "name3": "value3", + }), + add: New(map[string][]string{ + "name4": {"value4a", "value4b"}, + "name5": {"value5"}, + "name6": {"value6a", "value6b", "value6c"}, + }), + want: map[string][]string{ + "name1": {"value1"}, + "name2": {"value2"}, + "name3": {"value3"}, + "name4": {"value4a", "value4b"}, + "name5": {"value5"}, + "name6": {"value6a", "value6b", "value6c"}, + }, + }, + { + name: "mixed", + filters: New(map[string][]string{ + "name1": {"value1a"}, + "name2": {"value2a", "value2b"}, + }), + add: map[string]string{ + "name1": "value1b", + "name3": "value3", + }, + want: map[string][]string{ + "name1": {"value1a", "value1b"}, + "name2": {"value2a", "value2b"}, + "name3": {"value3"}, + }, + }, + { + name: "from_set", + filters: New(schema.NewSet(testNameValuesFiltersHashSet, []interface{}{ + map[string]interface{}{ + "name": "name1", + "values": schema.NewSet(schema.HashString, []interface{}{ + "value1", + }), + }, + map[string]interface{}{ + "name": "name2", + "values": schema.NewSet(schema.HashString, []interface{}{ + "value2a", + "value2b", + }), + }, + map[string]interface{}{ + "name": "name3", + "values": schema.NewSet(schema.HashString, []interface{}{ + "value3", + }), + }, + })), + add: map[string][]string{ + "name1": {"value1"}, + "name2": {"value2c"}, + }, + want: map[string][]string{ + "name1": {"value1"}, + "name2": {"value2a", "value2b", "value2c"}, + "name3": {"value3"}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.filters.Add(testCase.add) + + testNameValuesFiltersVerifyMap(t, got.Map(), testCase.want) + }) + } +} + +func testNameValuesFiltersVerifyMap(t *testing.T, got map[string][]string, want map[string][]string) { + for k, wantV := range want { + gotV, ok := got[k] + + if !ok { + t.Errorf("want missing name: %s", k) + continue + } + + if !reflect.DeepEqual(gotV, wantV) { + t.Errorf("got name (%s) values %s; want values %s", k, gotV, wantV) + } + } + + for k := range got { + if _, ok := want[k]; !ok { + t.Errorf("got extra name: %s", k) + } + } +} + +func testNameValuesFiltersHashSet(v interface{}) int { + m := v.(map[string]interface{}) + return hashcode.String(m["name"].(string)) +} diff --git a/aws/internal/namevaluesfilters/service_filters_gen.go b/aws/internal/namevaluesfilters/service_filters_gen.go new file mode 100644 index 000000000000..b43aa0cae19e --- /dev/null +++ b/aws/internal/namevaluesfilters/service_filters_gen.go @@ -0,0 +1,308 @@ +// Code generated by generators/servicefilters/main.go; DO NOT EDIT. + +package namevaluesfilters + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/databasemigrationservice" + "github.com/aws/aws-sdk-go/service/docdb" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/elasticinference" + "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/aws/aws-sdk-go/service/imagebuilder" + "github.com/aws/aws-sdk-go/service/licensemanager" + "github.com/aws/aws-sdk-go/service/neptune" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" + "github.com/aws/aws-sdk-go/service/route53resolver" +) + +// []*SERVICE.Filter handling + +// AutoscalingFilters returns autoscaling service filters. +func (filters NameValuesFilters) AutoscalingFilters() []*autoscaling.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*autoscaling.Filter, 0, len(m)) + + for k, v := range m { + filter := &autoscaling.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// DatabasemigrationserviceFilters returns databasemigrationservice service filters. +func (filters NameValuesFilters) DatabasemigrationserviceFilters() []*databasemigrationservice.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*databasemigrationservice.Filter, 0, len(m)) + + for k, v := range m { + filter := &databasemigrationservice.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// DocdbFilters returns docdb service filters. +func (filters NameValuesFilters) DocdbFilters() []*docdb.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*docdb.Filter, 0, len(m)) + + for k, v := range m { + filter := &docdb.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// Ec2Filters returns ec2 service filters. +func (filters NameValuesFilters) Ec2Filters() []*ec2.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*ec2.Filter, 0, len(m)) + + for k, v := range m { + filter := &ec2.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// ElasticinferenceFilters returns elasticinference service filters. +func (filters NameValuesFilters) ElasticinferenceFilters() []*elasticinference.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*elasticinference.Filter, 0, len(m)) + + for k, v := range m { + filter := &elasticinference.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// ElasticsearchserviceFilters returns elasticsearchservice service filters. +func (filters NameValuesFilters) ElasticsearchserviceFilters() []*elasticsearchservice.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*elasticsearchservice.Filter, 0, len(m)) + + for k, v := range m { + filter := &elasticsearchservice.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// FsxFilters returns fsx service filters. +func (filters NameValuesFilters) FsxFilters() []*fsx.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*fsx.Filter, 0, len(m)) + + for k, v := range m { + filter := &fsx.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// ImagebuilderFilters returns imagebuilder service filters. +func (filters NameValuesFilters) ImagebuilderFilters() []*imagebuilder.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*imagebuilder.Filter, 0, len(m)) + + for k, v := range m { + filter := &imagebuilder.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// LicensemanagerFilters returns licensemanager service filters. +func (filters NameValuesFilters) LicensemanagerFilters() []*licensemanager.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*licensemanager.Filter, 0, len(m)) + + for k, v := range m { + filter := &licensemanager.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// NeptuneFilters returns neptune service filters. +func (filters NameValuesFilters) NeptuneFilters() []*neptune.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*neptune.Filter, 0, len(m)) + + for k, v := range m { + filter := &neptune.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// RdsFilters returns rds service filters. +func (filters NameValuesFilters) RdsFilters() []*rds.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*rds.Filter, 0, len(m)) + + for k, v := range m { + filter := &rds.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// ResourcegroupstaggingapiFilters returns resourcegroupstaggingapi service filters. +func (filters NameValuesFilters) ResourcegroupstaggingapiFilters() []*resourcegroupstaggingapi.TagFilter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*resourcegroupstaggingapi.TagFilter, 0, len(m)) + + for k, v := range m { + filter := &resourcegroupstaggingapi.TagFilter{ + Key: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} + +// Route53resolverFilters returns route53resolver service filters. +func (filters NameValuesFilters) Route53resolverFilters() []*route53resolver.Filter { + m := filters.Map() + + if len(m) == 0 { + return nil + } + + result := make([]*route53resolver.Filter, 0, len(m)) + + for k, v := range m { + filter := &route53resolver.Filter{ + Name: aws.String(k), + Values: aws.StringSlice(v), + } + + result = append(result, filter) + } + + return result +} diff --git a/aws/internal/namevaluesfilters/service_generation_customizations.go b/aws/internal/namevaluesfilters/service_generation_customizations.go new file mode 100644 index 000000000000..ec295cd1c951 --- /dev/null +++ b/aws/internal/namevaluesfilters/service_generation_customizations.go @@ -0,0 +1,39 @@ +// This file contains code generation customizations for each AWS Go SDK service. + +package namevaluesfilters + +// ServiceFilterPackage determines the service filter type package. +func ServiceFilterPackage(serviceName string) string { + switch serviceName { + default: + return serviceName + } +} + +// ServiceFilterType determines the service filter type. +func ServiceFilterType(serviceName string) string { + switch serviceName { + case "resourcegroupstaggingapi": + return "TagFilter" + default: + return "Filter" + } +} + +// ServiceFilterTypeNameField determines the service filter type name field. +func ServiceFilterTypeNameField(serviceName string) string { + switch serviceName { + case "resourcegroupstaggingapi": + return "Key" + default: + return "Name" + } +} + +// ServiceFilterTypeValuesField determines the service filter type values field. +func ServiceFilterTypeValuesField(serviceName string) string { + switch serviceName { + default: + return "Values" + } +} diff --git a/aws/internal/naming/naming.go b/aws/internal/naming/naming.go index 38425a507de8..96cf38f96c43 100644 --- a/aws/internal/naming/naming.go +++ b/aws/internal/naming/naming.go @@ -8,28 +8,33 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -var resourceUniqueIDSuffixRegexpPattern = fmt.Sprintf("[[:xdigit:]]{%d}$", resource.UniqueIDSuffixLength) -var resourceUniqueIDSuffixRegexp = regexp.MustCompile(resourceUniqueIDSuffixRegexpPattern) - -var resourceUniqueIDRegexpPattern = resourcePrefixedUniqueIDRegexpPattern(resource.UniqueIdPrefix) -var resourceUniqueIDRegexp = regexp.MustCompile(resourceUniqueIDRegexpPattern) - // Generate returns in order the name if non-empty, a prefix generated name if non-empty, or fully generated name prefixed with terraform- func Generate(name string, namePrefix string) string { + return GenerateWithSuffix(name, namePrefix, "") +} + +// GenerateWithSuffix returns in order the name if non-empty, a prefix generated name if non-empty, or fully generated name prefixed with "terraform-". +// In the latter two cases, any suffix is appended to the generated name +func GenerateWithSuffix(name string, namePrefix string, nameSuffix string) string { if name != "" { return name } if namePrefix != "" { - return resource.PrefixedUniqueId(namePrefix) + return resource.PrefixedUniqueId(namePrefix) + nameSuffix } - return resource.UniqueId() + return resource.UniqueId() + nameSuffix } // HasResourceUniqueIdSuffix returns true if the string has the built-in unique ID suffix func HasResourceUniqueIdSuffix(s string) bool { - return resourceUniqueIDSuffixRegexp.MatchString(s) + return HasResourceUniqueIdPlusAdditionalSuffix(s, "") +} + +// HasResourceUniqueIdPlusAdditionalSuffix returns true if the string has the built-in unique ID suffix plus an additional suffix +func HasResourceUniqueIdPlusAdditionalSuffix(s string, additionalSuffix string) bool { + return resourceUniqueIDPlusAdditionalSuffixRegexp(additionalSuffix).MatchString(s) } // NamePrefixFromName returns a name prefix if the string matches prefix criteria @@ -42,11 +47,15 @@ func HasResourceUniqueIdSuffix(s string) bool { // d.Set("name_prefix", naming.NamePrefixFromName(d.Id())) // func NamePrefixFromName(name string) *string { - if !HasResourceUniqueIdSuffix(name) { + return NamePrefixFromNameWithSuffix(name, "") +} + +func NamePrefixFromNameWithSuffix(name, nameSuffix string) *string { + if !HasResourceUniqueIdPlusAdditionalSuffix(name, nameSuffix) { return nil } - namePrefixIndex := len(name) - resource.UniqueIDSuffixLength + namePrefixIndex := len(name) - resource.UniqueIDSuffixLength - len(nameSuffix) if namePrefixIndex <= 0 { return nil @@ -59,8 +68,13 @@ func NamePrefixFromName(name string) *string { // TestCheckResourceAttrNameFromPrefix verifies that the state attribute value matches name generated from given prefix func TestCheckResourceAttrNameFromPrefix(resourceName string, attributeName string, prefix string) resource.TestCheckFunc { + return TestCheckResourceAttrNameWithSuffixFromPrefix(resourceName, attributeName, prefix, "") +} + +// TestCheckResourceAttrNameWithSuffixFromPrefix verifies that the state attribute value matches name with suffix generated from given prefix +func TestCheckResourceAttrNameWithSuffixFromPrefix(resourceName string, attributeName string, prefix string, suffix string) resource.TestCheckFunc { return func(s *terraform.State) error { - nameRegexpPattern := resourcePrefixedUniqueIDRegexpPattern(prefix) + nameRegexpPattern := resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern(prefix, suffix) attributeMatch, err := regexp.Compile(nameRegexpPattern) if err != nil { @@ -73,11 +87,37 @@ func TestCheckResourceAttrNameFromPrefix(resourceName string, attributeName stri // TestCheckResourceAttrNameGenerated verifies that the state attribute value matches name automatically generated without prefix func TestCheckResourceAttrNameGenerated(resourceName string, attributeName string) resource.TestCheckFunc { + return TestCheckResourceAttrNameWithSuffixGenerated(resourceName, attributeName, "") +} + +// TestCheckResourceAttrNameWithSuffixGenerated verifies that the state attribute value matches name with suffix automatically generated without prefix +func TestCheckResourceAttrNameWithSuffixGenerated(resourceName string, attributeName string, suffix string) resource.TestCheckFunc { return func(s *terraform.State) error { - return resource.TestMatchResourceAttr(resourceName, attributeName, resourceUniqueIDRegexp)(s) + return resource.TestMatchResourceAttr(resourceName, attributeName, resourceUniqueIDPrefixPlusAdditionalSuffixRegexp(suffix))(s) } } -func resourcePrefixedUniqueIDRegexpPattern(prefix string) string { - return fmt.Sprintf("^%s%s", prefix, resourceUniqueIDSuffixRegexpPattern) +// Regexp pattern for "<26 lowercase hex digits>". +func resourceUniqueIDPlusAdditionalSuffixRegexpPattern(additionalSuffix string) string { + return fmt.Sprintf("[[:xdigit:]]{%d}%s$", resource.UniqueIDSuffixLength, additionalSuffix) +} + +// Regexp for "<26 lowercase hex digits>". +func resourceUniqueIDPlusAdditionalSuffixRegexp(additionalSuffix string) *regexp.Regexp { + return regexp.MustCompile(resourceUniqueIDPlusAdditionalSuffixRegexpPattern(additionalSuffix)) +} + +// Regexp pattern for "<26 lowercase hex digits>". +func resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern(prefix string, additionalSuffix string) string { + return fmt.Sprintf("^%s%s", prefix, resourceUniqueIDPlusAdditionalSuffixRegexpPattern(additionalSuffix)) +} + +// Regexp pattern for "terraform-<26 lowercase hex digits>". +func resourceUniqueIDPrefixPlusAdditionalSuffixRegexpPattern(additionalSuffix string) string { + return resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern(resource.UniqueIdPrefix, additionalSuffix) +} + +// Regexp for "terraform-<26 lowercase hex digits>". +func resourceUniqueIDPrefixPlusAdditionalSuffixRegexp(additionalSuffix string) *regexp.Regexp { + return regexp.MustCompile(resourceUniqueIDPrefixPlusAdditionalSuffixRegexpPattern(additionalSuffix)) } diff --git a/aws/internal/naming/naming_test.go b/aws/internal/naming/naming_test.go index a4cc0b6e0d05..c3f0486279d3 100644 --- a/aws/internal/naming/naming_test.go +++ b/aws/internal/naming/naming_test.go @@ -22,17 +22,23 @@ func TestGenerate(t *testing.T) { NamePrefix: "", ExpectedRegexpPattern: "^test$", }, + { + TestName: "name ignores prefix", + Name: "test", + NamePrefix: "prefix", + ExpectedRegexpPattern: "^test$", + }, { TestName: "name prefix", Name: "", - NamePrefix: "test", - ExpectedRegexpPattern: resourcePrefixedUniqueIDRegexpPattern("test"), + NamePrefix: "prefix", + ExpectedRegexpPattern: resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern("prefix", ""), }, { TestName: "fully generated", Name: "", NamePrefix: "", - ExpectedRegexpPattern: resourceUniqueIDRegexpPattern, + ExpectedRegexpPattern: resourceUniqueIDPlusAdditionalSuffixRegexpPattern(""), }, } @@ -53,6 +59,75 @@ func TestGenerate(t *testing.T) { } } +func TestGenerateWithSuffix(t *testing.T) { + testCases := []struct { + TestName string + Name string + NamePrefix string + NameSuffix string + ExpectedRegexpPattern string + }{ + { + TestName: "name", + Name: "test", + NamePrefix: "", + NameSuffix: "", + ExpectedRegexpPattern: "^test$", + }, + { + TestName: "name ignores prefix and suffix", + Name: "test", + NamePrefix: "prefix", + NameSuffix: "suffix", + ExpectedRegexpPattern: "^test$", + }, + { + TestName: "name prefix no suffix", + Name: "", + NamePrefix: "prefix", + NameSuffix: "", + ExpectedRegexpPattern: resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern("prefix", ""), + }, + { + TestName: "name prefix with suffix", + Name: "", + NamePrefix: "prefix", + NameSuffix: "suffix", + ExpectedRegexpPattern: resourcePrefixedUniqueIDPlusAdditionalSuffixRegexpPattern("prefix", "suffix"), + }, + { + TestName: "fully generated no suffix", + Name: "", + NamePrefix: "", + NameSuffix: "", + ExpectedRegexpPattern: resourceUniqueIDPlusAdditionalSuffixRegexpPattern(""), + }, + { + TestName: "fully generated with suffix", + Name: "", + NamePrefix: "", + NameSuffix: "suffix", + ExpectedRegexpPattern: resourceUniqueIDPlusAdditionalSuffixRegexpPattern("suffix"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got := GenerateWithSuffix(testCase.Name, testCase.NamePrefix, testCase.NameSuffix) + + expectedRegexp, err := regexp.Compile(testCase.ExpectedRegexpPattern) + + if err != nil { + t.Errorf("unable to compile regular expression pattern %s: %s", testCase.ExpectedRegexpPattern, err) + } + + if !expectedRegexp.MatchString(got) { + t.Errorf("got %s, expected to match regular expression pattern %s", got, testCase.ExpectedRegexpPattern) + } + }) + } +} + func TestHasResourceUniqueIdSuffix(t *testing.T) { testCases := []struct { TestName string @@ -92,6 +167,55 @@ func TestHasResourceUniqueIdSuffix(t *testing.T) { } } +func TestHasResourceUniqueIdPlusAdditionalSuffix(t *testing.T) { + testCases := []struct { + TestName string + Input string + Expected bool + }{ + { + TestName: "empty", + Input: "", + Expected: false, + }, + { + TestName: "incorrect suffix", + Input: "test-123", + Expected: false, + }, + { + TestName: "missing additional suffix with numbers", + Input: "test-20060102150405000000000001", + Expected: false, + }, + { + TestName: "correct suffix with numbers", + Input: "test-20060102150405000000000001suffix", + Expected: true, + }, + { + TestName: "missing additional suffix with hex", + Input: "test-200601021504050000000000a1", + Expected: false, + }, + { + TestName: "correct suffix with hex", + Input: "test-200601021504050000000000a1suffix", + Expected: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got := HasResourceUniqueIdPlusAdditionalSuffix(testCase.Input, "suffix") + + if got != testCase.Expected { + t.Errorf("got %t, expected %t", got, testCase.Expected) + } + }) + } +} + func TestNamePrefixFromName(t *testing.T) { testCases := []struct { TestName string @@ -129,6 +253,11 @@ func TestNamePrefixFromName(t *testing.T) { Input: "terraform-20060102150405000000000001", Expected: strPtr("terraform-"), }, + { + TestName: "KMS alias prefix", + Input: "alias/20210723150229087000000002", + Expected: strPtr("alias/"), + }, } for _, testCase := range testCases { @@ -166,3 +295,98 @@ func TestNamePrefixFromName(t *testing.T) { } }) } + +func TestNamePrefixFromNameWithSuffix(t *testing.T) { + testCases := []struct { + TestName string + Input string + Expected *string + }{ + { + TestName: "empty", + Input: "", + Expected: nil, + }, + { + TestName: "incorrect suffix", + Input: "test-123", + Expected: nil, + }, + { + TestName: "prefix without hyphen, missing additional suffix", + Input: "test20060102150405000000000001", + Expected: nil, + }, + { + TestName: "prefix without hyphen, correct suffix", + Input: "test20060102150405000000000001suffix", + Expected: strPtr("test"), + }, + { + TestName: "prefix with hyphen, missing additional suffix", + Input: "test-20060102150405000000000001", + Expected: nil, + }, + { + TestName: "prefix with hyphen, correct suffix", + Input: "test-20060102150405000000000001suffix", + Expected: strPtr("test-"), + }, + { + TestName: "prefix with hyphen, missing additional suffix with hex", + Input: "test-200601021504050000000000f1", + Expected: nil, + }, + { + TestName: "prefix with hyphen, correct suffix with hex", + Input: "test-200601021504050000000000f1suffix", + Expected: strPtr("test-"), + }, + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/17017 + { + TestName: "terraform prefix, missing additional suffix", + Input: "terraform-20060102150405000000000001", + Expected: nil, + }, + { + TestName: "terraform prefix, correct suffix", + Input: "terraform-20060102150405000000000001suffix", + Expected: strPtr("terraform-"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + expected := testCase.Expected + got := NamePrefixFromNameWithSuffix(testCase.Input, "suffix") + + if expected == nil && got != nil { + t.Errorf("got %s, expected nil", *got) + } + + if expected != nil && got == nil { + t.Errorf("got nil, expected %s", *expected) + } + + if expected != nil && got != nil && *expected != *got { + t.Errorf("got %s, expected %s", *got, *expected) + } + }) + } + + t.Run("extracting prefix from generated name", func(t *testing.T) { + for i := 0; i < 10; i++ { + prefix := "test-" + input := GenerateWithSuffix("", prefix, "suffix") + got := NamePrefixFromNameWithSuffix(input, "suffix") + + if got == nil { + t.Errorf("run%d: got nil, expected %s for input %s", i, prefix, input) + } + + if got != nil && prefix != *got { + t.Errorf("run%d: got %s, expected %s for input %s", i, *got, prefix, input) + } + } + }) +} diff --git a/aws/internal/net/cidr.go b/aws/internal/net/cidr.go new file mode 100644 index 000000000000..77ac94522f2c --- /dev/null +++ b/aws/internal/net/cidr.go @@ -0,0 +1,34 @@ +package net + +import ( + "net" +) + +// CIDRBlocksEqual returns whether or not two CIDR blocks are equal: +// - Both CIDR blocks parse to an IP address and network +// - The string representation of the IP addresses are equal +// - The string representation of the networks are equal +// This function is especially useful for IPv6 CIDR blocks which have multiple valid representations. +func CIDRBlocksEqual(cidr1, cidr2 string) bool { + ip1, ipnet1, err := net.ParseCIDR(cidr1) + if err != nil { + return false + } + ip2, ipnet2, err := net.ParseCIDR(cidr2) + if err != nil { + return false + } + + return ip2.String() == ip1.String() && ipnet2.String() == ipnet1.String() +} + +// CanonicalCIDRBlock returns the canonical representation of a CIDR block. +// This function is especially useful for hash functions for sets which include IPv6 CIDR blocks. +func CanonicalCIDRBlock(cidr string) string { + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return cidr + } + + return ipnet.String() +} diff --git a/aws/internal/net/cidr_test.go b/aws/internal/net/cidr_test.go new file mode 100644 index 000000000000..308f76a50799 --- /dev/null +++ b/aws/internal/net/cidr_test.go @@ -0,0 +1,45 @@ +package net_test + +import ( + "testing" + + tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net" +) + +func TestCIDRBlocksEqual(t *testing.T) { + for _, ts := range []struct { + cidr1 string + cidr2 string + equal bool + }{ + {"10.2.2.0/24", "10.2.2.0/24", true}, + {"10.2.2.0/1234", "10.2.2.0/24", false}, + {"10.2.2.0/24", "10.2.2.0/1234", false}, + {"2001::/15", "2001::/15", true}, + {"::/0", "2001::/15", false}, + {"::/0", "::0/0", true}, + {"", "", false}, + } { + equal := tfnet.CIDRBlocksEqual(ts.cidr1, ts.cidr2) + if ts.equal != equal { + t.Fatalf("CIDRBlocksEqual(%q, %q) should be: %t", ts.cidr1, ts.cidr2, ts.equal) + } + } +} + +func TestCanonicalCIDRBlock(t *testing.T) { + for _, ts := range []struct { + cidr string + expected string + }{ + {"10.2.2.0/24", "10.2.2.0/24"}, + {"::/0", "::/0"}, + {"::0/0", "::/0"}, + {"", ""}, + } { + got := tfnet.CanonicalCIDRBlock(ts.cidr) + if ts.expected != got { + t.Fatalf("CanonicalCIDRBlock(%q) should be: %q, got: %q", ts.cidr, ts.expected, got) + } + } +} diff --git a/aws/internal/service/acmpca/finder/finder.go b/aws/internal/service/acmpca/finder/finder.go index f6dfc51bb5ba..7cf9c2f36a86 100644 --- a/aws/internal/service/acmpca/finder/finder.go +++ b/aws/internal/service/acmpca/finder/finder.go @@ -3,6 +3,8 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) // CertificateAuthorityByARN returns the certificate authority corresponding to the specified ARN. @@ -23,3 +25,31 @@ func CertificateAuthorityByARN(conn *acmpca.ACMPCA, arn string) (*acmpca.Certifi return output.CertificateAuthority, nil } + +// CertificateAuthorityCertificateByARN returns the certificate for the certificate authority corresponding to the specified ARN. +// Returns a resource.NotFoundError if no certificate authority is found or the certificate authority does not have a certificate assigned. +func CertificateAuthorityCertificateByARN(conn *acmpca.ACMPCA, arn string) (*acmpca.GetCertificateAuthorityCertificateOutput, error) { + input := &acmpca.GetCertificateAuthorityCertificateInput{ + CertificateAuthorityArn: aws.String(arn), + } + + output, err := conn.GetCertificateAuthorityCertificate(input) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/acmpca/waiter/waiter.go b/aws/internal/service/acmpca/waiter/waiter.go index c9c4c803f9d7..0205dabec2ac 100644 --- a/aws/internal/service/acmpca/waiter/waiter.go +++ b/aws/internal/service/acmpca/waiter/waiter.go @@ -24,3 +24,7 @@ func CertificateAuthorityCreated(conn *acmpca.ACMPCA, arn string, timeout time.D return nil, err } + +const ( + CertificateAuthorityActiveTimeout = 1 * time.Minute +) diff --git a/aws/internal/service/amplify/consts.go b/aws/internal/service/amplify/consts.go new file mode 100644 index 000000000000..3049f2bcfc21 --- /dev/null +++ b/aws/internal/service/amplify/consts.go @@ -0,0 +1,5 @@ +package amplify + +const ( + StageNone = "NONE" +) diff --git a/aws/internal/service/amplify/finder/finder.go b/aws/internal/service/amplify/finder/finder.go new file mode 100644 index 000000000000..9440c53a1f0d --- /dev/null +++ b/aws/internal/service/amplify/finder/finder.go @@ -0,0 +1,151 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func AppByID(conn *amplify.Amplify, id string) (*amplify.App, error) { + input := &lify.GetAppInput{ + AppId: aws.String(id), + } + + output, err := conn.GetApp(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.App == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.App, nil +} + +func BackendEnvironmentByAppIDAndEnvironmentName(conn *amplify.Amplify, appID, environmentName string) (*amplify.BackendEnvironment, error) { + input := &lify.GetBackendEnvironmentInput{ + AppId: aws.String(appID), + EnvironmentName: aws.String(environmentName), + } + + output, err := conn.GetBackendEnvironment(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.BackendEnvironment == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.BackendEnvironment, nil +} + +func BranchByAppIDAndBranchName(conn *amplify.Amplify, appID, branchName string) (*amplify.Branch, error) { + input := &lify.GetBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + } + + output, err := conn.GetBranch(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Branch == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Branch, nil +} + +func DomainAssociationByAppIDAndDomainName(conn *amplify.Amplify, appID, domainName string) (*amplify.DomainAssociation, error) { + input := &lify.GetDomainAssociationInput{ + AppId: aws.String(appID), + DomainName: aws.String(domainName), + } + + output, err := conn.GetDomainAssociation(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.DomainAssociation == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.DomainAssociation, nil +} + +func WebhookByID(conn *amplify.Amplify, id string) (*amplify.Webhook, error) { + input := &lify.GetWebhookInput{ + WebhookId: aws.String(id), + } + + output, err := conn.GetWebhook(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Webhook == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Webhook, nil +} diff --git a/aws/internal/service/amplify/id.go b/aws/internal/service/amplify/id.go new file mode 100644 index 000000000000..aeda71e1b481 --- /dev/null +++ b/aws/internal/service/amplify/id.go @@ -0,0 +1,63 @@ +package amplify + +import ( + "fmt" + "strings" +) + +const backendEnvironmentResourceIDSeparator = "/" + +func BackendEnvironmentCreateResourceID(appID, environmentName string) string { + parts := []string{appID, environmentName} + id := strings.Join(parts, backendEnvironmentResourceIDSeparator) + + return id +} + +func BackendEnvironmentParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, backendEnvironmentResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected APPID%[2]sENVIRONMENTNAME", id, backendEnvironmentResourceIDSeparator) +} + +const branchResourceIDSeparator = "/" + +func BranchCreateResourceID(appID, branchName string) string { + parts := []string{appID, branchName} + id := strings.Join(parts, branchResourceIDSeparator) + + return id +} + +func BranchParseResourceID(id string) (string, string, error) { + parts := strings.SplitN(id, branchResourceIDSeparator, 2) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected APPID%[2]sBRANCHNAME", id, branchResourceIDSeparator) +} + +const domainAssociationResourceIDSeparator = "/" + +func DomainAssociationCreateResourceID(appID, domainName string) string { + parts := []string{appID, domainName} + id := strings.Join(parts, domainAssociationResourceIDSeparator) + + return id +} + +func DomainAssociationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, domainAssociationResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected APPID%[2]sDOMAINNAME", id, domainAssociationResourceIDSeparator) +} diff --git a/aws/internal/service/amplify/id_test.go b/aws/internal/service/amplify/id_test.go new file mode 100644 index 000000000000..b90b3c86618f --- /dev/null +++ b/aws/internal/service/amplify/id_test.go @@ -0,0 +1,68 @@ +package amplify_test + +import ( + "testing" + + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" +) + +func TestBranchParseResourceID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectError bool + ExpectedAppID string + ExpectedBranchName string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectError: true, + }, + { + TestName: "incorrect format", + InputID: "test", + ExpectError: true, + }, + { + TestName: "valid ID", + InputID: tfamplify.BranchCreateResourceID("appID", "branchName"), + ExpectedAppID: "appID", + ExpectedBranchName: "branchName", + }, + { + TestName: "valid ID one slash", + InputID: tfamplify.BranchCreateResourceID("appID", "part1/part_2"), + ExpectedAppID: "appID", + ExpectedBranchName: "part1/part_2", + }, + { + TestName: "valid ID three slashes", + InputID: tfamplify.BranchCreateResourceID("appID", "part1/part_2/part-3/part4"), + ExpectedAppID: "appID", + ExpectedBranchName: "part1/part_2/part-3/part4", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotAppID, gotBranchName, err := tfamplify.BranchParseResourceID(testCase.InputID) + + if err == nil && testCase.ExpectError { + t.Fatalf("expected error") + } + + if err != nil && !testCase.ExpectError { + t.Fatalf("unexpected error") + } + + if gotAppID != testCase.ExpectedAppID { + t.Errorf("got AppID %s, expected %s", gotAppID, testCase.ExpectedAppID) + } + + if gotBranchName != testCase.ExpectedBranchName { + t.Errorf("got BranchName %s, expected %s", gotBranchName, testCase.ExpectedBranchName) + } + }) + } +} diff --git a/aws/internal/service/amplify/lister/list.go b/aws/internal/service/amplify/lister/list.go new file mode 100644 index 000000000000..3b7007c9979c --- /dev/null +++ b/aws/internal/service/amplify/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=ListApps github.com/aws/aws-sdk-go/service/amplify + +package lister diff --git a/aws/internal/service/amplify/lister/list_pages_gen.go b/aws/internal/service/amplify/lister/list_pages_gen.go new file mode 100644 index 000000000000..30fa4b129300 --- /dev/null +++ b/aws/internal/service/amplify/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=ListApps github.com/aws/aws-sdk-go/service/amplify"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" +) + +func ListAppsPages(conn *amplify.Amplify, input *amplify.ListAppsInput, fn func(*amplify.ListAppsOutput, bool) bool) error { + return ListAppsPagesWithContext(context.Background(), conn, input, fn) +} + +func ListAppsPagesWithContext(ctx context.Context, conn *amplify.Amplify, input *amplify.ListAppsInput, fn func(*amplify.ListAppsOutput, bool) bool) error { + for { + output, err := conn.ListAppsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/amplify/waiter/status.go b/aws/internal/service/amplify/waiter/status.go new file mode 100644 index 000000000000..49be5ec89c8f --- /dev/null +++ b/aws/internal/service/amplify/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func DomainAssociationStatus(conn *amplify.Amplify, appID, domainName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + domainAssociation, err := finder.DomainAssociationByAppIDAndDomainName(conn, appID, domainName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return domainAssociation, aws.StringValue(domainAssociation.DomainStatus), nil + } +} diff --git a/aws/internal/service/amplify/waiter/waiter.go b/aws/internal/service/amplify/waiter/waiter.go new file mode 100644 index 000000000000..02105061a37d --- /dev/null +++ b/aws/internal/service/amplify/waiter/waiter.go @@ -0,0 +1,58 @@ +package waiter + +import ( + "errors" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + DomainAssociationCreatedTimeout = 5 * time.Minute + DomainAssociationVerifiedTimeout = 15 * time.Minute +) + +func DomainAssociationCreated(conn *amplify.Amplify, appID, domainName string) (*amplify.DomainAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{amplify.DomainStatusCreating, amplify.DomainStatusInProgress, amplify.DomainStatusRequestingCertificate}, + Target: []string{amplify.DomainStatusPendingVerification, amplify.DomainStatusPendingDeployment, amplify.DomainStatusAvailable}, + Refresh: DomainAssociationStatus(conn, appID, domainName), + Timeout: DomainAssociationCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*amplify.DomainAssociation); ok { + if status := aws.StringValue(v.DomainStatus); status == amplify.DomainStatusFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(v.StatusReason))) + } + + return v, err + } + + return nil, err +} + +func DomainAssociationVerified(conn *amplify.Amplify, appID, domainName string) (*amplify.DomainAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{amplify.DomainStatusUpdating, amplify.DomainStatusInProgress, amplify.DomainStatusPendingVerification}, + Target: []string{amplify.DomainStatusPendingDeployment, amplify.DomainStatusAvailable}, + Refresh: DomainAssociationStatus(conn, appID, domainName), + Timeout: DomainAssociationVerifiedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*amplify.DomainAssociation); ok { + if v != nil && aws.StringValue(v.DomainStatus) == amplify.DomainStatusFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(v.StatusReason))) + } + + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/apigateway/waiter/status.go b/aws/internal/service/apigateway/waiter/status.go new file mode 100644 index 000000000000..864915761a10 --- /dev/null +++ b/aws/internal/service/apigateway/waiter/status.go @@ -0,0 +1,31 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apigateway" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func apiGatewayVpcLinkStatus(conn *apigateway.APIGateway, vpcLinkId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := conn.GetVpcLink(&apigateway.GetVpcLinkInput{ + VpcLinkId: aws.String(vpcLinkId), + }) + if tfawserr.ErrCodeEquals(err, apigateway.ErrCodeNotFoundException) { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + + // Error messages can also be contained in the response with FAILED status + if aws.StringValue(output.Status) == apigateway.VpcLinkStatusFailed { + return output, apigateway.VpcLinkStatusFailed, fmt.Errorf("%s: %s", apigateway.VpcLinkStatusFailed, aws.StringValue(output.StatusMessage)) + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/apigateway/waiter/waiter.go b/aws/internal/service/apigateway/waiter/waiter.go new file mode 100644 index 000000000000..bcfced8d857a --- /dev/null +++ b/aws/internal/service/apigateway/waiter/waiter.go @@ -0,0 +1,48 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/apigateway" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time for VpcLink to become available + ApiGatewayVpcLinkAvailableTimeout = 20 * time.Minute + + // Maximum amount of time for VpcLink to delete + ApiGatewayVpcLinkDeleteTimeout = 20 * time.Minute +) + +func ApiGatewayVpcLinkAvailable(conn *apigateway.APIGateway, vpcLinkId string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{apigateway.VpcLinkStatusPending}, + Target: []string{apigateway.VpcLinkStatusAvailable}, + Refresh: apiGatewayVpcLinkStatus(conn, vpcLinkId), + Timeout: ApiGatewayVpcLinkAvailableTimeout, + MinTimeout: 3 * time.Second, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ApiGatewayVpcLinkDeleted(conn *apigateway.APIGateway, vpcLinkId string) error { + stateConf := resource.StateChangeConf{ + Pending: []string{ + apigateway.VpcLinkStatusPending, + apigateway.VpcLinkStatusAvailable, + apigateway.VpcLinkStatusDeleting, + }, + Target: []string{}, + Timeout: ApiGatewayVpcLinkDeleteTimeout, + MinTimeout: 1 * time.Second, + Refresh: apiGatewayVpcLinkStatus(conn, vpcLinkId), + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/internal/service/apigatewayv2/finder/finder.go b/aws/internal/service/apigatewayv2/finder/finder.go index 679475b65292..c673be953ef3 100644 --- a/aws/internal/service/apigatewayv2/finder/finder.go +++ b/aws/internal/service/apigatewayv2/finder/finder.go @@ -3,18 +3,105 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apigatewayv2/lister" ) // ApiByID returns the API corresponding to the specified ID. +// Returns NotFoundError if no API is found. func ApiByID(conn *apigatewayv2.ApiGatewayV2, apiID string) (*apigatewayv2.GetApiOutput, error) { input := &apigatewayv2.GetApiInput{ ApiId: aws.String(apiID), } + return Api(conn, input) +} + +// Api returns the API corresponding to the specified input. +// Returns NotFoundError if no API is found. +func Api(conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetApiInput) (*apigatewayv2.GetApiOutput, error) { output, err := conn.GetApi(input) + + if tfawserr.ErrCodeEquals(err, apigatewayv2.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + // Handle any empty result. + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +// Apis returns the APIs corresponding to the specified input. +// Returns an empty slice if no APIs are found. +func Apis(conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetApisInput) ([]*apigatewayv2.Api, error) { + var apis []*apigatewayv2.Api + + err := lister.GetApisPages(conn, input, func(page *apigatewayv2.GetApisOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.Items { + if item == nil { + continue + } + + apis = append(apis, item) + } + + return !lastPage + }) + if err != nil { return nil, err } + return apis, nil +} + +func DomainNameByName(conn *apigatewayv2.ApiGatewayV2, name string) (*apigatewayv2.GetDomainNameOutput, error) { + input := &apigatewayv2.GetDomainNameInput{ + DomainName: aws.String(name), + } + + return DomainName(conn, input) +} + +func DomainName(conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetDomainNameInput) (*apigatewayv2.GetDomainNameOutput, error) { + output, err := conn.GetDomainName(input) + + if tfawserr.ErrCodeEquals(err, apigatewayv2.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + // Handle any empty result. + if output == nil || len(output.DomainNameConfigurations) == 0 { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + return output, nil } diff --git a/aws/internal/service/apigatewayv2/lister/list.go b/aws/internal/service/apigatewayv2/lister/list.go new file mode 100644 index 000000000000..0ba32f6e67e7 --- /dev/null +++ b/aws/internal/service/apigatewayv2/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=GetApis,GetDomainNames github.com/aws/aws-sdk-go/service/apigatewayv2 + +package lister diff --git a/aws/internal/service/apigatewayv2/lister/list_pages_gen.go b/aws/internal/service/apigatewayv2/lister/list_pages_gen.go new file mode 100644 index 000000000000..499fa7f2c9b9 --- /dev/null +++ b/aws/internal/service/apigatewayv2/lister/list_pages_gen.go @@ -0,0 +1,52 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=GetApis,GetDomainNames github.com/aws/aws-sdk-go/service/apigatewayv2"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apigatewayv2" +) + +func GetApisPages(conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetApisInput, fn func(*apigatewayv2.GetApisOutput, bool) bool) error { + return GetApisPagesWithContext(context.Background(), conn, input, fn) +} + +func GetApisPagesWithContext(ctx context.Context, conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetApisInput, fn func(*apigatewayv2.GetApisOutput, bool) bool) error { + for { + output, err := conn.GetApisWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} + +func GetDomainNamesPages(conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetDomainNamesInput, fn func(*apigatewayv2.GetDomainNamesOutput, bool) bool) error { + return GetDomainNamesPagesWithContext(context.Background(), conn, input, fn) +} + +func GetDomainNamesPagesWithContext(ctx context.Context, conn *apigatewayv2.ApiGatewayV2, input *apigatewayv2.GetDomainNamesInput, fn func(*apigatewayv2.GetDomainNamesOutput, bool) bool) error { + for { + output, err := conn.GetDomainNamesWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/apigatewayv2/waiter/status.go b/aws/internal/service/apigatewayv2/waiter/status.go index 478600fa77aa..dc0bd7e20525 100644 --- a/aws/internal/service/apigatewayv2/waiter/status.go +++ b/aws/internal/service/apigatewayv2/waiter/status.go @@ -2,10 +2,13 @@ package waiter import ( "fmt" + "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigatewayv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apigatewayv2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // DeploymentStatus fetches the Deployment and its Status @@ -32,6 +35,26 @@ func DeploymentStatus(conn *apigatewayv2.ApiGatewayV2, apiId, deploymentId strin } } +func DomainNameStatus(conn *apigatewayv2.ApiGatewayV2, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + domainName, err := finder.DomainNameByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if statusMessage := aws.StringValue(domainName.DomainNameConfigurations[0].DomainNameStatusMessage); statusMessage != "" { + log.Printf("[INFO] API Gateway v2 domain name (%s) status message: %s", name, statusMessage) + } + + return domainName, aws.StringValue(domainName.DomainNameConfigurations[0].DomainNameStatus), nil + } +} + // VpcLinkStatus fetches the VPC Link and its Status func VpcLinkStatus(conn *apigatewayv2.ApiGatewayV2, vpcLinkId string) resource.StateRefreshFunc { return func() (interface{}, string, error) { diff --git a/aws/internal/service/apigatewayv2/waiter/waiter.go b/aws/internal/service/apigatewayv2/waiter/waiter.go index 4f3d536f5b99..73419717ff17 100644 --- a/aws/internal/service/apigatewayv2/waiter/waiter.go +++ b/aws/internal/service/apigatewayv2/waiter/waiter.go @@ -36,6 +36,23 @@ func DeploymentDeployed(conn *apigatewayv2.ApiGatewayV2, apiId, deploymentId str return nil, err } +func DomainNameAvailable(conn *apigatewayv2.ApiGatewayV2, name string, timeout time.Duration) (*apigatewayv2.GetDomainNameOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{apigatewayv2.DomainNameStatusUpdating}, + Target: []string{apigatewayv2.DomainNameStatusAvailable}, + Refresh: DomainNameStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*apigatewayv2.GetDomainNameOutput); ok { + return v, err + } + + return nil, err +} + // VpcLinkAvailable waits for a VPC Link to return Available func VpcLinkAvailable(conn *apigatewayv2.ApiGatewayV2, vpcLinkId string) (*apigatewayv2.GetVpcLinkOutput, error) { stateConf := &resource.StateChangeConf{ diff --git a/aws/internal/service/applicationautoscaling/finder/finder.go b/aws/internal/service/applicationautoscaling/finder/finder.go new file mode 100644 index 000000000000..fdc40e924db9 --- /dev/null +++ b/aws/internal/service/applicationautoscaling/finder/finder.go @@ -0,0 +1,46 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/applicationautoscaling" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func ScheduledAction(conn *applicationautoscaling.ApplicationAutoScaling, name, serviceNamespace, resourceId string) (*applicationautoscaling.ScheduledAction, error) { + var result *applicationautoscaling.ScheduledAction + + input := &applicationautoscaling.DescribeScheduledActionsInput{ + ScheduledActionNames: []*string{aws.String(name)}, + ServiceNamespace: aws.String(serviceNamespace), + ResourceId: aws.String(resourceId), + } + err := conn.DescribeScheduledActionsPages(input, func(page *applicationautoscaling.DescribeScheduledActionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.ScheduledActions { + if item == nil { + continue + } + + if name == aws.StringValue(item.ScheduledActionName) { + result = item + return false + } + } + + return !lastPage + }) + if err != nil { + return nil, err + } + + if result == nil { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return result, nil +} diff --git a/aws/internal/service/appmesh/waiter/waiter.go b/aws/internal/service/appmesh/waiter/waiter.go new file mode 100644 index 000000000000..6ed2977a7dc7 --- /dev/null +++ b/aws/internal/service/appmesh/waiter/waiter.go @@ -0,0 +1,10 @@ +package waiter + +import ( + "time" +) + +const ( + // Maximum amount of time to wait for Appmesh changes to propagate + PropagationTimeout = 2 * time.Minute +) diff --git a/aws/internal/service/apprunner/finder/finder.go b/aws/internal/service/apprunner/finder/finder.go new file mode 100644 index 000000000000..8da12ffbc308 --- /dev/null +++ b/aws/internal/service/apprunner/finder/finder.go @@ -0,0 +1,82 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" +) + +func ConnectionSummaryByName(ctx context.Context, conn *apprunner.AppRunner, name string) (*apprunner.ConnectionSummary, error) { + input := &apprunner.ListConnectionsInput{ + ConnectionName: aws.String(name), + } + + var cs *apprunner.ConnectionSummary + + err := conn.ListConnectionsPagesWithContext(ctx, input, func(page *apprunner.ListConnectionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.ConnectionSummaryList { + if c == nil { + continue + } + + if aws.StringValue(c.ConnectionName) == name { + cs = c + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if cs == nil { + return nil, nil + } + + return cs, nil +} + +func CustomDomain(ctx context.Context, conn *apprunner.AppRunner, domainName, serviceArn string) (*apprunner.CustomDomain, error) { + input := &apprunner.DescribeCustomDomainsInput{ + ServiceArn: aws.String(serviceArn), + } + + var customDomain *apprunner.CustomDomain + + err := conn.DescribeCustomDomainsPagesWithContext(ctx, input, func(page *apprunner.DescribeCustomDomainsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, cd := range page.CustomDomains { + if cd == nil { + continue + } + + if aws.StringValue(cd.DomainName) == domainName { + customDomain = cd + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if customDomain == nil { + return nil, nil + } + + return customDomain, nil +} diff --git a/aws/internal/service/apprunner/id.go b/aws/internal/service/apprunner/id.go new file mode 100644 index 000000000000..1674c942501b --- /dev/null +++ b/aws/internal/service/apprunner/id.go @@ -0,0 +1,14 @@ +package apprunner + +import ( + "fmt" + "strings" +) + +func CustomDomainAssociationParseID(id string) (string, string, error) { + idParts := strings.Split(id, ",") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected domain_name,service_arn", id) + } + return idParts[0], idParts[1], nil +} diff --git a/aws/internal/service/apprunner/id_test.go b/aws/internal/service/apprunner/id_test.go new file mode 100644 index 000000000000..a6ba3b2a9956 --- /dev/null +++ b/aws/internal/service/apprunner/id_test.go @@ -0,0 +1,78 @@ +package apprunner_test + +import ( + "fmt" + "testing" + + tfapprunner "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner" +) + +func TestCustomDomainAssociationParseID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectedError bool + ExpectedPart0 string + ExpectedPart1 string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectedError: true, + }, + { + TestName: "single part", + InputID: "example.com", + ExpectedError: true, + }, + { + TestName: "two parts", + InputID: fmt.Sprintf("%s,%s", "example.com", "arn:aws:apprunner:us-east-1:1234567890:service/example/0a03292a89764e5882c41d8f991c82fe"), //lintignore:AWSAT005 + ExpectedPart0: "example.com", + ExpectedPart1: "arn:aws:apprunner:us-east-1:1234567890:service/example/0a03292a89764e5882c41d8f991c82fe", //lintignore:AWSAT005 + }, + + { + TestName: "empty both parts", + InputID: ",", + ExpectedError: true, + }, + { + TestName: "empty first part", + InputID: ",arn:aws:apprunner:us-east-1:1234567890:service/example/0a03292a89764e5882c41d8f991c82fe", //lintignore:AWSAT005 + ExpectedError: true, + }, + { + TestName: "empty second part", + InputID: "example.com,", + ExpectedError: true, + }, + { + TestName: "three parts", + InputID: "example.com,arn:aws:apprunner:us-east-1:1234567890:service/example/0a03292a89764e5882c41d8f991c82fe,example", //lintignore:AWSAT005 + ExpectedError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotPart0, gotPart1, err := tfapprunner.CustomDomainAssociationParseID(testCase.InputID) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotPart0 != testCase.ExpectedPart0 { + t.Errorf("got part 0 %s, expected %s", gotPart0, testCase.ExpectedPart0) + } + + if gotPart1 != testCase.ExpectedPart1 { + t.Errorf("got part 1 %s, expected %s", gotPart1, testCase.ExpectedPart1) + } + }) + } +} diff --git a/aws/internal/service/apprunner/waiter/status.go b/aws/internal/service/apprunner/waiter/status.go new file mode 100644 index 000000000000..137e205a5c58 --- /dev/null +++ b/aws/internal/service/apprunner/waiter/status.go @@ -0,0 +1,92 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/finder" +) + +const ( + AutoScalingConfigurationStatusActive = "active" + AutoScalingConfigurationStatusInactive = "inactive" + + CustomDomainAssociationStatusActive = "active" + CustomDomainAssociationStatusCreating = "creating" + CustomDomainAssociationStatusDeleting = "deleting" + CustomDomainAssociationStatusPendingCertificateDnsValidation = "pending_certificate_dns_validation" +) + +func AutoScalingConfigurationStatus(ctx context.Context, conn *apprunner.AppRunner, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &apprunner.DescribeAutoScalingConfigurationInput{ + AutoScalingConfigurationArn: aws.String(arn), + } + + output, err := conn.DescribeAutoScalingConfigurationWithContext(ctx, input) + + if err != nil { + return nil, "", err + } + + if output == nil || output.AutoScalingConfiguration == nil { + return nil, "", nil + } + + return output.AutoScalingConfiguration, aws.StringValue(output.AutoScalingConfiguration.Status), nil + } +} + +func ConnectionStatus(ctx context.Context, conn *apprunner.AppRunner, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + c, err := finder.ConnectionSummaryByName(ctx, conn, name) + + if err != nil { + return nil, "", err + } + + if c == nil { + return nil, "", nil + } + + return c, aws.StringValue(c.Status), nil + } +} + +func CustomDomainStatus(ctx context.Context, conn *apprunner.AppRunner, domainName, serviceArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + customDomain, err := finder.CustomDomain(ctx, conn, domainName, serviceArn) + + if err != nil { + return nil, "", err + } + + if customDomain == nil { + return nil, "", nil + } + + return customDomain, aws.StringValue(customDomain.Status), nil + } +} + +func ServiceStatus(ctx context.Context, conn *apprunner.AppRunner, serviceArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &apprunner.DescribeServiceInput{ + ServiceArn: aws.String(serviceArn), + } + + output, err := conn.DescribeServiceWithContext(ctx, input) + + if err != nil { + return nil, "", err + } + + if output == nil || output.Service == nil { + return nil, "", nil + } + + return output.Service, aws.StringValue(output.Service.Status), nil + } +} diff --git a/aws/internal/service/apprunner/waiter/waiter.go b/aws/internal/service/apprunner/waiter/waiter.go new file mode 100644 index 000000000000..06e8e93e2572 --- /dev/null +++ b/aws/internal/service/apprunner/waiter/waiter.go @@ -0,0 +1,127 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + AutoScalingConfigurationCreateTimeout = 2 * time.Minute + AutoScalingConfigurationDeleteTimeout = 2 * time.Minute + + ConnectionDeleteTimeout = 5 * time.Minute + + CustomDomainAssociationCreateTimeout = 5 * time.Minute + CustomDomainAssociationDeleteTimeout = 5 * time.Minute + + ServiceCreateTimeout = 20 * time.Minute + ServiceDeleteTimeout = 20 * time.Minute + ServiceUpdateTimeout = 20 * time.Minute +) + +func AutoScalingConfigurationActive(ctx context.Context, conn *apprunner.AppRunner, arn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{AutoScalingConfigurationStatusActive}, + Refresh: AutoScalingConfigurationStatus(ctx, conn, arn), + Timeout: AutoScalingConfigurationCreateTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func AutoScalingConfigurationInactive(ctx context.Context, conn *apprunner.AppRunner, arn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{AutoScalingConfigurationStatusActive}, + Target: []string{AutoScalingConfigurationStatusInactive}, + Refresh: AutoScalingConfigurationStatus(ctx, conn, arn), + Timeout: AutoScalingConfigurationDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ConnectionDeleted(ctx context.Context, conn *apprunner.AppRunner, name string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{apprunner.ConnectionStatusPendingHandshake, apprunner.ConnectionStatusAvailable, apprunner.ConnectionStatusDeleted}, + Target: []string{}, + Refresh: ConnectionStatus(ctx, conn, name), + Timeout: ConnectionDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func CustomDomainAssociationCreated(ctx context.Context, conn *apprunner.AppRunner, domainName, serviceArn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{CustomDomainAssociationStatusCreating}, + Target: []string{CustomDomainAssociationStatusPendingCertificateDnsValidation}, + Refresh: CustomDomainStatus(ctx, conn, domainName, serviceArn), + Timeout: CustomDomainAssociationCreateTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func CustomDomainAssociationDeleted(ctx context.Context, conn *apprunner.AppRunner, domainName, serviceArn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{CustomDomainAssociationStatusActive, CustomDomainAssociationStatusDeleting}, + Target: []string{}, + Refresh: CustomDomainStatus(ctx, conn, domainName, serviceArn), + Timeout: CustomDomainAssociationDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ServiceCreated(ctx context.Context, conn *apprunner.AppRunner, serviceArn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{apprunner.ServiceStatusOperationInProgress}, + Target: []string{apprunner.ServiceStatusRunning}, + Refresh: ServiceStatus(ctx, conn, serviceArn), + Timeout: ServiceCreateTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ServiceUpdated(ctx context.Context, conn *apprunner.AppRunner, serviceArn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{apprunner.ServiceStatusOperationInProgress}, + Target: []string{apprunner.ServiceStatusRunning}, + Refresh: ServiceStatus(ctx, conn, serviceArn), + Timeout: ServiceUpdateTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ServiceDeleted(ctx context.Context, conn *apprunner.AppRunner, serviceArn string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{apprunner.ServiceStatusRunning, apprunner.ServiceStatusOperationInProgress}, + Target: []string{apprunner.ServiceStatusDeleted}, + Refresh: ServiceStatus(ctx, conn, serviceArn), + Timeout: ServiceDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/internal/service/appstream/finder/finder.go b/aws/internal/service/appstream/finder/finder.go new file mode 100644 index 000000000000..4cc7aa90b0b4 --- /dev/null +++ b/aws/internal/service/appstream/finder/finder.go @@ -0,0 +1,95 @@ +package finder + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/lister" +) + +// StackByName Retrieve a appstream stack by name +func StackByName(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Stack, error) { + input := &appstream.DescribeStacksInput{ + Names: []*string{aws.String(name)}, + } + + var stack *appstream.Stack + resp, err := conn.DescribeStacksWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if len(resp.Stacks) > 1 { + return nil, fmt.Errorf("got more than one stack with the name %s", name) + } + + if len(resp.Stacks) == 1 { + stack = resp.Stacks[0] + } + + return stack, nil +} + +// FleetByName Retrieve a appstream fleet by name +func FleetByName(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + input := &appstream.DescribeFleetsInput{ + Names: []*string{aws.String(name)}, + } + + var fleet *appstream.Fleet + resp, err := conn.DescribeFleetsWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if len(resp.Fleets) > 1 { + return nil, fmt.Errorf("got more than one fleet with the name %s", name) + } + + if len(resp.Fleets) == 1 { + fleet = resp.Fleets[0] + } + + return fleet, nil +} + +// ImageBuilderByName Retrieve a appstream ImageBuilder by name +func ImageBuilderByName(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + input := &appstream.DescribeImageBuildersInput{ + Names: []*string{aws.String(name)}, + } + + var result *appstream.ImageBuilder + + err := lister.DescribeImageBuildersPagesWithContext(ctx, conn, input, func(page *appstream.DescribeImageBuildersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, imageBuilder := range page.ImageBuilders { + if imageBuilder == nil { + continue + } + if aws.StringValue(imageBuilder.Name) == name { + result = imageBuilder + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if result == nil { + return nil, nil + } + + return result, nil +} diff --git a/aws/internal/service/appstream/lister/list.go b/aws/internal/service/appstream/lister/list.go new file mode 100644 index 000000000000..1f9ea59c7f78 --- /dev/null +++ b/aws/internal/service/appstream/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeImageBuilders github.com/aws/aws-sdk-go/service/appstream + +package lister diff --git a/aws/internal/service/appstream/lister/list_pages_gen.go b/aws/internal/service/appstream/lister/list_pages_gen.go new file mode 100644 index 000000000000..cfc91fd780b4 --- /dev/null +++ b/aws/internal/service/appstream/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeImageBuilders github.com/aws/aws-sdk-go/service/appstream"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" +) + +func DescribeImageBuildersPages(conn *appstream.AppStream, input *appstream.DescribeImageBuildersInput, fn func(*appstream.DescribeImageBuildersOutput, bool) bool) error { + return DescribeImageBuildersPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeImageBuildersPagesWithContext(ctx context.Context, conn *appstream.AppStream, input *appstream.DescribeImageBuildersInput, fn func(*appstream.DescribeImageBuildersOutput, bool) bool) error { + for { + output, err := conn.DescribeImageBuildersWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/appstream/waiter/status.go b/aws/internal/service/appstream/waiter/status.go new file mode 100644 index 000000000000..2d6ee9365dcd --- /dev/null +++ b/aws/internal/service/appstream/waiter/status.go @@ -0,0 +1,60 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" +) + +//StackState fetches the fleet and its state +func StackState(ctx context.Context, conn *appstream.AppStream, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + stack, err := finder.StackByName(ctx, conn, name) + if err != nil { + return nil, "Unknown", err + } + + if stack == nil { + return stack, "NotFound", nil + } + + return stack, "AVAILABLE", nil + } +} + +//FleetState fetches the fleet and its state +func FleetState(ctx context.Context, conn *appstream.AppStream, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + fleet, err := finder.FleetByName(ctx, conn, name) + + if err != nil { + return nil, "Unknown", err + } + + if fleet == nil { + return fleet, "NotFound", nil + } + + return fleet, aws.StringValue(fleet.State), nil + } +} + +//ImageBuilderState fetches the ImageBuilder and its state +func ImageBuilderState(ctx context.Context, conn *appstream.AppStream, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + imageBuilder, err := finder.ImageBuilderByName(ctx, conn, name) + + if err != nil { + return nil, "", err + } + + if imageBuilder == nil { + return nil, "", nil + } + + return imageBuilder, aws.StringValue(imageBuilder.State), nil + } +} diff --git a/aws/internal/service/appstream/waiter/waiter.go b/aws/internal/service/appstream/waiter/waiter.go new file mode 100644 index 000000000000..234ef7b30910 --- /dev/null +++ b/aws/internal/service/appstream/waiter/waiter.go @@ -0,0 +1,135 @@ +package waiter + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + // StackOperationTimeout Maximum amount of time to wait for Stack operation eventual consistency + StackOperationTimeout = 4 * time.Minute + + // FleetStateTimeout Maximum amount of time to wait for the FleetState to be RUNNING or STOPPED + FleetStateTimeout = 180 * time.Minute + // FleetOperationTimeout Maximum amount of time to wait for Fleet operation eventual consistency + FleetOperationTimeout = 15 * time.Minute + // ImageBuilderStateTimeout Maximum amount of time to wait for the ImageBuilderState to be RUNNING + // or for the ImageBuilder to be deleted + ImageBuilderStateTimeout = 60 * time.Minute +) + +// StackStateDeleted waits for a deleted stack +func StackStateDeleted(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Stack, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{"NotFound", "Unknown"}, + Refresh: StackState(ctx, conn, name), + Timeout: StackOperationTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.Stack); ok { + return output, err + } + + return nil, err +} + +// FleetStateRunning waits for a fleet running +func FleetStateRunning(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.FleetStateStarting}, + Target: []string{appstream.FleetStateRunning}, + Refresh: FleetState(ctx, conn, name), + Timeout: FleetStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.Fleet); ok { + return output, err + } + + return nil, err +} + +// FleetStateStopped waits for a fleet stopped +func FleetStateStopped(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.FleetStateStopping}, + Target: []string{appstream.FleetStateStopped}, + Refresh: FleetState(ctx, conn, name), + Timeout: FleetStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.Fleet); ok { + return output, err + } + + return nil, err +} + +// ImageBuilderStateRunning waits for a ImageBuilder running +func ImageBuilderStateRunning(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.ImageBuilderStatePending}, + Target: []string{appstream.ImageBuilderStateRunning}, + Refresh: ImageBuilderState(ctx, conn, name), + Timeout: ImageBuilderStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.ImageBuilder); ok { + if state, errors := aws.StringValue(output.State), output.ImageBuilderErrors; state == appstream.ImageBuilderStateFailed && len(errors) > 0 { + var errs *multierror.Error + + for _, err := range errors { + errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.ErrorCode), aws.StringValue(err.ErrorMessage))) + } + + tfresource.SetLastError(err, errs.ErrorOrNil()) + } + + return output, err + } + + return nil, err +} + +// ImageBuilderStateDeleted waits for a ImageBuilder deleted +func ImageBuilderStateDeleted(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.ImageBuilderStatePending, appstream.ImageBuilderStateDeleting}, + Target: []string{}, + Refresh: ImageBuilderState(ctx, conn, name), + Timeout: ImageBuilderStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.ImageBuilder); ok { + if state, errors := aws.StringValue(output.State), output.ImageBuilderErrors; state == appstream.ImageBuilderStateFailed && len(errors) > 0 { + var errs *multierror.Error + + for _, err := range errors { + errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.ErrorCode), aws.StringValue(err.ErrorMessage))) + } + + tfresource.SetLastError(err, errs.ErrorOrNil()) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/autoscaling/enum.go b/aws/internal/service/autoscaling/enum.go new file mode 100644 index 000000000000..21e6ec3d1f66 --- /dev/null +++ b/aws/internal/service/autoscaling/enum.go @@ -0,0 +1,5 @@ +package autoscaling + +const ( + TagResourceTypeAutoScalingGroup = `auto-scaling-group` +) diff --git a/aws/internal/service/backup/errors.go b/aws/internal/service/backup/errors.go new file mode 100644 index 000000000000..b07811ea2474 --- /dev/null +++ b/aws/internal/service/backup/errors.go @@ -0,0 +1,5 @@ +package backup + +const ( + ErrCodeAccessDeniedException = "AccessDeniedException" +) diff --git a/aws/internal/service/backup/finder/finder.go b/aws/internal/service/backup/finder/finder.go new file mode 100644 index 000000000000..5541d757a306 --- /dev/null +++ b/aws/internal/service/backup/finder/finder.go @@ -0,0 +1,37 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/backup" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfbackup "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/backup" +) + +func BackupVaultAccessPolicyByName(conn *backup.Backup, name string) (*backup.GetBackupVaultAccessPolicyOutput, error) { + input := &backup.GetBackupVaultAccessPolicyInput{ + BackupVaultName: aws.String(name), + } + + output, err := conn.GetBackupVaultAccessPolicy(input) + + if tfawserr.ErrCodeEquals(err, backup.ErrCodeResourceNotFoundException) || tfawserr.ErrCodeEquals(err, tfbackup.ErrCodeAccessDeniedException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/backup/waiter/waiter.go b/aws/internal/service/backup/waiter/waiter.go new file mode 100644 index 000000000000..705e7456c25e --- /dev/null +++ b/aws/internal/service/backup/waiter/waiter.go @@ -0,0 +1,10 @@ +package waiter + +import ( + "time" +) + +const ( + // Maximum amount of time to wait for Backup changes to propagate + PropagationTimeout = 2 * time.Minute +) diff --git a/aws/internal/service/batch/consts.go b/aws/internal/service/batch/consts.go new file mode 100644 index 000000000000..815aaa9cd842 --- /dev/null +++ b/aws/internal/service/batch/consts.go @@ -0,0 +1,5 @@ +package batch + +const ( + JobDefinitionStatusInactive = "INACTIVE" +) diff --git a/aws/internal/service/batch/equivalency/container_properties.go b/aws/internal/service/batch/equivalency/container_properties.go index a96f1a475f1c..272cf3ee0343 100644 --- a/aws/internal/service/batch/equivalency/container_properties.go +++ b/aws/internal/service/batch/equivalency/container_properties.go @@ -29,6 +29,46 @@ func (cp *containerProperties) Reduce() error { cp.Environment = nil } + // Prevent difference of API response that contains the default Fargate platform configuration + if cp.FargatePlatformConfiguration != nil { + if aws.StringValue(cp.FargatePlatformConfiguration.PlatformVersion) == "LATEST" { + cp.FargatePlatformConfiguration = nil + } + } + + if cp.LinuxParameters != nil { + if len(cp.LinuxParameters.Devices) == 0 { + cp.LinuxParameters.Devices = nil + } + + for _, device := range cp.LinuxParameters.Devices { + if len(device.Permissions) == 0 { + device.Permissions = nil + } + } + + if len(cp.LinuxParameters.Tmpfs) == 0 { + cp.LinuxParameters.Tmpfs = nil + } + + for _, tmpfs := range cp.LinuxParameters.Tmpfs { + if len(tmpfs.MountOptions) == 0 { + tmpfs.MountOptions = nil + } + } + } + + // Prevent difference of API response that adds an empty array when not configured during the request + if cp.LogConfiguration != nil { + if len(cp.LogConfiguration.Options) == 0 { + cp.LogConfiguration.Options = nil + } + + if len(cp.LogConfiguration.SecretOptions) == 0 { + cp.LogConfiguration.SecretOptions = nil + } + } + // Prevent difference of API response that adds an empty array when not configured during the request if len(cp.MountPoints) == 0 { cp.MountPoints = nil diff --git a/aws/internal/service/batch/equivalency/container_properties_test.go b/aws/internal/service/batch/equivalency/container_properties_test.go index ee7c53425e42..7baca6f05f0b 100644 --- a/aws/internal/service/batch/equivalency/container_properties_test.go +++ b/aws/internal/service/batch/equivalency/container_properties_test.go @@ -209,7 +209,7 @@ func TestEquivalentBatchContainerPropertiesJSON(t *testing.T) { ExpectEquivalent: true, }, { - Name: "empty command, mountPoints, resourceRequirements, secrets, ulimits, volumes", + Name: "empty command, logConfiguration.secretOptions, mountPoints, resourceRequirements, secrets, ulimits, volumes", ApiJson: ` { "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", @@ -219,6 +219,10 @@ func TestEquivalentBatchContainerPropertiesJSON(t *testing.T) { "jobRoleArn": "arn:aws:iam::123:role/role-test", "volumes": [], "environment": [{"name":"ENVIRONMENT","value":"test"}], + "logConfiguration": { + "logDriver": "awslogs", + "secretOptions": [] + }, "mountPoints": [], "ulimits": [], "resourceRequirements": [], @@ -236,7 +240,130 @@ func TestEquivalentBatchContainerPropertiesJSON(t *testing.T) { "name": "ENVIRONMENT", "value": "test" } - ] + ], + "logConfiguration": { + "logDriver": "awslogs" + } +} +`, + ExpectEquivalent: true, + }, + { + Name: "no fargatePlatformConfiguration", + ApiJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "resourceRequirements": [ + { + "type": "MEMORY", + "value": "512" + }, + { + "type": "VCPU", + "value": "0.25" + } + ], + "fargatePlatformConfiguration": { + "platformVersion": "LATEST" + } +} +`, + ConfigurationJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "resourceRequirements": [ + { + "type": "MEMORY", + "value": "512" + }, + { + "type": "VCPU", + "value": "0.25" + } + ] +} +`, + ExpectEquivalent: true, + }, + { + Name: "empty linuxParameters.devices, linuxParameters.tmpfs, logConfiguration.options", + ApiJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "vcpus": 1, + "memory": 4096, + "jobRoleArn": "arn:aws:iam::123:role/role-test", + "environment": [{"name":"ENVIRONMENT","value":"test"}], + "linuxParameters": { + "devices": [], + "initProcessEnabled": true, + "tmpfs": [] + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": {} + } +} +`, + ConfigurationJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "vcpus": 1, + "memory": 4096, + "jobRoleArn": "arn:aws:iam::123:role/role-test", + "environment": [{"name":"ENVIRONMENT","value":"test"}], + "linuxParameters": { + "initProcessEnabled": true + }, + "logConfiguration": { + "logDriver": "awslogs" + } +} +`, + ExpectEquivalent: true, + }, + { + Name: "empty linuxParameters.devices.permissions, linuxParameters.tmpfs.mountOptions", + ApiJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "vcpus": 1, + "memory": 4096, + "jobRoleArn": "arn:aws:iam::123:role/role-test", + "environment": [{"name":"ENVIRONMENT","value":"test"}], + "linuxParameters": { + "devices": [{ + "containerPath": "/test", + "hostPath": "/tmp", + "permissions": [] + }], + "initProcessEnabled": true, + "tmpfs": [{ + "containerPath": "/tmp", + "mountOptions": [], + "size": 4096 + }] + } +} +`, + ConfigurationJson: ` +{ + "image": "123.dkr.ecr.us-east-1.amazonaws.com/my-app", + "vcpus": 1, + "memory": 4096, + "jobRoleArn": "arn:aws:iam::123:role/role-test", + "environment": [{"name":"ENVIRONMENT","value":"test"}], + "linuxParameters": { + "devices": [{ + "containerPath": "/test", + "hostPath": "/tmp" + }], + "initProcessEnabled": true, + "tmpfs": [{ + "containerPath": "/tmp", + "size": 4096 + }] + } } `, ExpectEquivalent: true, diff --git a/aws/internal/service/batch/finder/finder.go b/aws/internal/service/batch/finder/finder.go new file mode 100644 index 000000000000..6c56156993d2 --- /dev/null +++ b/aws/internal/service/batch/finder/finder.go @@ -0,0 +1,88 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/batch" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfbatch "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/batch" +) + +func ComputeEnvironmentDetailByName(conn *batch.Batch, name string) (*batch.ComputeEnvironmentDetail, error) { + input := &batch.DescribeComputeEnvironmentsInput{ + ComputeEnvironments: aws.StringSlice([]string{name}), + } + + computeEnvironmentDetail, err := ComputeEnvironmentDetail(conn, input) + + if err != nil { + return nil, err + } + + if status := aws.StringValue(computeEnvironmentDetail.Status); status == batch.CEStatusDeleted { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return computeEnvironmentDetail, nil +} + +func ComputeEnvironmentDetail(conn *batch.Batch, input *batch.DescribeComputeEnvironmentsInput) (*batch.ComputeEnvironmentDetail, error) { + output, err := conn.DescribeComputeEnvironments(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.ComputeEnvironments) == 0 || output.ComputeEnvironments[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO if len(output.ComputeEnvironments) > 1 + + return output.ComputeEnvironments[0], nil +} + +func JobDefinitionByARN(conn *batch.Batch, arn string) (*batch.JobDefinition, error) { + input := &batch.DescribeJobDefinitionsInput{ + JobDefinitions: aws.StringSlice([]string{arn}), + } + + jobDefinition, err := JobDefinition(conn, input) + + if err != nil { + return nil, err + } + + if status := aws.StringValue(jobDefinition.Status); status == tfbatch.JobDefinitionStatusInactive { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return jobDefinition, nil +} + +func JobDefinition(conn *batch.Batch, input *batch.DescribeJobDefinitionsInput) (*batch.JobDefinition, error) { + output, err := conn.DescribeJobDefinitions(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.JobDefinitions) == 0 || output.JobDefinitions[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO if len(output.JobDefinitions) > 1 + + return output.JobDefinitions[0], nil +} diff --git a/aws/internal/service/batch/waiter/status.go b/aws/internal/service/batch/waiter/status.go new file mode 100644 index 000000000000..819b38988116 --- /dev/null +++ b/aws/internal/service/batch/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/batch" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/batch/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ComputeEnvironmentStatus(conn *batch.Batch, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + computeEnvironmentDetail, err := finder.ComputeEnvironmentDetailByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return computeEnvironmentDetail, aws.StringValue(computeEnvironmentDetail.Status), nil + } +} diff --git a/aws/internal/service/batch/waiter/waiter.go b/aws/internal/service/batch/waiter/waiter.go new file mode 100644 index 000000000000..a42a37d35a9f --- /dev/null +++ b/aws/internal/service/batch/waiter/waiter.go @@ -0,0 +1,76 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/batch" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func ComputeEnvironmentCreated(conn *batch.Batch, name string, timeout time.Duration) (*batch.ComputeEnvironmentDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{batch.CEStatusCreating}, + Target: []string{batch.CEStatusValid}, + Refresh: ComputeEnvironmentStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*batch.ComputeEnvironmentDetail); ok { + return v, err + } + + return nil, err +} + +func ComputeEnvironmentDeleted(conn *batch.Batch, name string, timeout time.Duration) (*batch.ComputeEnvironmentDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{batch.CEStatusDeleting}, + Target: []string{}, + Refresh: ComputeEnvironmentStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*batch.ComputeEnvironmentDetail); ok { + return v, err + } + + return nil, err +} + +func ComputeEnvironmentDisabled(conn *batch.Batch, name string, timeout time.Duration) (*batch.ComputeEnvironmentDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{batch.CEStatusUpdating}, + Target: []string{batch.CEStatusValid, batch.CEStatusInvalid}, + Refresh: ComputeEnvironmentStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*batch.ComputeEnvironmentDetail); ok { + return v, err + } + + return nil, err +} + +func ComputeEnvironmentUpdated(conn *batch.Batch, name string, timeout time.Duration) (*batch.ComputeEnvironmentDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{batch.CEStatusUpdating}, + Target: []string{batch.CEStatusValid}, + Refresh: ComputeEnvironmentStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*batch.ComputeEnvironmentDetail); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/budgets/finder/finder.go b/aws/internal/service/budgets/finder/finder.go new file mode 100644 index 000000000000..d2f199313442 --- /dev/null +++ b/aws/internal/service/budgets/finder/finder.go @@ -0,0 +1,156 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func ActionByAccountIDActionIDAndBudgetName(conn *budgets.Budgets, accountID, actionID, budgetName string) (*budgets.Action, error) { + input := &budgets.DescribeBudgetActionInput{ + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + BudgetName: aws.String(budgetName), + } + + output, err := conn.DescribeBudgetAction(input) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Action == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Action, nil +} + +func BudgetByAccountIDAndBudgetName(conn *budgets.Budgets, accountID, budgetName string) (*budgets.Budget, error) { + input := &budgets.DescribeBudgetInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(budgetName), + } + + output, err := conn.DescribeBudget(input) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Budget == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Budget, nil +} + +func NotificationsByAccountIDAndBudgetName(conn *budgets.Budgets, accountID, budgetName string) ([]*budgets.Notification, error) { + input := &budgets.DescribeNotificationsForBudgetInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(budgetName), + } + var output []*budgets.Notification + + err := conn.DescribeNotificationsForBudgetPages(input, func(page *budgets.DescribeNotificationsForBudgetOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, notification := range page.Notifications { + if notification == nil { + continue + } + + output = append(output, notification) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if len(output) == 0 { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func SubscribersByAccountIDBudgetNameAndNotification(conn *budgets.Budgets, accountID, budgetName string, notification *budgets.Notification) ([]*budgets.Subscriber, error) { + input := &budgets.DescribeSubscribersForNotificationInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(budgetName), + Notification: notification, + } + var output []*budgets.Subscriber + + err := conn.DescribeSubscribersForNotificationPages(input, func(page *budgets.DescribeSubscribersForNotificationOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, subscriber := range page.Subscribers { + if subscriber == nil { + continue + } + + output = append(output, subscriber) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if len(output) == 0 { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/budgets/id.go b/aws/internal/service/budgets/id.go new file mode 100644 index 000000000000..f74bd5665e9e --- /dev/null +++ b/aws/internal/service/budgets/id.go @@ -0,0 +1,44 @@ +package budgets + +import ( + "fmt" + "strings" +) + +const budgetActionResourceIDSeparator = ":" + +func BudgetActionCreateResourceID(accountID, actionID, budgetName string) string { + parts := []string{accountID, actionID, budgetName} + id := strings.Join(parts, budgetActionResourceIDSeparator) + + return id +} + +func BudgetActionParseResourceID(id string) (string, string, string, error) { + parts := strings.Split(id, budgetActionResourceIDSeparator) + + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected AccountID%[2]sActionID%[2]sBudgetName", id, budgetActionResourceIDSeparator) +} + +const budgetResourceIDSeparator = ":" + +func BudgetCreateResourceID(accountID, budgetName string) string { + parts := []string{accountID, budgetName} + id := strings.Join(parts, budgetResourceIDSeparator) + + return id +} + +func BudgetParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, budgetResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected AccountID%[2]sBudgetName", id, budgetActionResourceIDSeparator) +} diff --git a/aws/internal/service/budgets/time.go b/aws/internal/service/budgets/time.go new file mode 100644 index 000000000000..ab2d4acac241 --- /dev/null +++ b/aws/internal/service/budgets/time.go @@ -0,0 +1,44 @@ +package budgets + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" +) + +const ( + timePeriodLayout = "2006-01-02_15:04" +) + +func TimePeriodTimestampFromString(s string) (*time.Time, error) { + if s == "" { + return nil, nil + } + + ts, err := time.Parse(timePeriodLayout, s) + + if err != nil { + return nil, err + } + + return aws.Time(ts), nil +} + +func TimePeriodTimestampToString(ts *time.Time) string { + if ts == nil { + return "" + } + + return aws.TimeValue(ts).Format(timePeriodLayout) +} + +func ValidateTimePeriodTimestamp(v interface{}, k string) (ws []string, errors []error) { + _, err := time.Parse(timePeriodLayout, v.(string)) + + if err != nil { + errors = append(errors, fmt.Errorf("%q cannot be parsed as %q: %w", k, timePeriodLayout, err)) + } + + return +} diff --git a/aws/internal/service/budgets/waiter/status.go b/aws/internal/service/budgets/waiter/status.go new file mode 100644 index 000000000000..22f5f2f3aa4f --- /dev/null +++ b/aws/internal/service/budgets/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ActionStatus(conn *budgets.Budgets, accountID, actionID, budgetName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ActionByAccountIDActionIDAndBudgetName(conn, accountID, actionID, budgetName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/budgets/waiter/waiter.go b/aws/internal/service/budgets/waiter/waiter.go new file mode 100644 index 000000000000..208f81336b28 --- /dev/null +++ b/aws/internal/service/budgets/waiter/waiter.go @@ -0,0 +1,36 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ActionAvailableTimeout = 2 * time.Minute +) + +func ActionAvailable(conn *budgets.Budgets, accountID, actionID, budgetName string) (*budgets.Action, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + budgets.ActionStatusExecutionInProgress, + budgets.ActionStatusStandby, + }, + Target: []string{ + budgets.ActionStatusExecutionSuccess, + budgets.ActionStatusExecutionFailure, + budgets.ActionStatusPending, + }, + Refresh: ActionStatus(conn, accountID, actionID, budgetName), + Timeout: ActionAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*budgets.Action); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/cloudcontrol/finder/finder.go b/aws/internal/service/cloudcontrol/finder/finder.go new file mode 100644 index 000000000000..3da4855c49a6 --- /dev/null +++ b/aws/internal/service/cloudcontrol/finder/finder.go @@ -0,0 +1,78 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ProgressEventByRequestToken(ctx context.Context, conn *cloudcontrolapi.CloudControlApi, requestToken string) (*cloudcontrolapi.ProgressEvent, error) { + input := &cloudcontrolapi.GetResourceRequestStatusInput{ + RequestToken: aws.String(requestToken), + } + + output, err := conn.GetResourceRequestStatusWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudcontrolapi.ErrCodeRequestTokenNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ProgressEvent == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ProgressEvent, nil +} + +func ResourceByID(ctx context.Context, conn *cloudcontrolapi.CloudControlApi, resourceID, typeName, typeVersionID, roleARN string) (*cloudcontrolapi.ResourceDescription, error) { + input := &cloudcontrolapi.GetResourceInput{ + Identifier: aws.String(resourceID), + TypeName: aws.String(typeName), + } + if roleARN != "" { + input.RoleArn = aws.String(roleARN) + } + if typeVersionID != "" { + input.TypeVersionId = aws.String(typeVersionID) + } + + output, err := conn.GetResourceWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudcontrolapi.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + // TEMPORARY: + // Some CloudFormation Resources do not correctly re-map "not found" errors, instead returning a HandlerFailureException. + // These should be reported and fixed upstream over time, but for now work around the issue. + if tfawserr.ErrMessageContains(err, cloudcontrolapi.ErrCodeHandlerFailureException, "not found") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ResourceDescription == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ResourceDescription, nil +} diff --git a/aws/internal/service/cloudcontrol/waiter/status.go b/aws/internal/service/cloudcontrol/waiter/status.go new file mode 100644 index 000000000000..cf622cad850c --- /dev/null +++ b/aws/internal/service/cloudcontrol/waiter/status.go @@ -0,0 +1,27 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudcontrol/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ProgressEventOperationStatus(ctx context.Context, conn *cloudcontrolapi.CloudControlApi, requestToken string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ProgressEventByRequestToken(ctx, conn, requestToken) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.OperationStatus), nil + } +} diff --git a/aws/internal/service/cloudcontrol/waiter/waiter.go b/aws/internal/service/cloudcontrol/waiter/waiter.go new file mode 100644 index 000000000000..f525aca9e2c8 --- /dev/null +++ b/aws/internal/service/cloudcontrol/waiter/waiter.go @@ -0,0 +1,33 @@ +package waiter + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ProgressEventOperationStatusSuccess(ctx context.Context, conn *cloudcontrolapi.CloudControlApi, requestToken string, timeout time.Duration) (*cloudcontrolapi.ProgressEvent, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudcontrolapi.OperationStatusInProgress, cloudcontrolapi.OperationStatusPending}, + Target: []string{cloudcontrolapi.OperationStatusSuccess}, + Refresh: ProgressEventOperationStatus(ctx, conn, requestToken), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*cloudcontrolapi.ProgressEvent); ok { + if operationStatus := aws.StringValue(output.OperationStatus); operationStatus == cloudcontrolapi.OperationStatusFailed { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(output.ErrorCode), aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/cloudformation/arn.go b/aws/internal/service/cloudformation/arn.go new file mode 100644 index 000000000000..55057b4809fc --- /dev/null +++ b/aws/internal/service/cloudformation/arn.go @@ -0,0 +1,51 @@ +package cloudformation + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +const ( + ARNSeparator = "/" + ARNService = "cloudformation" + + ResourcePrefixType = "type" +) + +// TypeVersionARNToTypeARNAndVersionID converts Type Version Amazon Resource Name (ARN) to Type ARN and Version ID. +// +// Given input: arn:aws:cloudformation:us-west-2:123456789012:type/resource/HashiCorp-TerraformAwsProvider-TfAccTestzwv6r2i7/00000001, +// returns arn:aws:cloudformation:us-west-2:123456789012:type/resource/HashiCorp-TerraformAwsProvider-TfAccTestzwv6r2i7 and 00000001. +func TypeVersionARNToTypeARNAndVersionID(inputARN string) (string, string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + resourceParts := strings.Split(parsedARN.Resource, ARNSeparator) + + if actual, expected := len(resourceParts), 4; actual != expected { + return "", "", fmt.Errorf("expected %d resource parts in ARN (%s), got: %d", expected, inputARN, actual) + } + + if actual, expected := resourceParts[0], ResourcePrefixType; actual != expected { + return "", "", fmt.Errorf("expected resource prefix %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + outputTypeARN := arn.ARN{ + Partition: parsedARN.Partition, + Service: parsedARN.Service, + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + Resource: strings.Join(resourceParts[:3], ARNSeparator), + }.String() + + return outputTypeARN, resourceParts[len(resourceParts)-1], nil +} diff --git a/aws/internal/service/cloudformation/arn_test.go b/aws/internal/service/cloudformation/arn_test.go new file mode 100644 index 000000000000..d9a7edc26c25 --- /dev/null +++ b/aws/internal/service/cloudformation/arn_test.go @@ -0,0 +1,76 @@ +package cloudformation_test + +import ( + "regexp" + "testing" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation" +) + +func TestTypeVersionARNToTypeARNAndVersionID(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedTypeARN string + ExpectedVersionID string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2:us-west-2:123456789012:instance/i-12345678", + ExpectedError: regexp.MustCompile(`expected service cloudformation`), + }, + { + TestName: "invalid ARN resource parts", + InputARN: "arn:aws:cloudformation:us-west-2:123456789012:type/resource/HashiCorp-TerraformAwsProvider-TfAccTestzwv6r2i7", + ExpectedError: regexp.MustCompile(`expected 4 resource parts`), + }, + { + TestName: "invalid ARN resource prefix", + InputARN: "arn:aws:cloudformation:us-west-2:123456789012:stack/name/1/2", + ExpectedError: regexp.MustCompile(`expected resource prefix type`), + }, + { + TestName: "valid ARN", + InputARN: "arn:aws:cloudformation:us-west-2:123456789012:type/resource/HashiCorp-TerraformAwsProvider-TfAccTestzwv6r2i7/00000001", + ExpectedTypeARN: "arn:aws:cloudformation:us-west-2:123456789012:type/resource/HashiCorp-TerraformAwsProvider-TfAccTestzwv6r2i7", + ExpectedVersionID: "00000001", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotTypeARN, gotVersionID, err := cloudformation.TypeVersionARNToTypeARNAndVersionID(testCase.InputARN) + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if gotTypeARN != testCase.ExpectedTypeARN { + t.Errorf("got %s, expected %s", gotTypeARN, testCase.ExpectedTypeARN) + } + + if gotVersionID != testCase.ExpectedVersionID { + t.Errorf("got %s, expected %s", gotVersionID, testCase.ExpectedVersionID) + } + }) + } +} diff --git a/aws/internal/service/cloudformation/errors.go b/aws/internal/service/cloudformation/errors.go new file mode 100644 index 000000000000..28e5473ed141 --- /dev/null +++ b/aws/internal/service/cloudformation/errors.go @@ -0,0 +1,32 @@ +package cloudformation + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + multierror "github.com/hashicorp/go-multierror" +) + +const ( + ErrCodeValidationError = "ValidationError" +) + +func StackSetOperationError(apiObjects []*cloudformation.StackSetOperationResultSummary) error { + var errors *multierror.Error + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + errors = multierror.Append(errors, fmt.Errorf("Account (%s) Region (%s) Status (%s) Status Reason: %s", + aws.StringValue(apiObject.Account), + aws.StringValue(apiObject.Region), + aws.StringValue(apiObject.Status), + aws.StringValue(apiObject.StatusReason), + )) + } + + return errors.ErrorOrNil() +} diff --git a/aws/internal/service/cloudformation/finder/finder.go b/aws/internal/service/cloudformation/finder/finder.go index 88f58fed5bab..467aac9b3fed 100644 --- a/aws/internal/service/cloudformation/finder/finder.go +++ b/aws/internal/service/cloudformation/finder/finder.go @@ -1,56 +1,224 @@ package finder import ( - "log" + "context" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfcloudformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -func Stack(conn *cloudformation.CloudFormation, stackID string) (*cloudformation.Stack, error) { +func ChangeSetByStackIDAndChangeSetName(conn *cloudformation.CloudFormation, stackID, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { + input := &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeSetName), + StackName: aws.String(stackID), + } + + output, err := conn.DescribeChangeSet(input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeChangeSetNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func StackByID(conn *cloudformation.CloudFormation, id string) (*cloudformation.Stack, error) { input := &cloudformation.DescribeStacksInput{ - StackName: aws.String(stackID), + StackName: aws.String(id), } - log.Printf("[DEBUG] Querying CloudFormation Stack: %s", input) - resp, err := conn.DescribeStacks(input) - if tfawserr.ErrCodeEquals(err, "ValidationError") { + + output, err := conn.DescribeStacks(input) + + if tfawserr.ErrMessageContains(err, tfcloudformation.ErrCodeValidationError, "does not exist") { return nil, &resource.NotFoundError{ - LastError: err, - LastRequest: input, - LastResponse: resp, + LastError: err, + LastRequest: input, } } + if err != nil { return nil, err } - if resp == nil { + if output == nil || len(output.Stacks) == 0 || output.Stacks[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Stacks); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + stack := output.Stacks[0] + + if status := aws.StringValue(stack.StackStatus); status == cloudformation.StackStatusDeleteComplete { return nil, &resource.NotFoundError{ - LastRequest: input, - LastResponse: resp, - Message: "returned empty response", + LastRequest: input, + Message: status, } + } + return stack, nil +} + +func StackInstanceByName(conn *cloudformation.CloudFormation, stackSetName, accountID, region string) (*cloudformation.StackInstance, error) { + input := &cloudformation.DescribeStackInstanceInput{ + StackInstanceAccount: aws.String(accountID), + StackInstanceRegion: aws.String(region), + StackSetName: aws.String(stackSetName), } - stacks := resp.Stacks - if len(stacks) < 1 { + + output, err := conn.DescribeStackInstance(input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { return nil, &resource.NotFoundError{ - LastRequest: input, - LastResponse: resp, - Message: "returned no results", + LastError: err, + LastRequest: input, } } - stack := stacks[0] - if aws.StringValue(stack.StackStatus) == cloudformation.StackStatusDeleteComplete { + if err != nil { + return nil, err + } + + if output == nil || output.StackInstance == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackInstance, nil +} + +func StackSetByName(conn *cloudformation.CloudFormation, name string) (*cloudformation.StackSet, error) { + input := &cloudformation.DescribeStackSetInput{ + StackSetName: aws.String(name), + } + + output, err := conn.DescribeStackSet(input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { return nil, &resource.NotFoundError{ - LastRequest: input, - LastResponse: resp, - Message: "CloudFormation Stack deleted", + LastError: err, + LastRequest: input, } } - return stack, nil + if err != nil { + return nil, err + } + + if output == nil || output.StackSet == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackSet, nil +} + +func StackSetOperationByStackSetNameAndOperationID(conn *cloudformation.CloudFormation, stackSetName, operationID string) (*cloudformation.StackSetOperation, error) { + input := &cloudformation.DescribeStackSetOperationInput{ + OperationId: aws.String(operationID), + StackSetName: aws.String(stackSetName), + } + + output, err := conn.DescribeStackSetOperation(input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeOperationNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.StackSetOperation == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.StackSetOperation, nil +} + +func TypeByARN(ctx context.Context, conn *cloudformation.CloudFormation, arn string) (*cloudformation.DescribeTypeOutput, error) { + input := &cloudformation.DescribeTypeInput{ + Arn: aws.String(arn), + } + + return Type(ctx, conn, input) +} + +func TypeByName(ctx context.Context, conn *cloudformation.CloudFormation, name string) (*cloudformation.DescribeTypeOutput, error) { + input := &cloudformation.DescribeTypeInput{ + Type: aws.String(cloudformation.RegistryTypeResource), + TypeName: aws.String(name), + } + + return Type(ctx, conn, input) +} + +func Type(ctx context.Context, conn *cloudformation.CloudFormation, input *cloudformation.DescribeTypeInput) (*cloudformation.DescribeTypeOutput, error) { + output, err := conn.DescribeTypeWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeTypeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if status := aws.StringValue(output.DeprecatedStatus); status == cloudformation.DeprecatedStatusDeprecated { + return nil, &resource.NotFoundError{ + LastRequest: input, + Message: status, + } + } + + return output, nil +} + +func TypeRegistrationByToken(ctx context.Context, conn *cloudformation.CloudFormation, registrationToken string) (*cloudformation.DescribeTypeRegistrationOutput, error) { + input := &cloudformation.DescribeTypeRegistrationInput{ + RegistrationToken: aws.String(registrationToken), + } + + output, err := conn.DescribeTypeRegistrationWithContext(ctx, input) + + if tfawserr.ErrMessageContains(err, cloudformation.ErrCodeCFNRegistryException, "No registration token matches") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil } diff --git a/aws/internal/service/cloudformation/id.go b/aws/internal/service/cloudformation/id.go new file mode 100644 index 000000000000..ad320b07bc36 --- /dev/null +++ b/aws/internal/service/cloudformation/id.go @@ -0,0 +1,25 @@ +package cloudformation + +import ( + "fmt" + "strings" +) + +const stackSetInstanceResourceIDSeparator = "," + +func StackSetInstanceCreateResourceID(stackSetName, accountID, region string) string { + parts := []string{stackSetName, accountID, region} + id := strings.Join(parts, stackSetInstanceResourceIDSeparator) + + return id +} + +func StackSetInstanceParseResourceID(id string) (string, string, string, error) { + parts := strings.Split(id, stackSetInstanceResourceIDSeparator) + + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected STACKSETNAME%[2]sACCOUNDID%[2]sREGION", id, stackSetInstanceResourceIDSeparator) +} diff --git a/aws/internal/service/cloudformation/waiter/status.go b/aws/internal/service/cloudformation/waiter/status.go index 25dcfc2e5e4a..f95d220c1c43 100644 --- a/aws/internal/service/cloudformation/waiter/status.go +++ b/aws/internal/service/cloudformation/waiter/status.go @@ -1,93 +1,44 @@ package waiter import ( - "fmt" - "log" - "strings" + "context" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func ChangeSetStatus(conn *cloudformation.CloudFormation, stackID, changeSetName string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - resp, err := conn.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ - ChangeSetName: aws.String(changeSetName), - StackName: aws.String(stackID), - }) - if err != nil { - log.Printf("[ERROR] Failed to describe CloudFormation change set: %s", err) - return nil, "", err - } + output, err := finder.ChangeSetByStackIDAndChangeSetName(conn, stackID, changeSetName) - if resp == nil { - log.Printf("[WARN] Describing CloudFormation change set returned no response") + if tfresource.NotFound(err) { return nil, "", nil } - status := aws.StringValue(resp.Status) + if err != nil { + return nil, "", err + } - return resp, status, err + return output, aws.StringValue(output.Status), nil } } func StackSetOperationStatus(conn *cloudformation.CloudFormation, stackSetName, operationID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &cloudformation.DescribeStackSetOperationInput{ - OperationId: aws.String(operationID), - StackSetName: aws.String(stackSetName), - } - - output, err := conn.DescribeStackSetOperation(input) + output, err := finder.StackSetOperationByStackSetNameAndOperationID(conn, stackSetName, operationID) - if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeOperationNotFoundException) { - return nil, cloudformation.StackSetOperationStatusRunning, nil + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return nil, cloudformation.StackSetOperationStatusFailed, err - } - - if output == nil || output.StackSetOperation == nil { - return nil, cloudformation.StackSetOperationStatusRunning, nil - } - - if aws.StringValue(output.StackSetOperation.Status) == cloudformation.StackSetOperationStatusFailed { - allResults := make([]string, 0) - listOperationResultsInput := &cloudformation.ListStackSetOperationResultsInput{ - OperationId: aws.String(operationID), - StackSetName: aws.String(stackSetName), - } - - // TODO: PAGES - for { - listOperationResultsOutput, err := conn.ListStackSetOperationResults(listOperationResultsInput) - - if err != nil { - return output.StackSetOperation, cloudformation.StackSetOperationStatusFailed, fmt.Errorf("error listing Operation (%s) errors: %w", operationID, err) - } - - if listOperationResultsOutput == nil { - continue - } - - for _, summary := range listOperationResultsOutput.Summaries { - allResults = append(allResults, fmt.Sprintf("Account (%s) Region (%s) Status (%s) Status Reason: %s", aws.StringValue(summary.Account), aws.StringValue(summary.Region), aws.StringValue(summary.Status), aws.StringValue(summary.StatusReason))) - } - - if aws.StringValue(listOperationResultsOutput.NextToken) == "" { - break - } - - listOperationResultsInput.NextToken = listOperationResultsOutput.NextToken - } - - return output.StackSetOperation, cloudformation.StackSetOperationStatusFailed, fmt.Errorf("Operation (%s) Results:\n%s", operationID, strings.Join(allResults, "\n")) + return nil, "", err } - return output.StackSetOperation, aws.StringValue(output.StackSetOperation.Status), nil + return output, aws.StringValue(output.Status), nil } } @@ -112,3 +63,19 @@ func StackStatus(conn *cloudformation.CloudFormation, stackName string) resource return resp.Stacks[0], aws.StringValue(resp.Stacks[0].StackStatus), err } } + +func TypeRegistrationProgressStatus(ctx context.Context, conn *cloudformation.CloudFormation, registrationToken string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.TypeRegistrationByToken(ctx, conn, registrationToken) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ProgressStatus), nil + } +} diff --git a/aws/internal/service/cloudformation/waiter/waiter.go b/aws/internal/service/cloudformation/waiter/waiter.go index 39d0aacaf237..3f4dc21b973e 100644 --- a/aws/internal/service/cloudformation/waiter/waiter.go +++ b/aws/internal/service/cloudformation/waiter/waiter.go @@ -1,44 +1,43 @@ package waiter import ( + "context" + "errors" "fmt" - "log" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfcloudformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/lister" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - // Maximum amount of time to wait for a Change Set to be Created ChangeSetCreatedTimeout = 5 * time.Minute ) func ChangeSetCreated(conn *cloudformation.CloudFormation, stackID, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { stateConf := resource.StateChangeConf{ - Pending: []string{ - cloudformation.ChangeSetStatusCreatePending, - cloudformation.ChangeSetStatusCreateInProgress, - }, - Target: []string{ - cloudformation.ChangeSetStatusCreateComplete, - }, + Pending: []string{cloudformation.ChangeSetStatusCreateInProgress, cloudformation.ChangeSetStatusCreatePending}, + Target: []string{cloudformation.ChangeSetStatusCreateComplete}, Timeout: ChangeSetCreatedTimeout, Refresh: ChangeSetStatus(conn, stackID, changeSetName), } + outputRaw, err := stateConf.WaitForState() - if err != nil { - return nil, err - } - changeSet, ok := outputRaw.(*cloudformation.DescribeChangeSetOutput) - if !ok { - return nil, err + if output, ok := outputRaw.(*cloudformation.DescribeChangeSetOutput); ok { + if status := aws.StringValue(output.Status); status == cloudformation.ChangeSetStatusFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusReason))) + } + + return output, err } - return changeSet, err + + return nil, err } const ( @@ -59,7 +58,7 @@ const ( StackSetUpdatedDefaultTimeout = 30 * time.Minute ) -func StackSetOperationSucceeded(conn *cloudformation.CloudFormation, stackSetName, operationID string, timeout time.Duration) error { +func StackSetOperationSucceeded(conn *cloudformation.CloudFormation, stackSetName, operationID string, timeout time.Duration) (*cloudformation.StackSetOperation, error) { stateConf := &resource.StateChangeConf{ Pending: []string{cloudformation.StackSetOperationStatusRunning}, Target: []string{cloudformation.StackSetOperationStatusSucceeded}, @@ -68,10 +67,37 @@ func StackSetOperationSucceeded(conn *cloudformation.CloudFormation, stackSetNam Delay: stackSetOperationDelay, } - log.Printf("[DEBUG] Waiting for CloudFormation StackSet (%s) operation: %s", stackSetName, operationID) - _, err := stateConf.WaitForState() + outputRaw, waitErr := stateConf.WaitForState() + + if output, ok := outputRaw.(*cloudformation.StackSetOperation); ok { + if status := aws.StringValue(output.Status); status == cloudformation.StackSetOperationStatusFailed { + input := &cloudformation.ListStackSetOperationResultsInput{ + OperationId: aws.String(operationID), + StackSetName: aws.String(stackSetName), + } + var summaries []*cloudformation.StackSetOperationResultSummary + + listErr := conn.ListStackSetOperationResultsPages(input, func(page *cloudformation.ListStackSetOperationResultsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + summaries = append(summaries, page.Summaries...) + + return !lastPage + }) - return err + if listErr == nil { + tfresource.SetLastError(waitErr, fmt.Errorf("Operation (%s) Results: %w", operationID, tfcloudformation.StackSetOperationError(summaries))) + } else { + tfresource.SetLastError(waitErr, fmt.Errorf("error listing CloudFormation Stack Set (%s) Operation (%s) results: %w", stackSetName, operationID, listErr)) + } + } + + return output, waitErr + } + + return nil, waitErr } const ( @@ -235,6 +261,27 @@ func StackDeleted(conn *cloudformation.CloudFormation, stackID, requestToken str return stack, nil } +const ( + TypeRegistrationTimeout = 5 * time.Minute +) + +func TypeRegistrationProgressStatusComplete(ctx context.Context, conn *cloudformation.CloudFormation, registrationToken string) (*cloudformation.DescribeTypeRegistrationOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudformation.RegistrationStatusInProgress}, + Target: []string{cloudformation.RegistrationStatusComplete}, + Refresh: TypeRegistrationProgressStatus(ctx, conn, registrationToken), + Timeout: TypeRegistrationTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*cloudformation.DescribeTypeRegistrationOutput); ok { + return output, err + } + + return nil, err +} + func getCloudFormationDeletionReasons(conn *cloudformation.CloudFormation, stackID, requestToken string) ([]string, error) { var failures []string diff --git a/aws/internal/service/cloudfront/finder/finder.go b/aws/internal/service/cloudfront/finder/finder.go index baabd19f2fdc..3f6a532602ea 100644 --- a/aws/internal/service/cloudfront/finder/finder.go +++ b/aws/internal/service/cloudfront/finder/finder.go @@ -3,8 +3,39 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func FunctionByNameAndStage(conn *cloudfront.CloudFront, name, stage string) (*cloudfront.DescribeFunctionOutput, error) { + input := &cloudfront.DescribeFunctionInput{ + Name: aws.String(name), + Stage: aws.String(stage), + } + + output, err := conn.DescribeFunction(input) + + if tfawserr.ErrCodeEquals(err, cloudfront.ErrCodeNoSuchFunctionExists) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + // RealtimeLogConfigByARN returns the real-time log configuration corresponding to the specified ARN. // Returns nil if no configuration is found. func RealtimeLogConfigByARN(conn *cloudfront.CloudFront, arn string) (*cloudfront.RealtimeLogConfig, error) { @@ -23,3 +54,22 @@ func RealtimeLogConfigByARN(conn *cloudfront.CloudFront, arn string) (*cloudfron return output.RealtimeLogConfig, nil } + +// MonitoringSubscriptionByDistributionId returns the monitoring subscription corresponding to the specified distribution id. +// Returns nil if no subscription is found. +func MonitoringSubscriptionByDistributionId(conn *cloudfront.CloudFront, id string) (*cloudfront.MonitoringSubscription, error) { + input := &cloudfront.GetMonitoringSubscriptionInput{ + DistributionId: aws.String(id), + } + + output, err := conn.GetMonitoringSubscription(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.MonitoringSubscription, nil +} diff --git a/aws/internal/service/cloudfront/lister/list.go b/aws/internal/service/cloudfront/lister/list.go new file mode 100644 index 000000000000..69d4516b8456 --- /dev/null +++ b/aws/internal/service/cloudfront/lister/list.go @@ -0,0 +1,25 @@ +package lister + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudfront" +) + +// Custom Cloudfront listing functions using similar formatting as other service generated code. + +func ListFunctionsPages(conn *cloudfront.CloudFront, input *cloudfront.ListFunctionsInput, fn func(*cloudfront.ListFunctionsOutput, bool) bool) error { + for { + output, err := conn.ListFunctions(input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.FunctionList.NextMarker) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.Marker = output.FunctionList.NextMarker + } + return nil +} diff --git a/aws/internal/service/cloudhsmv2/finder/finder.go b/aws/internal/service/cloudhsmv2/finder/finder.go new file mode 100644 index 000000000000..eed75c6f4b73 --- /dev/null +++ b/aws/internal/service/cloudhsmv2/finder/finder.go @@ -0,0 +1,81 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" +) + +func Cluster(conn *cloudhsmv2.CloudHSMV2, id string) (*cloudhsmv2.Cluster, error) { + input := &cloudhsmv2.DescribeClustersInput{ + Filters: map[string][]*string{ + "clusterIds": aws.StringSlice([]string{id}), + }, + } + + var result *cloudhsmv2.Cluster + + err := conn.DescribeClustersPages(input, func(page *cloudhsmv2.DescribeClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, cluster := range page.Clusters { + if cluster == nil { + continue + } + + if aws.StringValue(cluster.ClusterId) == id { + result = cluster + break + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +func Hsm(conn *cloudhsmv2.CloudHSMV2, hsmID string, eniID string) (*cloudhsmv2.Hsm, error) { + input := &cloudhsmv2.DescribeClustersInput{} + + var result *cloudhsmv2.Hsm + + err := conn.DescribeClustersPages(input, func(page *cloudhsmv2.DescribeClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, cluster := range page.Clusters { + if cluster == nil { + continue + } + + for _, hsm := range cluster.Hsms { + if hsm == nil { + continue + } + + // CloudHSMv2 HSM instances can be recreated, but the ENI ID will + // remain consistent. Without this ENI matching, HSM instances + // instances can become orphaned. + if aws.StringValue(hsm.HsmId) == hsmID || aws.StringValue(hsm.EniId) == eniID { + result = hsm + return false + } + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/aws/internal/service/cloudhsmv2/waiter/status.go b/aws/internal/service/cloudhsmv2/waiter/status.go new file mode 100644 index 000000000000..fbe47bff298c --- /dev/null +++ b/aws/internal/service/cloudhsmv2/waiter/status.go @@ -0,0 +1,40 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudhsmv2/finder" +) + +func ClusterState(conn *cloudhsmv2.CloudHSMV2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + cluster, err := finder.Cluster(conn, id) + + if err != nil { + return nil, "", err + } + + if cluster == nil { + return nil, "", nil + } + + return cluster, aws.StringValue(cluster.State), err + } +} + +func HsmState(conn *cloudhsmv2.CloudHSMV2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + hsm, err := finder.Hsm(conn, id, "") + + if err != nil { + return nil, "", err + } + + if hsm == nil { + return nil, "", nil + } + + return hsm, aws.StringValue(hsm.State), err + } +} diff --git a/aws/internal/service/cloudhsmv2/waiter/waiter.go b/aws/internal/service/cloudhsmv2/waiter/waiter.go new file mode 100644 index 000000000000..590f6ec787ec --- /dev/null +++ b/aws/internal/service/cloudhsmv2/waiter/waiter.go @@ -0,0 +1,109 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/cloudhsmv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func ClusterActive(conn *cloudhsmv2.CloudHSMV2, id string, timeout time.Duration) (*cloudhsmv2.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + cloudhsmv2.ClusterStateCreateInProgress, + cloudhsmv2.ClusterStateInitializeInProgress, + }, + Target: []string{cloudhsmv2.ClusterStateActive}, + Refresh: ClusterState(conn, id), + Timeout: timeout, + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*cloudhsmv2.Cluster); ok { + return v, err + } + + return nil, err +} + +func ClusterDeleted(conn *cloudhsmv2.CloudHSMV2, id string, timeout time.Duration) (*cloudhsmv2.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.ClusterStateDeleteInProgress}, + Target: []string{cloudhsmv2.ClusterStateDeleted}, + Refresh: ClusterState(conn, id), + Timeout: timeout, + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*cloudhsmv2.Cluster); ok { + return v, err + } + + return nil, err +} + +func ClusterUninitialized(conn *cloudhsmv2.CloudHSMV2, id string, timeout time.Duration) (*cloudhsmv2.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + cloudhsmv2.ClusterStateCreateInProgress, + cloudhsmv2.ClusterStateInitializeInProgress, + }, + Target: []string{cloudhsmv2.ClusterStateUninitialized}, + Refresh: ClusterState(conn, id), + Timeout: timeout, + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*cloudhsmv2.Cluster); ok { + return v, err + } + + return nil, err +} + +func HsmActive(conn *cloudhsmv2.CloudHSMV2, id string, timeout time.Duration) (*cloudhsmv2.Hsm, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.HsmStateCreateInProgress}, + Target: []string{cloudhsmv2.HsmStateActive}, + Refresh: HsmState(conn, id), + Timeout: timeout, + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*cloudhsmv2.Hsm); ok { + return v, err + } + + return nil, err +} + +func HsmDeleted(conn *cloudhsmv2.CloudHSMV2, id string, timeout time.Duration) (*cloudhsmv2.Hsm, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{cloudhsmv2.HsmStateDeleteInProgress}, + Target: []string{}, + Refresh: HsmState(conn, id), + Timeout: timeout, + MinTimeout: 30 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*cloudhsmv2.Hsm); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/cloudtrail/enum.go b/aws/internal/service/cloudtrail/enum.go new file mode 100644 index 000000000000..cf30153a419a --- /dev/null +++ b/aws/internal/service/cloudtrail/enum.go @@ -0,0 +1,35 @@ +package cloudtrail + +const ( + ResourceTypeDynamoDBTable = "AWS::DynamoDB::Table" + ResourceTypeLambdaFunction = "AWS::Lambda::Function" + ResourceTypeS3Object = "AWS::S3::Object" +) + +func ResourceType_Values() []string { + return []string{ + ResourceTypeDynamoDBTable, + ResourceTypeLambdaFunction, + ResourceTypeS3Object, + } +} + +const ( + FieldEventCategory = "eventCategory" + FieldEventName = "eventName" + FieldEventSource = "eventSource" + FieldReadOnly = "readOnly" + FieldResourcesARN = "resources.ARN" + FieldResourcesType = "resources.type" +) + +func Field_Values() []string { + return []string{ + FieldEventCategory, + FieldEventName, + FieldEventSource, + FieldReadOnly, + FieldResourcesARN, + FieldResourcesType, + } +} diff --git a/aws/internal/service/cloudwatch/finder/finder.go b/aws/internal/service/cloudwatch/finder/finder.go index 1de5bf2c9d68..e78cf2ca9138 100644 --- a/aws/internal/service/cloudwatch/finder/finder.go +++ b/aws/internal/service/cloudwatch/finder/finder.go @@ -24,3 +24,21 @@ func CompositeAlarmByName(ctx context.Context, conn *cloudwatch.CloudWatch, name return output.CompositeAlarms[0], nil } + +func MetricAlarmByName(conn *cloudwatch.CloudWatch, name string) (*cloudwatch.MetricAlarm, error) { + input := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(name)}, + AlarmTypes: aws.StringSlice([]string{cloudwatch.AlarmTypeMetricAlarm}), + } + + output, err := conn.DescribeAlarms(&input) + if err != nil { + return nil, err + } + + if output == nil || len(output.MetricAlarms) != 1 { + return nil, nil + } + + return output.MetricAlarms[0], nil +} diff --git a/aws/internal/service/cloudwatch/waiter/status.go b/aws/internal/service/cloudwatch/waiter/status.go new file mode 100644 index 000000000000..fc84d760e857 --- /dev/null +++ b/aws/internal/service/cloudwatch/waiter/status.go @@ -0,0 +1,28 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func MetricStreamState(ctx context.Context, conn *cloudwatch.CloudWatch, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := cloudwatch.GetMetricStreamInput{ + Name: aws.String(name), + } + + metricStream, err := conn.GetMetricStreamWithContext(ctx, &input) + if err != nil { + if tfawserr.ErrCodeEquals(err, cloudwatch.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + return nil, "", err + } + + return metricStream, aws.StringValue(metricStream.State), err + } +} diff --git a/aws/internal/service/cloudwatch/waiter/waiter.go b/aws/internal/service/cloudwatch/waiter/waiter.go new file mode 100644 index 000000000000..a09a97e61f17 --- /dev/null +++ b/aws/internal/service/cloudwatch/waiter/waiter.go @@ -0,0 +1,58 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + MetricStreamDeleteTimeout = 2 * time.Minute + MetricStreamReadyTimeout = 1 * time.Minute + + StateRunning = "running" + StateStopped = "stopped" +) + +func MetricStreamDeleted(ctx context.Context, conn *cloudwatch.CloudWatch, name string) (*cloudwatch.GetMetricStreamOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + StateRunning, + StateStopped, + }, + Target: []string{}, + Refresh: MetricStreamState(ctx, conn, name), + Timeout: MetricStreamDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if v, ok := outputRaw.(*cloudwatch.GetMetricStreamOutput); ok { + return v, err + } + + return nil, err +} + +func MetricStreamReady(ctx context.Context, conn *cloudwatch.CloudWatch, name string) (*cloudwatch.GetMetricStreamOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + StateStopped, + }, + Target: []string{ + StateRunning, + }, + Refresh: MetricStreamState(ctx, conn, name), + Timeout: MetricStreamReadyTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if v, ok := outputRaw.(*cloudwatch.GetMetricStreamOutput); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/cloudwatchevents/enum.go b/aws/internal/service/cloudwatchevents/enum.go new file mode 100644 index 000000000000..a4ed557c839b --- /dev/null +++ b/aws/internal/service/cloudwatchevents/enum.go @@ -0,0 +1,5 @@ +package cloudwatchevents + +const ( + DefaultEventBusName = "default" +) diff --git a/aws/internal/service/cloudwatchevents/finder/finder.go b/aws/internal/service/cloudwatchevents/finder/finder.go index 14b1829448c7..65dcf4459d7d 100644 --- a/aws/internal/service/cloudwatchevents/finder/finder.go +++ b/aws/internal/service/cloudwatchevents/finder/finder.go @@ -5,29 +5,80 @@ import ( "github.com/aws/aws-sdk-go/aws" events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/lister" ) -func Rule(conn *events.CloudWatchEvents, eventBusName, ruleName string) (*events.DescribeRuleOutput, error) { +func ConnectionByName(conn *events.CloudWatchEvents, name string) (*events.DescribeConnectionOutput, error) { + input := &events.DescribeConnectionInput{ + Name: aws.String(name), + } + + output, err := conn.DescribeConnection(input) + + if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func RuleByEventBusAndRuleNames(conn *events.CloudWatchEvents, eventBusName, ruleName string) (*events.DescribeRuleOutput, error) { input := events.DescribeRuleInput{ Name: aws.String(ruleName), } + if eventBusName != "" { input.EventBusName = aws.String(eventBusName) } - return conn.DescribeRule(&input) + output, err := conn.DescribeRule(&input) + if tfawserr.ErrCodeEquals(err, events.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil } -func RuleByID(conn *events.CloudWatchEvents, ruleID string) (*events.DescribeRuleOutput, error) { - busName, ruleName, err := tfevents.RuleParseID(ruleID) +func RuleByResourceID(conn *events.CloudWatchEvents, id string) (*events.DescribeRuleOutput, error) { + eventBusName, ruleName, err := tfevents.RuleParseResourceID(id) + if err != nil { return nil, err } - return Rule(conn, busName, ruleName) + return RuleByEventBusAndRuleNames(conn, eventBusName, ruleName) } func Target(conn *events.CloudWatchEvents, busName, ruleName, targetId string) (*events.Target, error) { diff --git a/aws/internal/service/cloudwatchevents/id.go b/aws/internal/service/cloudwatchevents/id.go index 0da79c7054cb..34a73a59a703 100644 --- a/aws/internal/service/cloudwatchevents/id.go +++ b/aws/internal/service/cloudwatchevents/id.go @@ -2,22 +2,31 @@ package cloudwatchevents import ( "fmt" + "regexp" "strings" ) -const DefaultEventBusName = "default" +var ( + eventBusARNPattern = regexp.MustCompile(`^arn:aws[\w-]*:events:[a-z]{2}-[a-z]+-[\w-]+:[0-9]{12}:event-bus\/[\.\-_A-Za-z0-9]+$`) + partnerEventBusPattern = regexp.MustCompile(`^aws\.partner(/[\.\-_A-Za-z0-9]+){2,}$`) +) -const PermissionIDSeparator = "/" +const permissionResourceIDSeparator = "/" -func PermissionCreateID(eventBusName, statementID string) string { +func PermissionCreateResourceID(eventBusName, statementID string) string { if eventBusName == "" || eventBusName == DefaultEventBusName { return statementID } - return eventBusName + PermissionIDSeparator + statementID + + parts := []string{eventBusName, statementID} + id := strings.Join(parts, permissionResourceIDSeparator) + + return id } -func PermissionParseID(id string) (string, string, error) { - parts := strings.Split(id, PermissionIDSeparator) +func PermissionParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, permissionResourceIDSeparator) + if len(parts) == 1 && parts[0] != "" { return DefaultEventBusName, parts[0], nil } @@ -25,52 +34,87 @@ func PermissionParseID(id string) (string, string, error) { return parts[0], parts[1], nil } - return "", "", fmt.Errorf("unexpected format for ID (%q), expected "+PermissionIDSeparator+" or ", id) + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sSTATEMENTID or STATEMENTID", id, permissionResourceIDSeparator) } -const ruleIDSeparator = "/" +const ruleResourceIDSeparator = "/" -func RuleCreateID(eventBusName, ruleName string) string { +func RuleCreateResourceID(eventBusName, ruleName string) string { if eventBusName == "" || eventBusName == DefaultEventBusName { return ruleName } - return eventBusName + ruleIDSeparator + ruleName + + parts := []string{eventBusName, ruleName} + id := strings.Join(parts, ruleResourceIDSeparator) + + return id } -func RuleParseID(id string) (string, string, error) { - parts := strings.Split(id, ruleIDSeparator) +func RuleParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, ruleResourceIDSeparator) + if len(parts) == 1 && parts[0] != "" { return DefaultEventBusName, parts[0], nil } if len(parts) == 2 && parts[0] != "" && parts[1] != "" { return parts[0], parts[1], nil } + if len(parts) > 2 { + i := strings.LastIndex(id, ruleResourceIDSeparator) + eventBusName := id[:i] + ruleName := id[i+1:] + if eventBusARNPattern.MatchString(eventBusName) && ruleName != "" { + return eventBusName, ruleName, nil + } + if partnerEventBusPattern.MatchString(eventBusName) && ruleName != "" { + return eventBusName, ruleName, nil + } + } - return "", "", fmt.Errorf("unexpected format for ID (%q), expected "+ruleIDSeparator+" or ", id) + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sRULENAME or RULENAME", id, ruleResourceIDSeparator) } -// Terraform state IDs for Targets are not parseable, since the separator used ("-") is also a valid -// character in both the rule name and the target id. +// Terraform resource IDs for Targets are not parseable as the separator used ("-") is also a valid character in both the rule name and the target ID. -const targetIDSeparator = "-" +const targetResourceIDSeparator = "-" const targetImportIDSeparator = "/" -func TargetCreateID(eventBusName, ruleName, targetID string) string { - id := ruleName + targetIDSeparator + targetID - if eventBusName != "" && eventBusName != DefaultEventBusName { - id = eventBusName + targetIDSeparator + id +func TargetCreateResourceID(eventBusName, ruleName, targetID string) string { + var parts []string + + if eventBusName == "" || eventBusName == DefaultEventBusName { + parts = []string{ruleName, targetID} + } else { + parts = []string{eventBusName, ruleName, targetID} } + + id := strings.Join(parts, targetResourceIDSeparator) + return id } func TargetParseImportID(id string) (string, string, string, error) { parts := strings.Split(id, targetImportIDSeparator) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { return DefaultEventBusName, parts[0], parts[1], nil } if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { return parts[0], parts[1], parts[2], nil } + if len(parts) > 3 { + iTarget := strings.LastIndex(id, targetImportIDSeparator) + targetID := id[iTarget+1:] + iRule := strings.LastIndex(id[:iTarget], targetImportIDSeparator) + eventBusName := id[:iRule] + ruleName := id[iRule+1 : iTarget] + if eventBusARNPattern.MatchString(eventBusName) && ruleName != "" && targetID != "" { + return eventBusName, ruleName, targetID, nil + } + if partnerEventBusPattern.MatchString(eventBusName) && ruleName != "" && targetID != "" { + return eventBusName, ruleName, targetID, nil + } + } - return "", "", "", fmt.Errorf("unexpected format for ID (%q), expected "+targetImportIDSeparator+""+targetImportIDSeparator+" or "+targetImportIDSeparator+"", id) + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EVENTBUSNAME%[2]sRULENAME%[2]sTARGETID or RULENAME%[2]sTARGETID", id, targetImportIDSeparator) } diff --git a/aws/internal/service/cloudwatchevents/id_test.go b/aws/internal/service/cloudwatchevents/id_test.go new file mode 100644 index 000000000000..95009010463c --- /dev/null +++ b/aws/internal/service/cloudwatchevents/id_test.go @@ -0,0 +1,350 @@ +package cloudwatchevents_test + +import ( + "testing" + + tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" +) + +func TestPermissionParseResourceID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectedError bool + ExpectedPart0 string + ExpectedPart1 string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectedError: true, + }, + { + TestName: "single part", + InputID: "TestStatement", + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestStatement", + }, + { + TestName: "two parts", + InputID: tfevents.PermissionCreateResourceID("TestEventBus", "TestStatement"), + ExpectedPart0: "TestEventBus", + ExpectedPart1: "TestStatement", + }, + { + TestName: "two parts with default event bus", + InputID: tfevents.PermissionCreateResourceID(tfevents.DefaultEventBusName, "TestStatement"), + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestStatement", + }, + { + TestName: "partner event bus", + InputID: "aws.partner/example.com/Test/TestStatement", + ExpectedError: true, + }, + { + TestName: "empty both parts", + InputID: "/", + ExpectedError: true, + }, + { + TestName: "empty first part", + InputID: "/TestStatement", + ExpectedError: true, + }, + { + TestName: "empty second part", + InputID: "TestEventBus/", + ExpectedError: true, + }, + { + TestName: "three parts", + InputID: "TestEventBus/TestStatement/Suffix", + ExpectedError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotPart0, gotPart1, err := tfevents.PermissionParseResourceID(testCase.InputID) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotPart0 != testCase.ExpectedPart0 { + t.Errorf("got part 0 %s, expected %s", gotPart0, testCase.ExpectedPart0) + } + + if gotPart1 != testCase.ExpectedPart1 { + t.Errorf("got part 1 %s, expected %s", gotPart1, testCase.ExpectedPart1) + } + }) + } +} + +func TestRuleParseResourceID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectedError bool + ExpectedPart0 string + ExpectedPart1 string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectedError: true, + }, + { + TestName: "single part", + InputID: "TestRule", + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestRule", + }, + { + TestName: "two parts", + InputID: tfevents.RuleCreateResourceID("TestEventBus", "TestRule"), + ExpectedPart0: "TestEventBus", + ExpectedPart1: "TestRule", + }, + { + TestName: "two parts with default event bus", + InputID: tfevents.RuleCreateResourceID(tfevents.DefaultEventBusName, "TestRule"), + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestRule", + }, + { + TestName: "partner event bus 1", + InputID: "aws.partner/example.com/Test/TestRule", + ExpectedPart0: "aws.partner/example.com/Test", + ExpectedPart1: "TestRule", + }, + { + TestName: "partner event bus 2", + InputID: "aws.partner/foo.com/foo/18554d09-58ff-aa42-ba9c-c4c33899006f/test", + ExpectedPart0: "aws.partner/foo.com/foo/18554d09-58ff-aa42-ba9c-c4c33899006f", + ExpectedPart1: "test", + }, + { + TestName: "ARN event bus", + InputID: tfevents.RuleCreateResourceID("arn:aws:events:us-east-2:123456789012:event-bus/default", "TestRule"), + ExpectedPart0: "arn:aws:events:us-east-2:123456789012:event-bus/default", + ExpectedPart1: "TestRule", + }, + { + TestName: "empty both parts", + InputID: "/", + ExpectedError: true, + }, + { + TestName: "empty first part", + InputID: "/TestRule", + ExpectedError: true, + }, + { + TestName: "empty second part", + InputID: "TestEventBus/", + ExpectedError: true, + }, + { + TestName: "empty partner event rule", + InputID: "aws.partner/example.com/Test/", + ExpectedError: true, + }, + { + TestName: "three parts", + InputID: "TestEventBus/TestRule/Suffix", + ExpectedError: true, + }, + { + TestName: "four parts", + InputID: "abc.partner/TestEventBus/TestRule/Suffix", + ExpectedError: true, + }, + { + TestName: "five parts", + InputID: "test/aws.partner/example.com/Test/TestRule", + ExpectedError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotPart0, gotPart1, err := tfevents.RuleParseResourceID(testCase.InputID) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotPart0 != testCase.ExpectedPart0 { + t.Errorf("got part 0 %s, expected %s", gotPart0, testCase.ExpectedPart0) + } + + if gotPart1 != testCase.ExpectedPart1 { + t.Errorf("got part 1 %s, expected %s", gotPart1, testCase.ExpectedPart1) + } + }) + } +} + +func TestTargetParseImportID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectedError bool + ExpectedPart0 string + ExpectedPart1 string + ExpectedPart2 string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectedError: true, + }, + { + TestName: "single part", + InputID: "TestRule", + ExpectedError: true, + }, + { + TestName: "two parts", + InputID: "TestTarget/TestRule", + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestTarget", + ExpectedPart2: "TestRule", + }, + { + TestName: "three parts", + InputID: "TestEventBus/TestRule/TestTarget", + ExpectedPart0: "TestEventBus", + ExpectedPart1: "TestRule", + ExpectedPart2: "TestTarget", + }, + { + TestName: "three parts with default event bus", + InputID: tfevents.DefaultEventBusName + "/TestRule/TestTarget", + ExpectedPart0: tfevents.DefaultEventBusName, + ExpectedPart1: "TestRule", + ExpectedPart2: "TestTarget", + }, + { + TestName: "empty two parts", + InputID: "/", + ExpectedError: true, + }, + { + TestName: "empty three parts", + InputID: "//", + ExpectedError: true, + }, + { + TestName: "empty first part of two", + InputID: "/TestTarget", + ExpectedError: true, + }, + { + TestName: "empty second part of two", + InputID: "TestRule/", + ExpectedError: true, + }, + { + TestName: "empty first part of three", + InputID: "/TestRule/TestTarget", + ExpectedError: true, + }, + { + TestName: "empty second part of three", + InputID: "TestEventBus//TestTarget", + ExpectedError: true, + }, + { + TestName: "empty third part of three", + InputID: "TestEventBus/TestRule/", + ExpectedError: true, + }, + { + TestName: "empty first two of three parts", + InputID: "//TestTarget", + ExpectedError: true, + }, + { + TestName: "empty first and third of three parts", + InputID: "/TestRule/", + ExpectedError: true, + }, + { + TestName: "empty final two of three parts", + InputID: "TestEventBus//", + ExpectedError: true, + }, + { + TestName: "partner event bus", + InputID: "aws.partner/example.com/Test/TestRule/TestTarget", + ExpectedPart0: "aws.partner/example.com/Test", + ExpectedPart1: "TestRule", + ExpectedPart2: "TestTarget", + }, + { + TestName: "ARN event bus", + InputID: "arn:aws:events:us-east-2:123456789012:event-bus/default/TestRule/TestTarget", + ExpectedPart0: "arn:aws:events:us-east-2:123456789012:event-bus/default", + ExpectedPart1: "TestRule", + ExpectedPart2: "TestTarget", + }, + { + TestName: "empty partner event rule and target", + InputID: "aws.partner/example.com/Test//", + ExpectedError: true, + }, + { + TestName: "four parts", + InputID: "aws.partner/example.com/Test/TestRule", + ExpectedError: true, + }, + { + TestName: "five parts", + InputID: "abc.partner/example.com/Test/TestRule/TestTarget", + ExpectedError: true, + }, + { + TestName: "six parts", + InputID: "test/aws.partner/example.com/Test/TestRule/TestTarget", + ExpectedError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotPart0, gotPart1, gotPart2, err := tfevents.TargetParseImportID(testCase.InputID) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotPart0 != testCase.ExpectedPart0 { + t.Errorf("got part 0 %s, expected %s", gotPart0, testCase.ExpectedPart0) + } + + if gotPart1 != testCase.ExpectedPart1 { + t.Errorf("got part 1 %s, expected %s", gotPart1, testCase.ExpectedPart1) + } + + if gotPart2 != testCase.ExpectedPart2 { + t.Errorf("got part 2 %s, expected %s", gotPart2, testCase.ExpectedPart2) + } + }) + } +} diff --git a/aws/internal/service/cloudwatchevents/state.go b/aws/internal/service/cloudwatchevents/state.go new file mode 100644 index 000000000000..915ff9618b91 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/state.go @@ -0,0 +1,31 @@ +package cloudwatchevents + +import ( + "fmt" + + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" +) + +// RuleEnabledFromState infers from its state whether or not a rule is enabled. +func RuleEnabledFromState(state string) (bool, error) { + if state == events.RuleStateEnabled { + return true, nil + } + + if state == events.RuleStateDisabled { + return false, nil + } + + // We don't just blindly trust AWS as they tend to return + // unexpected values in similar cases (different casing etc.) + return false, fmt.Errorf("unable to infer enabled from state: %s", state) +} + +// RuleStateFromEnabled returns a rule's state based on whether or not it is enabled. +func RuleStateFromEnabled(enabled bool) string { + if enabled { + return events.RuleStateEnabled + } + + return events.RuleStateDisabled +} diff --git a/aws/internal/service/cloudwatchevents/state_test.go b/aws/internal/service/cloudwatchevents/state_test.go new file mode 100644 index 000000000000..966c95cca806 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/state_test.go @@ -0,0 +1,83 @@ +package cloudwatchevents_test + +import ( + "testing" + + tfevents "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents" +) + +func TestRuleEnabledFromState(t *testing.T) { + testCases := []struct { + TestName string + State string + ExpectedError bool + ExpectedEnabled bool + }{ + { + TestName: "empty state", + ExpectedError: true, + }, + { + TestName: "invalid state", + State: "UNKNOWN", + ExpectedError: true, + }, + { + TestName: "enabled", + State: "ENABLED", + ExpectedEnabled: true, + }, + { + TestName: "disabled", + State: "DISABLED", + ExpectedEnabled: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotEnabled, err := tfevents.RuleEnabledFromState(testCase.State) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error, got no error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("got unexpected error: %s", err) + } + + if gotEnabled != testCase.ExpectedEnabled { + t.Errorf("got enabled %t, expected %t", gotEnabled, testCase.ExpectedEnabled) + } + }) + } +} + +func RuleStateFromEnabled(t *testing.T) { + testCases := []struct { + TestName string + Enabled bool + ExpectedState string + }{ + { + TestName: "enabled", + Enabled: true, + ExpectedState: "ENABLED", + }, + { + TestName: "disabled", + Enabled: false, + ExpectedState: "DISABLED", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotState := tfevents.RuleStateFromEnabled(testCase.Enabled) + + if gotState != testCase.ExpectedState { + t.Errorf("got enabled %s, expected %s", gotState, testCase.ExpectedState) + } + }) + } +} diff --git a/aws/internal/service/cloudwatchevents/waiter/status.go b/aws/internal/service/cloudwatchevents/waiter/status.go new file mode 100644 index 000000000000..7201c0fee798 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchevents/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ConnectionState(conn *events.CloudWatchEvents, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ConnectionByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ConnectionState), nil + } +} diff --git a/aws/internal/service/cloudwatchevents/waiter/waiter.go b/aws/internal/service/cloudwatchevents/waiter/waiter.go new file mode 100644 index 000000000000..834d746a5ea8 --- /dev/null +++ b/aws/internal/service/cloudwatchevents/waiter/waiter.go @@ -0,0 +1,65 @@ +package waiter + +import ( + "time" + + events "github.com/aws/aws-sdk-go/service/cloudwatchevents" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ConnectionCreatedTimeout = 2 * time.Minute + ConnectionDeletedTimeout = 2 * time.Minute + ConnectionUpdatedTimeout = 2 * time.Minute +) + +func ConnectionCreated(conn *events.CloudWatchEvents, id string) (*events.DescribeConnectionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{events.ConnectionStateCreating, events.ConnectionStateAuthorizing}, + Target: []string{events.ConnectionStateAuthorized, events.ConnectionStateDeauthorized}, + Refresh: ConnectionState(conn, id), + Timeout: ConnectionCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*events.DescribeConnectionOutput); ok { + return v, err + } + + return nil, err +} + +func ConnectionDeleted(conn *events.CloudWatchEvents, id string) (*events.DescribeConnectionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{events.ConnectionStateDeleting}, + Target: []string{}, + Refresh: ConnectionState(conn, id), + Timeout: ConnectionDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*events.DescribeConnectionOutput); ok { + return v, err + } + + return nil, err +} + +func ConnectionUpdated(conn *events.CloudWatchEvents, id string) (*events.DescribeConnectionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{events.ConnectionStateUpdating, events.ConnectionStateAuthorizing, events.ConnectionStateDeauthorizing}, + Target: []string{events.ConnectionStateAuthorized, events.ConnectionStateDeauthorized}, + Refresh: ConnectionState(conn, id), + Timeout: ConnectionUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*events.DescribeConnectionOutput); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/cloudwatchlogs/arn.go b/aws/internal/service/cloudwatchlogs/arn.go new file mode 100644 index 000000000000..57b2101d3ffb --- /dev/null +++ b/aws/internal/service/cloudwatchlogs/arn.go @@ -0,0 +1,14 @@ +package cloudwatchlogs + +import ( + "strings" +) + +const ( + logGroupARNWildcardSuffix = ":*" +) + +// TrimLogGroupARNWildcardSuffix trims any wilcard suffix from a Log Group ARN. +func TrimLogGroupARNWildcardSuffix(arn string) string { + return strings.TrimSuffix(arn, logGroupARNWildcardSuffix) +} diff --git a/aws/internal/service/cloudwatchlogs/arn_test.go b/aws/internal/service/cloudwatchlogs/arn_test.go new file mode 100644 index 000000000000..56938dd54210 --- /dev/null +++ b/aws/internal/service/cloudwatchlogs/arn_test.go @@ -0,0 +1,39 @@ +package cloudwatchlogs_test + +import ( + "testing" + + tfcloudwatchlogs "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchlogs" +) + +func TestTrimLogGroupARNWildcardSuffix(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedARN string + }{ + { + TestName: "Empty string", + }, + { + TestName: "No suffix", + InputARN: "arn:aws-us-gov:logs:us-gov-west-1:123456789012:log-group:tf-acc-test-6899758375212691725/1", + ExpectedARN: "arn:aws-us-gov:logs:us-gov-west-1:123456789012:log-group:tf-acc-test-6899758375212691725/1", + }, + { + TestName: "With suffix", + InputARN: "arn:aws-us-gov:logs:us-gov-west-1:123456789012:log-group:tf-acc-test-6899758375212691725/1:*", + ExpectedARN: "arn:aws-us-gov:logs:us-gov-west-1:123456789012:log-group:tf-acc-test-6899758375212691725/1", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got := tfcloudwatchlogs.TrimLogGroupARNWildcardSuffix(testCase.InputARN) + + if got != testCase.ExpectedARN { + t.Errorf("got %s, expected %s", got, testCase.ExpectedARN) + } + }) + } +} diff --git a/aws/internal/service/cloudwatchlogs/finder/finder.go b/aws/internal/service/cloudwatchlogs/finder/finder.go new file mode 100644 index 000000000000..568023f2f744 --- /dev/null +++ b/aws/internal/service/cloudwatchlogs/finder/finder.go @@ -0,0 +1,34 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudwatchlogs/lister" +) + +func QueryDefinition(ctx context.Context, conn *cloudwatchlogs.CloudWatchLogs, name, queryDefinitionID string) (*cloudwatchlogs.QueryDefinition, error) { + input := &cloudwatchlogs.DescribeQueryDefinitionsInput{} + if name != "" { + input.QueryDefinitionNamePrefix = aws.String(name) + } + + var result *cloudwatchlogs.QueryDefinition + err := lister.DescribeQueryDefinitionsPagesWithContext(ctx, conn, input, func(page *cloudwatchlogs.DescribeQueryDefinitionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, qd := range page.QueryDefinitions { + if aws.StringValue(qd.QueryDefinitionId) == queryDefinitionID { + result = qd + return false + } + } + + return !lastPage + }) + + return result, err +} diff --git a/aws/internal/service/cloudwatchlogs/lister/list.go b/aws/internal/service/cloudwatchlogs/lister/list.go new file mode 100644 index 000000000000..2dd2a1b55e47 --- /dev/null +++ b/aws/internal/service/cloudwatchlogs/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeQueryDefinitions github.com/aws/aws-sdk-go/service/cloudwatchlogs + +package lister diff --git a/aws/internal/service/cloudwatchlogs/lister/list_pages_gen.go b/aws/internal/service/cloudwatchlogs/lister/list_pages_gen.go new file mode 100644 index 000000000000..066c118f7642 --- /dev/null +++ b/aws/internal/service/cloudwatchlogs/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeQueryDefinitions github.com/aws/aws-sdk-go/service/cloudwatchlogs"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" +) + +func DescribeQueryDefinitionsPages(conn *cloudwatchlogs.CloudWatchLogs, input *cloudwatchlogs.DescribeQueryDefinitionsInput, fn func(*cloudwatchlogs.DescribeQueryDefinitionsOutput, bool) bool) error { + return DescribeQueryDefinitionsPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeQueryDefinitionsPagesWithContext(ctx context.Context, conn *cloudwatchlogs.CloudWatchLogs, input *cloudwatchlogs.DescribeQueryDefinitionsInput, fn func(*cloudwatchlogs.DescribeQueryDefinitionsOutput, bool) bool) error { + for { + output, err := conn.DescribeQueryDefinitionsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/codestarconnections/finder/finder.go b/aws/internal/service/codestarconnections/finder/finder.go new file mode 100644 index 000000000000..f0ae5bc1f0df --- /dev/null +++ b/aws/internal/service/codestarconnections/finder/finder.go @@ -0,0 +1,22 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/codestarconnections" +) + +// ConnectionByArn returns the Connection corresponding to the specified Arn. +func ConnectionByArn(conn *codestarconnections.CodeStarConnections, arn string) (*codestarconnections.Connection, error) { + output, err := conn.GetConnection(&codestarconnections.GetConnectionInput{ + ConnectionArn: aws.String(arn), + }) + if err != nil { + return nil, err + } + + if output == nil || output.Connection == nil { + return nil, nil + } + + return output.Connection, nil +} diff --git a/aws/internal/service/codestarconnections/waiter/status.go b/aws/internal/service/codestarconnections/waiter/status.go new file mode 100644 index 000000000000..b251228f2265 --- /dev/null +++ b/aws/internal/service/codestarconnections/waiter/status.go @@ -0,0 +1,28 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/codestarconnections" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// HostStatus fetches the Host and its Status +func HostStatus(conn *codestarconnections.CodeStarConnections, hostARN string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &codestarconnections.GetHostInput{ + HostArn: aws.String(hostARN), + } + + output, err := conn.GetHost(input) + + if err != nil { + return nil, "", err + } + + if output == nil { + return nil, "", nil + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/codestarconnections/waiter/waiter.go b/aws/internal/service/codestarconnections/waiter/waiter.go new file mode 100644 index 000000000000..116a80135a26 --- /dev/null +++ b/aws/internal/service/codestarconnections/waiter/waiter.go @@ -0,0 +1,34 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/codestarconnections" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for a Host to be created + HostCreationTimeout = 30 * time.Minute +) + +// HostPendingOrAvailable waits for a Host to return PENDING or AVAILABLE +func HostPendingOrAvailable(conn *codestarconnections.CodeStarConnections, hostARN string) (*codestarconnections.Host, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"VPC_CONFIG_INITIALIZING"}, + Target: []string{ + "AVAILABLE", + "PENDING", + }, + Refresh: HostStatus(conn, hostARN), + Timeout: HostCreationTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*codestarconnections.Host); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/cognitoidentityprovider/finder/finder.go b/aws/internal/service/cognitoidentityprovider/finder/finder.go new file mode 100644 index 000000000000..692a7bad4741 --- /dev/null +++ b/aws/internal/service/cognitoidentityprovider/finder/finder.go @@ -0,0 +1,35 @@ +package finder + +import ( + "reflect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" +) + +// CognitoUserPoolUICustomization returns the UI Customization corresponding to the UserPoolId and ClientId. +// Returns nil if no UI Customization is found. +func CognitoUserPoolUICustomization(conn *cognitoidentityprovider.CognitoIdentityProvider, userPoolId, clientId string) (*cognitoidentityprovider.UICustomizationType, error) { + input := &cognitoidentityprovider.GetUICustomizationInput{ + ClientId: aws.String(clientId), + UserPoolId: aws.String(userPoolId), + } + + output, err := conn.GetUICustomization(input) + + if err != nil { + return nil, err + } + + if output == nil || output.UICustomization == nil { + return nil, nil + } + + // The GetUICustomization API operation will return an empty struct + // if nothing is present rather than nil or an error, so we equate that with nil + if reflect.DeepEqual(output.UICustomization, &cognitoidentityprovider.UICustomizationType{}) { + return nil, nil + } + + return output.UICustomization, nil +} diff --git a/aws/internal/service/connect/enum.go b/aws/internal/service/connect/enum.go new file mode 100644 index 000000000000..cb3545f59ed5 --- /dev/null +++ b/aws/internal/service/connect/enum.go @@ -0,0 +1,24 @@ +package connect + +import "github.com/aws/aws-sdk-go/service/connect" + +const InstanceStatusStatusNotFound = "ResourceNotFoundException" + +const ( + ListInstancesMaxResults = 10 + // MaxResults Valid Range: Minimum value of 1. Maximum value of 1000 + ListContactFlowsMaxResults = 60 +) + +func InstanceAttributeMapping() map[string]string { + return map[string]string{ + connect.InstanceAttributeTypeAutoResolveBestVoices: "auto_resolve_best_voices_enabled", + connect.InstanceAttributeTypeContactflowLogs: "contact_flow_logs_enabled", + connect.InstanceAttributeTypeContactLens: "contact_lens_enabled", + connect.InstanceAttributeTypeEarlyMedia: "early_media_enabled", + connect.InstanceAttributeTypeInboundCalls: "inbound_calls_enabled", + connect.InstanceAttributeTypeOutboundCalls: "outbound_calls_enabled", + // Pre-release feature requiring allow-list from AWS. Removing all functionality until feature is GA + //connect.InstanceAttributeTypeUseCustomTtsVoices: "use_custom_tts_voices_enabled", + } +} diff --git a/aws/internal/service/connect/errors.go b/aws/internal/service/connect/errors.go new file mode 100644 index 000000000000..7a30fcc2ce80 --- /dev/null +++ b/aws/internal/service/connect/errors.go @@ -0,0 +1,7 @@ +package connect + +// Error code constants missing from AWS Go SDK: +// https://docs.aws.amazon.com/sdk-for-go/api/service/connect/#pkg-constants +const ( + ErrCodeAccessDeniedException = "AccessDeniedException" +) diff --git a/aws/internal/service/connect/waiter/status.go b/aws/internal/service/connect/waiter/status.go new file mode 100644 index 000000000000..3db9398033f8 --- /dev/null +++ b/aws/internal/service/connect/waiter/status.go @@ -0,0 +1,31 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfconnect "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/connect" +) + +func InstanceStatus(ctx context.Context, conn *connect.Connect, instanceId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &connect.DescribeInstanceInput{ + InstanceId: aws.String(instanceId), + } + + output, err := conn.DescribeInstanceWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, tfconnect.InstanceStatusStatusNotFound) { + return output, tfconnect.InstanceStatusStatusNotFound, nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Instance.InstanceStatus), nil + } +} diff --git a/aws/internal/service/connect/waiter/waiter.go b/aws/internal/service/connect/waiter/waiter.go new file mode 100644 index 000000000000..763e830d282b --- /dev/null +++ b/aws/internal/service/connect/waiter/waiter.go @@ -0,0 +1,56 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/connect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfconnect "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/connect" +) + +const ( + // ConnectInstanceCreateTimeout Timeout for connect instance creation + ConnectInstanceCreatedTimeout = 5 * time.Minute + ConnectInstanceDeletedTimeout = 5 * time.Minute + + ConnectContactFlowCreateTimeout = 5 * time.Minute + ConnectContactFlowUpdateTimeout = 5 * time.Minute +) + +func InstanceCreated(ctx context.Context, conn *connect.Connect, instanceId string) (*connect.DescribeInstanceOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{connect.InstanceStatusCreationInProgress}, + Target: []string{connect.InstanceStatusActive}, + Refresh: InstanceStatus(ctx, conn, instanceId), + Timeout: ConnectInstanceCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*connect.DescribeInstanceOutput); ok { + return v, err + } + + return nil, err +} + +// We don't have a PENDING_DELETION or DELETED for the Connect instance. +// If the Connect Instance has an associated EXISTING DIRECTORY, removing the connect instance +// will cause an error because it is still has authorized applications. +func InstanceDeleted(ctx context.Context, conn *connect.Connect, instanceId string) (*connect.DescribeInstanceOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{connect.InstanceStatusActive}, + Target: []string{tfconnect.InstanceStatusStatusNotFound}, + Refresh: InstanceStatus(ctx, conn, instanceId), + Timeout: ConnectInstanceDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*connect.DescribeInstanceOutput); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/costandusagereportservice/finder/finder.go b/aws/internal/service/costandusagereportservice/finder/finder.go new file mode 100644 index 000000000000..f0986afc1f07 --- /dev/null +++ b/aws/internal/service/costandusagereportservice/finder/finder.go @@ -0,0 +1,36 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/costandusagereportservice" +) + +func ReportDefinitionByName(conn *costandusagereportservice.CostandUsageReportService, name string) (*costandusagereportservice.ReportDefinition, error) { + input := &costandusagereportservice.DescribeReportDefinitionsInput{} + + var result *costandusagereportservice.ReportDefinition + + err := conn.DescribeReportDefinitionsPages(input, func(page *costandusagereportservice.DescribeReportDefinitionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, reportDefinition := range page.ReportDefinitions { + if reportDefinition == nil { + continue + } + + if aws.StringValue(reportDefinition.ReportName) == name { + result = reportDefinition + return false + } + } + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/aws/internal/service/datasync/finder/finder.go b/aws/internal/service/datasync/finder/finder.go new file mode 100644 index 000000000000..c67586365293 --- /dev/null +++ b/aws/internal/service/datasync/finder/finder.go @@ -0,0 +1,64 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/datasync" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func AgentByARN(conn *datasync.DataSync, arn string) (*datasync.DescribeAgentOutput, error) { + input := &datasync.DescribeAgentInput{ + AgentArn: aws.String(arn), + } + + output, err := conn.DescribeAgent(input) + + if tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "does not exist") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func TaskByARN(conn *datasync.DataSync, arn string) (*datasync.DescribeTaskOutput, error) { + input := &datasync.DescribeTaskInput{ + TaskArn: aws.String(arn), + } + + output, err := conn.DescribeTask(input) + + if tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "not found") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/datasync/uri.go b/aws/internal/service/datasync/uri.go new file mode 100644 index 000000000000..bac9e606a8eb --- /dev/null +++ b/aws/internal/service/datasync/uri.go @@ -0,0 +1,45 @@ +package datasync + +import ( + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +var ( + locationURIPattern = regexp.MustCompile(`^(efs|nfs|s3|smb|fsxw)://(.+)$`) + locationURIGlobalIDAndSubdirPattern = regexp.MustCompile(`^([a-zA-Z0-9.\-]+)(/.*)$`) + s3OutpostsAccessPointARNResourcePattern = regexp.MustCompile(`^outpost/.*/accesspoint/.*?(/.*)$`) +) + +// SubdirectoryFromLocationURI extracts the subdirectory from a location URI. +// https://docs.aws.amazon.com/datasync/latest/userguide/API_LocationListEntry.html#DataSync-Type-LocationListEntry-LocationUri +func SubdirectoryFromLocationURI(uri string) (string, error) { + submatches := locationURIPattern.FindStringSubmatch(uri) + + if len(submatches) != 3 { + return "", fmt.Errorf("location URI (%s) does not match pattern %q", uri, locationURIPattern) + } + + globalIDAndSubdir := submatches[2] + parsedARN, err := arn.Parse(globalIDAndSubdir) + + if err == nil { + submatches = s3OutpostsAccessPointARNResourcePattern.FindStringSubmatch(parsedARN.Resource) + + if len(submatches) != 2 { + return "", fmt.Errorf("location URI S3 on Outposts access point ARN resource (%s) does not match pattern %q", parsedARN.Resource, s3OutpostsAccessPointARNResourcePattern) + } + + return submatches[1], nil + } + + submatches = locationURIGlobalIDAndSubdirPattern.FindStringSubmatch(globalIDAndSubdir) + + if len(submatches) != 3 { + return "", fmt.Errorf("location URI global ID and subdirectory (%s) does not match pattern %q", globalIDAndSubdir, locationURIGlobalIDAndSubdirPattern) + } + + return submatches[2], nil +} diff --git a/aws/internal/service/datasync/uri_test.go b/aws/internal/service/datasync/uri_test.go new file mode 100644 index 000000000000..6c90cf99a04f --- /dev/null +++ b/aws/internal/service/datasync/uri_test.go @@ -0,0 +1,145 @@ +package datasync_test + +import ( + "testing" + + tfdatasync "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync" +) + +func TestSubdirectoryFromLocationURI(t *testing.T) { + testCases := []struct { + TestName string + InputURI string + ExpectedError bool + ExpectedSubdirectory string + }{ + { + TestName: "empty URI", + InputURI: "", + ExpectedError: true, + }, + { + TestName: "invalid URI scheme", + InputURI: "test://testing/", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI no bucket name (1)", + InputURI: "s3://", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI no bucket name (2)", + InputURI: "s3:///", + ExpectedError: true, + }, + { + TestName: "S3 bucket URI top level", + InputURI: "s3://bucket/", + ExpectedSubdirectory: "/", + }, + { + TestName: "S3 bucket URI one level", + InputURI: "s3://bucket/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "S3 bucket URI two levels", + InputURI: "s3://bucket/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "S3 Outposts ARN URI top level", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/", + ExpectedSubdirectory: "/", + }, + { + TestName: "S3 Outposts ARN URI one level", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "S3 Outposts ARN URI two levels", + InputURI: "s3://arn:aws:s3-outposts:eu-west-3:123456789012:outpost/op-YYYYYYYYYY/accesspoint/my-access-point/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "EFS URI top level", + InputURI: "efs://us-west-2.fs-abcdef01/", + ExpectedSubdirectory: "/", + }, + { + TestName: "EFS URI one level", + InputURI: "efs://us-west-2.fs-abcdef01/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "EFS URI two levels", + InputURI: "efs://us-west-2.fs-abcdef01/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "NFS URI top level", + InputURI: "nfs://example.com/", + ExpectedSubdirectory: "/", + }, + { + TestName: "NFS URI one level", + InputURI: "nfs://example.com/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "NFS URI two levels", + InputURI: "nfs://example.com/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "SMB URI top level", + InputURI: "smb://192.168.1.1/", + ExpectedSubdirectory: "/", + }, + { + TestName: "SMB URI one level", + InputURI: "smb://192.168.1.1/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "SMB URI two levels", + InputURI: "smb://192.168.1.1/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + { + TestName: "FSx Windows URI top level", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/", + ExpectedSubdirectory: "/", + }, + { + TestName: "FSx Windows URI one level", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/my-folder-1/", + ExpectedSubdirectory: "/my-folder-1/", + }, + { + TestName: "FSx Windows URI two levels", + InputURI: "fsxw://us-west-2.fs-abcdef012345678901/my-folder-1/my-folder-2", + ExpectedSubdirectory: "/my-folder-1/my-folder-2", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfdatasync.SubdirectoryFromLocationURI(testCase.InputURI) + + if err == nil && testCase.ExpectedError { + t.Fatalf("expected error") + } + + if err != nil && !testCase.ExpectedError { + t.Fatalf("unexpected error: %s", err) + } + + if got != testCase.ExpectedSubdirectory { + t.Errorf("got %s, expected %s", got, testCase.ExpectedSubdirectory) + } + }) + } +} diff --git a/aws/internal/service/datasync/waiter/status.go b/aws/internal/service/datasync/waiter/status.go index 2b512733ef65..24663dd24d0e 100644 --- a/aws/internal/service/datasync/waiter/status.go +++ b/aws/internal/service/datasync/waiter/status.go @@ -3,33 +3,41 @@ package waiter import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/datasync" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - TaskStatusUnknown = "Unknown" + agentStatusReady = "ready" ) -// TaskStatus fetches the Operation and its Status -func TaskStatus(conn *datasync.DataSync, arn string) resource.StateRefreshFunc { +func AgentStatus(conn *datasync.DataSync, arn string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &datasync.DescribeTaskInput{ - TaskArn: aws.String(arn), - } + output, err := finder.AgentByARN(conn, arn) - output, err := conn.DescribeTask(input) - - if tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "not found") { + if tfresource.NotFound(err) { return nil, "", nil } if err != nil { - return output, TaskStatusUnknown, err + return nil, "", err } - if output == nil { - return output, TaskStatusUnknown, nil + return output, agentStatusReady, nil + } +} + +func TaskStatus(conn *datasync.DataSync, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.TaskByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err } return output, aws.StringValue(output.Status), nil diff --git a/aws/internal/service/datasync/waiter/waiter.go b/aws/internal/service/datasync/waiter/waiter.go index 031f3d6f64d2..1a757a34eaa2 100644 --- a/aws/internal/service/datasync/waiter/waiter.go +++ b/aws/internal/service/datasync/waiter/waiter.go @@ -10,17 +10,27 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -// TaskStatusAvailable waits for a Task to return Available -func TaskStatusAvailable(conn *datasync.DataSync, arn string, timeout time.Duration) (*datasync.DescribeTaskOutput, error) { +func AgentReady(conn *datasync.DataSync, arn string, timeout time.Duration) (*datasync.DescribeAgentOutput, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{ - datasync.TaskStatusCreating, - datasync.TaskStatusUnavailable, - }, - Target: []string{ - datasync.TaskStatusAvailable, - datasync.TaskStatusRunning, - }, + Pending: []string{}, + Target: []string{agentStatusReady}, + Refresh: AgentStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*datasync.DescribeAgentOutput); ok { + return output, err + } + + return nil, err +} + +func TaskAvailable(conn *datasync.DataSync, arn string, timeout time.Duration) (*datasync.DescribeTaskOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{datasync.TaskStatusCreating, datasync.TaskStatusUnavailable}, + Target: []string{datasync.TaskStatusAvailable, datasync.TaskStatusRunning}, Refresh: TaskStatus(conn, arn), Timeout: timeout, } diff --git a/aws/internal/service/directconnect/finder/finder.go b/aws/internal/service/directconnect/finder/finder.go new file mode 100644 index 000000000000..1548123829f9 --- /dev/null +++ b/aws/internal/service/directconnect/finder/finder.go @@ -0,0 +1,260 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ConnectionByID(conn *directconnect.DirectConnect, id string) (*directconnect.Connection, error) { + input := &directconnect.DescribeConnectionsInput{ + ConnectionId: aws.String(id), + } + + output, err := conn.DescribeConnections(input) + + if tfawserr.ErrMessageContains(err, directconnect.ErrCodeClientException, "Could not find Connection with ID") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Connections) == 0 || output.Connections[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Connections); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + connection := output.Connections[0] + + if state := aws.StringValue(connection.ConnectionState); state == directconnect.ConnectionStateDeleted || state == directconnect.ConnectionStateRejected { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return connection, nil +} + +func ConnectionAssociationExists(conn *directconnect.DirectConnect, connectionID, lagID string) error { + connection, err := ConnectionByID(conn, connectionID) + + if err != nil { + return err + } + + if lagID != aws.StringValue(connection.LagId) { + return &resource.NotFoundError{} + } + + return nil +} + +func GatewayByID(conn *directconnect.DirectConnect, id string) (*directconnect.Gateway, error) { + input := &directconnect.DescribeDirectConnectGatewaysInput{ + DirectConnectGatewayId: aws.String(id), + } + + output, err := conn.DescribeDirectConnectGateways(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.DirectConnectGateways) == 0 || output.DirectConnectGateways[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.DirectConnectGateways); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + gateway := output.DirectConnectGateways[0] + + if state := aws.StringValue(gateway.DirectConnectGatewayState); state == directconnect.GatewayStateDeleted { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return gateway, nil +} + +func GatewayAssociationByID(conn *directconnect.DirectConnect, id string) (*directconnect.GatewayAssociation, error) { + input := &directconnect.DescribeDirectConnectGatewayAssociationsInput{ + AssociationId: aws.String(id), + } + + return GatewayAssociation(conn, input) +} + +func GatewayAssociationByDirectConnectGatewayIDAndAssociatedGatewayID(conn *directconnect.DirectConnect, directConnectGatewayID, associatedGatewayID string) (*directconnect.GatewayAssociation, error) { + input := &directconnect.DescribeDirectConnectGatewayAssociationsInput{ + AssociatedGatewayId: aws.String(associatedGatewayID), + DirectConnectGatewayId: aws.String(directConnectGatewayID), + } + + return GatewayAssociation(conn, input) +} + +func GatewayAssociationByDirectConnectGatewayIDAndVirtualGatewayID(conn *directconnect.DirectConnect, directConnectGatewayID, virtualGatewayID string) (*directconnect.GatewayAssociation, error) { + input := &directconnect.DescribeDirectConnectGatewayAssociationsInput{ + DirectConnectGatewayId: aws.String(directConnectGatewayID), + VirtualGatewayId: aws.String(virtualGatewayID), + } + + return GatewayAssociation(conn, input) +} + +func GatewayAssociation(conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewayAssociationsInput) (*directconnect.GatewayAssociation, error) { + output, err := conn.DescribeDirectConnectGatewayAssociations(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.DirectConnectGatewayAssociations) == 0 || output.DirectConnectGatewayAssociations[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.DirectConnectGatewayAssociations); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + association := output.DirectConnectGatewayAssociations[0] + + if state := aws.StringValue(association.AssociationState); state == directconnect.GatewayAssociationStateDisassociated { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + if association.AssociatedGateway == nil { + return nil, &resource.NotFoundError{ + Message: "Empty AssociatedGateway", + LastRequest: input, + } + } + + return association, nil +} + +func GatewayAssociationProposalByID(conn *directconnect.DirectConnect, id string) (*directconnect.GatewayAssociationProposal, error) { + input := &directconnect.DescribeDirectConnectGatewayAssociationProposalsInput{ + ProposalId: aws.String(id), + } + + output, err := conn.DescribeDirectConnectGatewayAssociationProposals(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.DirectConnectGatewayAssociationProposals) == 0 || output.DirectConnectGatewayAssociationProposals[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.DirectConnectGatewayAssociationProposals); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + proposal := output.DirectConnectGatewayAssociationProposals[0] + + if state := aws.StringValue(proposal.ProposalState); state == directconnect.GatewayAssociationProposalStateDeleted { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + if proposal.AssociatedGateway == nil { + return nil, &resource.NotFoundError{ + Message: "Empty AssociatedGateway", + LastRequest: input, + } + } + + return proposal, nil +} + +func LagByID(conn *directconnect.DirectConnect, id string) (*directconnect.Lag, error) { + input := &directconnect.DescribeLagsInput{ + LagId: aws.String(id), + } + + output, err := conn.DescribeLags(input) + + if tfawserr.ErrMessageContains(err, directconnect.ErrCodeClientException, "Could not find Lag with ID") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Lags) == 0 || output.Lags[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Lags); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + lag := output.Lags[0] + + if state := aws.StringValue(lag.LagState); state == directconnect.LagStateDeleted { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return lag, nil +} + +func LocationByCode(conn *directconnect.DirectConnect, code string) (*directconnect.Location, error) { + input := &directconnect.DescribeLocationsInput{} + + locations, err := Locations(conn, input) + + if err != nil { + return nil, err + } + + for _, location := range locations { + if aws.StringValue(location.LocationCode) == code { + return location, nil + } + } + + return nil, tfresource.NewEmptyResultError(input) +} + +func Locations(conn *directconnect.DirectConnect, input *directconnect.DescribeLocationsInput) ([]*directconnect.Location, error) { + output, err := conn.DescribeLocations(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.Locations) == 0 { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Locations, nil +} diff --git a/aws/internal/service/directconnect/id.go b/aws/internal/service/directconnect/id.go new file mode 100644 index 000000000000..d1cfd0dd0618 --- /dev/null +++ b/aws/internal/service/directconnect/id.go @@ -0,0 +1,9 @@ +package directconnect + +import ( + "fmt" +) + +func GatewayAssociationCreateResourceID(directConnectGatewayID, associatedGatewayID string) string { + return fmt.Sprintf("ga-%s%s", directConnectGatewayID, associatedGatewayID) +} diff --git a/aws/internal/service/directconnect/lister/list.go b/aws/internal/service/directconnect/lister/list.go new file mode 100644 index 000000000000..5fa5e04b3413 --- /dev/null +++ b/aws/internal/service/directconnect/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeDirectConnectGateways,DescribeDirectConnectGatewayAssociations,DescribeDirectConnectGatewayAssociationProposals -paginator=NextToken github.com/aws/aws-sdk-go/service/directconnect + +package lister diff --git a/aws/internal/service/directconnect/lister/list_pages_gen.go b/aws/internal/service/directconnect/lister/list_pages_gen.go new file mode 100644 index 000000000000..e28a109e6c22 --- /dev/null +++ b/aws/internal/service/directconnect/lister/list_pages_gen.go @@ -0,0 +1,73 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeDirectConnectGateways,DescribeDirectConnectGatewayAssociations,DescribeDirectConnectGatewayAssociationProposals -paginator=NextToken github.com/aws/aws-sdk-go/service/directconnect"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" +) + +func DescribeDirectConnectGatewayAssociationProposalsPages(conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewayAssociationProposalsInput, fn func(*directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput, bool) bool) error { + return DescribeDirectConnectGatewayAssociationProposalsPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeDirectConnectGatewayAssociationProposalsPagesWithContext(ctx context.Context, conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewayAssociationProposalsInput, fn func(*directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput, bool) bool) error { + for { + output, err := conn.DescribeDirectConnectGatewayAssociationProposalsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} + +func DescribeDirectConnectGatewayAssociationsPages(conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewayAssociationsInput, fn func(*directconnect.DescribeDirectConnectGatewayAssociationsOutput, bool) bool) error { + return DescribeDirectConnectGatewayAssociationsPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeDirectConnectGatewayAssociationsPagesWithContext(ctx context.Context, conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewayAssociationsInput, fn func(*directconnect.DescribeDirectConnectGatewayAssociationsOutput, bool) bool) error { + for { + output, err := conn.DescribeDirectConnectGatewayAssociationsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} + +func DescribeDirectConnectGatewaysPages(conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewaysInput, fn func(*directconnect.DescribeDirectConnectGatewaysOutput, bool) bool) error { + return DescribeDirectConnectGatewaysPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeDirectConnectGatewaysPagesWithContext(ctx context.Context, conn *directconnect.DirectConnect, input *directconnect.DescribeDirectConnectGatewaysInput, fn func(*directconnect.DescribeDirectConnectGatewaysOutput, bool) bool) error { + for { + output, err := conn.DescribeDirectConnectGatewaysWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/directconnect/waiter/status.go b/aws/internal/service/directconnect/waiter/status.go new file mode 100644 index 000000000000..5785e26cf366 --- /dev/null +++ b/aws/internal/service/directconnect/waiter/status.go @@ -0,0 +1,73 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/directconnect/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ConnectionState(conn *directconnect.DirectConnect, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ConnectionByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ConnectionState), nil + } +} + +func GatewayState(conn *directconnect.DirectConnect, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.GatewayByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.DirectConnectGatewayState), nil + } +} + +func GatewayAssociationState(conn *directconnect.DirectConnect, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.GatewayAssociationByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.AssociationState), nil + } +} + +func LagState(conn *directconnect.DirectConnect, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.LagByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.LagState), nil + } +} diff --git a/aws/internal/service/directconnect/waiter/waiter.go b/aws/internal/service/directconnect/waiter/waiter.go new file mode 100644 index 000000000000..70d2c4f4410d --- /dev/null +++ b/aws/internal/service/directconnect/waiter/waiter.go @@ -0,0 +1,146 @@ +package waiter + +import ( + "errors" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directconnect" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + ConnectionDeletedTimeout = 10 * time.Minute + ConnectionDisassociatedTimeout = 1 * time.Minute + LagDeletedTimeout = 10 * time.Minute +) + +func ConnectionDeleted(conn *directconnect.DirectConnect, id string) (*directconnect.Connection, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.ConnectionStatePending, directconnect.ConnectionStateOrdering, directconnect.ConnectionStateAvailable, directconnect.ConnectionStateRequested, directconnect.ConnectionStateDeleting}, + Target: []string{}, + Refresh: ConnectionState(conn, id), + Timeout: ConnectionDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.Connection); ok { + return output, err + } + + return nil, err +} + +func GatewayCreated(conn *directconnect.DirectConnect, id string, timeout time.Duration) (*directconnect.Gateway, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.GatewayStatePending}, + Target: []string{directconnect.GatewayStateAvailable}, + Refresh: GatewayState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.Gateway); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateChangeError))) + + return output, err + } + + return nil, err +} + +func GatewayDeleted(conn *directconnect.DirectConnect, id string, timeout time.Duration) (*directconnect.Gateway, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.GatewayStatePending, directconnect.GatewayStateAvailable, directconnect.GatewayStateDeleting}, + Target: []string{}, + Refresh: GatewayState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.Gateway); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateChangeError))) + + return output, err + } + + return nil, err +} + +func GatewayAssociationCreated(conn *directconnect.DirectConnect, id string, timeout time.Duration) (*directconnect.GatewayAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.GatewayAssociationStateAssociating}, + Target: []string{directconnect.GatewayAssociationStateAssociated}, + Refresh: GatewayAssociationState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.GatewayAssociation); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateChangeError))) + + return output, err + } + + return nil, err +} + +func GatewayAssociationUpdated(conn *directconnect.DirectConnect, id string, timeout time.Duration) (*directconnect.GatewayAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.GatewayAssociationStateUpdating}, + Target: []string{directconnect.GatewayAssociationStateAssociated}, + Refresh: GatewayAssociationState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.GatewayAssociation); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateChangeError))) + + return output, err + } + + return nil, err +} + +func GatewayAssociationDeleted(conn *directconnect.DirectConnect, id string, timeout time.Duration) (*directconnect.GatewayAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.GatewayAssociationStateDisassociating}, + Target: []string{}, + Refresh: GatewayAssociationState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.GatewayAssociation); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateChangeError))) + + return output, err + } + + return nil, err +} + +func LagDeleted(conn *directconnect.DirectConnect, id string) (*directconnect.Lag, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directconnect.LagStateAvailable, directconnect.LagStateRequested, directconnect.LagStatePending, directconnect.LagStateDeleting}, + Target: []string{}, + Refresh: LagState(conn, id), + Timeout: LagDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directconnect.Lag); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/directoryservice/finder/finder.go b/aws/internal/service/directoryservice/finder/finder.go new file mode 100644 index 000000000000..3ce7a870786b --- /dev/null +++ b/aws/internal/service/directoryservice/finder/finder.go @@ -0,0 +1,48 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func DirectoryByID(conn *directoryservice.DirectoryService, id string) (*directoryservice.DirectoryDescription, error) { + input := &directoryservice.DescribeDirectoriesInput{ + DirectoryIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeDirectories(input) + + if tfawserr.ErrCodeEquals(err, directoryservice.ErrCodeEntityDoesNotExistException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.DirectoryDescriptions) == 0 || output.DirectoryDescriptions[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO Check for multiple results. + // TODO https://github.com/hashicorp/terraform-provider-aws/pull/17613. + + directory := output.DirectoryDescriptions[0] + + if stage := aws.StringValue(directory.Stage); stage == directoryservice.DirectoryStageDeleted { + return nil, &resource.NotFoundError{ + Message: stage, + LastRequest: input, + } + } + + return directory, nil +} diff --git a/aws/internal/service/directoryservice/waiter/status.go b/aws/internal/service/directoryservice/waiter/status.go new file mode 100644 index 000000000000..7f6796238b25 --- /dev/null +++ b/aws/internal/service/directoryservice/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/directoryservice/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func DirectoryStage(conn *directoryservice.DirectoryService, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DirectoryByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Stage), nil + } +} diff --git a/aws/internal/service/directoryservice/waiter/waiter.go b/aws/internal/service/directoryservice/waiter/waiter.go new file mode 100644 index 000000000000..47ad07a5606b --- /dev/null +++ b/aws/internal/service/directoryservice/waiter/waiter.go @@ -0,0 +1,54 @@ +package waiter + +import ( + "errors" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/directoryservice" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + DirectoryCreatedTimeout = 60 * time.Minute + DirectoryDeletedTimeout = 60 * time.Minute +) + +func DirectoryCreated(conn *directoryservice.DirectoryService, id string) (*directoryservice.DirectoryDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directoryservice.DirectoryStageRequested, directoryservice.DirectoryStageCreating, directoryservice.DirectoryStageCreated}, + Target: []string{directoryservice.DirectoryStageActive}, + Refresh: DirectoryStage(conn, id), + Timeout: DirectoryCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directoryservice.DirectoryDescription); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StageReason))) + + return output, err + } + + return nil, err +} + +func DirectoryDeleted(conn *directoryservice.DirectoryService, id string) (*directoryservice.DirectoryDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{directoryservice.DirectoryStageActive, directoryservice.DirectoryStageDeleting}, + Target: []string{}, + Refresh: DirectoryStage(conn, id), + Timeout: DirectoryDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*directoryservice.DirectoryDescription); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StageReason))) + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/dynamodb/finder/finder.go b/aws/internal/service/dynamodb/finder/finder.go new file mode 100644 index 000000000000..90a0f73fb956 --- /dev/null +++ b/aws/internal/service/dynamodb/finder/finder.go @@ -0,0 +1,121 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" +) + +func DynamoDBKinesisDataStreamDestination(ctx context.Context, conn *dynamodb.DynamoDB, streamArn, tableName string) (*dynamodb.KinesisDataStreamDestination, error) { + input := &dynamodb.DescribeKinesisStreamingDestinationInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeKinesisStreamingDestinationWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + var result *dynamodb.KinesisDataStreamDestination + + for _, destination := range output.KinesisDataStreamDestinations { + if destination == nil { + continue + } + + if aws.StringValue(destination.StreamArn) == streamArn { + result = destination + break + } + } + + return result, nil +} + +func DynamoDBTableByName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + input := &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeTable(input) + + if err != nil { + return nil, err + } + + if output == nil || output.Table == nil { + return nil, nil + } + + return output.Table, nil +} + +func DynamoDBGSIByTableNameIndexName(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + table, err := DynamoDBTableByName(conn, tableName) + + if err != nil { + return nil, err + } + + if table == nil { + return nil, nil + } + + for _, gsi := range table.GlobalSecondaryIndexes { + if aws.StringValue(gsi.IndexName) == indexName { + return gsi, nil + } + } + + return nil, nil +} + +func DynamoDBPITRDescriptionByTableName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.PointInTimeRecoveryDescription, error) { + input := &dynamodb.DescribeContinuousBackupsInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeContinuousBackups(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + if output.ContinuousBackupsDescription == nil || output.ContinuousBackupsDescription.PointInTimeRecoveryDescription == nil { + return nil, nil + } + + return output.ContinuousBackupsDescription.PointInTimeRecoveryDescription, nil +} + +func DynamoDBTTLRDescriptionByTableName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TimeToLiveDescription, error) { + input := &dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeTimeToLive(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + if output.TimeToLiveDescription == nil { + return nil, nil + } + + return output.TimeToLiveDescription, nil +} diff --git a/aws/internal/service/dynamodb/waiter/status.go b/aws/internal/service/dynamodb/waiter/status.go new file mode 100644 index 000000000000..f4e537f19827 --- /dev/null +++ b/aws/internal/service/dynamodb/waiter/status.go @@ -0,0 +1,187 @@ +package waiter + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/dynamodb/finder" +) + +func DynamoDBKinesisStreamingDestinationStatus(ctx context.Context, conn *dynamodb.DynamoDB, streamArn, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + result, err := finder.DynamoDBKinesisDataStreamDestination(ctx, conn, streamArn, tableName) + + if err != nil { + return nil, "", err + } + + if result == nil { + return nil, "", nil + } + + return result, aws.StringValue(result.DestinationStatus), nil + } +} + +func DynamoDBTableStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + table, err := finder.DynamoDBTableByName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if table == nil { + return nil, "", nil + } + + return table, aws.StringValue(table.TableStatus), nil + } +} + +func DynamoDBReplicaUpdate(conn *dynamodb.DynamoDB, tableName, region string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err != nil { + return 42, "", err + } + log.Printf("[DEBUG] DynamoDB replicas: %s", result.Table.Replicas) + + var targetReplica *dynamodb.ReplicaDescription + + for _, replica := range result.Table.Replicas { + if aws.StringValue(replica.RegionName) == region { + targetReplica = replica + break + } + } + + if targetReplica == nil { + return result, dynamodb.ReplicaStatusCreating, nil + } + + return result, aws.StringValue(targetReplica.ReplicaStatus), nil + } +} + +func DynamoDBReplicaDelete(conn *dynamodb.DynamoDB, tableName, region string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err != nil { + return 42, "", err + } + + log.Printf("[DEBUG] all replicas for waiting: %s", result.Table.Replicas) + var targetReplica *dynamodb.ReplicaDescription + + for _, replica := range result.Table.Replicas { + if aws.StringValue(replica.RegionName) == region { + targetReplica = replica + break + } + } + + if targetReplica == nil { + return result, "", nil + } + + return result, aws.StringValue(targetReplica.ReplicaStatus), nil + } +} + +func DynamoDBGSIStatus(conn *dynamodb.DynamoDB, tableName, indexName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + gsi, err := finder.DynamoDBGSIByTableNameIndexName(conn, tableName, indexName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if gsi == nil { + return nil, "", nil + } + + return gsi, aws.StringValue(gsi.IndexStatus), nil + } +} + +func DynamoDBPITRStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + pitr, err := finder.DynamoDBPITRDescriptionByTableName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if pitr == nil { + return nil, "", nil + } + + return pitr, aws.StringValue(pitr.PointInTimeRecoveryStatus), nil + } +} + +func DynamoDBTTLStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + ttl, err := finder.DynamoDBTTLRDescriptionByTableName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if ttl == nil { + return nil, "", nil + } + + return ttl, aws.StringValue(ttl.TimeToLiveStatus), nil + } +} + +func DynamoDBTableSESStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + table, err := finder.DynamoDBTableByName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if table == nil { + return nil, "", nil + } + + // Disabling SSE returns null SSEDescription + if table.SSEDescription == nil { + return table, dynamodb.SSEStatusDisabled, nil + } + + return table, aws.StringValue(table.SSEDescription.Status), nil + } +} diff --git a/aws/internal/service/dynamodb/waiter/waiter.go b/aws/internal/service/dynamodb/waiter/waiter.go new file mode 100644 index 000000000000..ac373323e8a7 --- /dev/null +++ b/aws/internal/service/dynamodb/waiter/waiter.go @@ -0,0 +1,260 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + KinesisStreamingDestinationActiveTimeout = 5 * time.Minute + KinesisStreamingDestinationDisabledTimeout = 5 * time.Minute + CreateTableTimeout = 20 * time.Minute + UpdateTableTimeoutTotal = 60 * time.Minute + ReplicaUpdateTimeout = 30 * time.Minute + UpdateTableTimeout = 20 * time.Minute + UpdateTableContinuousBackupsTimeout = 20 * time.Minute + DeleteTableTimeout = 10 * time.Minute + PITRUpdateTimeout = 30 * time.Second + TTLUpdateTimeout = 30 * time.Second +) + +func DynamoDBKinesisStreamingDestinationActive(ctx context.Context, conn *dynamodb.DynamoDB, streamArn, tableName string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{dynamodb.DestinationStatusDisabled, dynamodb.DestinationStatusEnabling}, + Target: []string{dynamodb.DestinationStatusActive}, + Timeout: KinesisStreamingDestinationActiveTimeout, + Refresh: DynamoDBKinesisStreamingDestinationStatus(ctx, conn, streamArn, tableName), + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} + +func DynamoDBKinesisStreamingDestinationDisabled(ctx context.Context, conn *dynamodb.DynamoDB, streamArn, tableName string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{dynamodb.DestinationStatusActive, dynamodb.DestinationStatusDisabling}, + Target: []string{dynamodb.DestinationStatusDisabled}, + Timeout: KinesisStreamingDestinationDisabledTimeout, + Refresh: DynamoDBKinesisStreamingDestinationStatus(ctx, conn, streamArn, tableName), + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} + +func DynamoDBTableActive(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.TableStatusCreating, + dynamodb.TableStatusUpdating, + }, + Target: []string{ + dynamodb.TableStatusActive, + }, + Timeout: CreateTableTimeout, + Refresh: DynamoDBTableStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBTableDeleted(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.TableStatusActive, + dynamodb.TableStatusDeleting, + }, + Target: []string{}, + Timeout: DeleteTableTimeout, + Refresh: DynamoDBTableStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBReplicaActive(conn *dynamodb.DynamoDB, tableName, region string) (*dynamodb.DescribeTableOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.ReplicaStatusCreating, + dynamodb.ReplicaStatusUpdating, + dynamodb.ReplicaStatusDeleting, + }, + Target: []string{ + dynamodb.ReplicaStatusActive, + }, + Timeout: ReplicaUpdateTimeout, + Refresh: DynamoDBReplicaUpdate(conn, tableName, region), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.DescribeTableOutput); ok { + return output, err + } + + return nil, err +} + +func DynamoDBReplicaDeleted(conn *dynamodb.DynamoDB, tableName, region string) (*dynamodb.DescribeTableOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.ReplicaStatusCreating, + dynamodb.ReplicaStatusUpdating, + dynamodb.ReplicaStatusDeleting, + dynamodb.ReplicaStatusActive, + }, + Target: []string{""}, + Timeout: ReplicaUpdateTimeout, + Refresh: DynamoDBReplicaDelete(conn, tableName, region), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.DescribeTableOutput); ok { + return output, err + } + + return nil, err +} + +func DynamoDBGSIActive(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.IndexStatusCreating, + dynamodb.IndexStatusUpdating, + }, + Target: []string{ + dynamodb.IndexStatusActive, + }, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBGSIStatus(conn, tableName, indexName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.GlobalSecondaryIndexDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBGSIDeleted(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.IndexStatusActive, + dynamodb.IndexStatusDeleting, + dynamodb.IndexStatusUpdating, + }, + Target: []string{}, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBGSIStatus(conn, tableName, indexName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.GlobalSecondaryIndexDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBPITRUpdated(conn *dynamodb.DynamoDB, tableName string, toEnable bool) (*dynamodb.PointInTimeRecoveryDescription, error) { + var pending []string + target := []string{dynamodb.TimeToLiveStatusDisabled} + + if toEnable { + pending = []string{ + "ENABLING", + } + target = []string{dynamodb.PointInTimeRecoveryStatusEnabled} + } + + stateConf := &resource.StateChangeConf{ + Pending: pending, + Target: target, + Timeout: PITRUpdateTimeout, + Refresh: DynamoDBPITRStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.PointInTimeRecoveryDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBTTLUpdated(conn *dynamodb.DynamoDB, tableName string, toEnable bool) (*dynamodb.TimeToLiveDescription, error) { + pending := []string{ + dynamodb.TimeToLiveStatusEnabled, + dynamodb.TimeToLiveStatusDisabling, + } + target := []string{dynamodb.TimeToLiveStatusDisabled} + + if toEnable { + pending = []string{ + dynamodb.TimeToLiveStatusDisabled, + dynamodb.TimeToLiveStatusEnabling, + } + target = []string{dynamodb.TimeToLiveStatusEnabled} + } + + stateConf := &resource.StateChangeConf{ + Pending: pending, + Target: target, + Timeout: TTLUpdateTimeout, + Refresh: DynamoDBTTLStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TimeToLiveDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBSSEUpdated(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.SSEStatusDisabling, + dynamodb.SSEStatusEnabling, + dynamodb.SSEStatusUpdating, + }, + Target: []string{ + dynamodb.SSEStatusDisabled, + dynamodb.SSEStatusEnabled, + }, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBTableSESStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/ebs/waiter/status.go b/aws/internal/service/ebs/waiter/status.go new file mode 100644 index 000000000000..abcceb71210e --- /dev/null +++ b/aws/internal/service/ebs/waiter/status.go @@ -0,0 +1,41 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" +) + +const ( + snapshotImportNotFound = "NotFound" +) + +func EbsSnapshotImportStatus(conn *ec2.EC2, importTaskId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + params := &ec2.DescribeImportSnapshotTasksInput{ + ImportTaskIds: []*string{aws.String(importTaskId)}, + } + + resp, err := conn.DescribeImportSnapshotTasks(params) + if err != nil { + return nil, "", err + } + + if resp == nil || len(resp.ImportSnapshotTasks) < 1 { + return nil, snapshotImportNotFound, nil + } + + if task := resp.ImportSnapshotTasks[0]; task != nil { + detail := task.SnapshotTaskDetail + if detail.Status != nil && aws.StringValue(detail.Status) == tfec2.EbsSnapshotImportDeleting { + err = fmt.Errorf("Snapshot import task is deleting") + } + return detail, aws.StringValue(detail.Status), err + } else { + return nil, snapshotImportNotFound, nil + } + } +} diff --git a/aws/internal/service/ebs/waiter/waiter.go b/aws/internal/service/ebs/waiter/waiter.go new file mode 100644 index 000000000000..29d83a6ad9ae --- /dev/null +++ b/aws/internal/service/ebs/waiter/waiter.go @@ -0,0 +1,31 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" +) + +func EbsSnapshotImportCompleted(conn *ec2.EC2, importTaskID string) (*ec2.SnapshotTaskDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfec2.EbsSnapshotImportActive, + tfec2.EbsSnapshotImportUpdating, + tfec2.EbsSnapshotImportValidating, + tfec2.EbsSnapshotImportValidated, + tfec2.EbsSnapshotImportConverting, + }, + Target: []string{tfec2.EbsSnapshotImportCompleted}, + Refresh: EbsSnapshotImportStatus(conn, importTaskID), + Timeout: 60 * time.Minute, + Delay: 10 * time.Second, + } + + detail, err := stateConf.WaitForState() + if err != nil { + return nil, err + } else { + return detail.(*ec2.SnapshotTaskDetail), nil + } +} diff --git a/aws/internal/service/ec2/enum.go b/aws/internal/service/ec2/enum.go new file mode 100644 index 000000000000..00bc8467672b --- /dev/null +++ b/aws/internal/service/ec2/enum.go @@ -0,0 +1,25 @@ +package ec2 + +const ( + // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreditSpecificationRequest.html#API_CreditSpecificationRequest_Contents + CpuCreditsStandard = "standard" + CpuCreditsUnlimited = "unlimited" +) + +func CpuCredits_Values() []string { + return []string{ + CpuCreditsStandard, + CpuCreditsUnlimited, + } +} + +const ( + // https://docs.aws.amazon.com/vpc/latest/privatelink/vpce-interface.html#vpce-interface-lifecycle + VpcEndpointStateAvailable = "available" + VpcEndpointStateDeleted = "deleted" + VpcEndpointStateDeleting = "deleting" + VpcEndpointStateFailed = "failed" + VpcEndpointStatePending = "pending" + VpcEndpointStatePendingAcceptance = "pendingAcceptance" + VpcEndpointStateRejected = "rejected" +) diff --git a/aws/internal/service/ec2/errors.go b/aws/internal/service/ec2/errors.go index 5a95520c06bb..33c9244b5c02 100644 --- a/aws/internal/service/ec2/errors.go +++ b/aws/internal/service/ec2/errors.go @@ -4,26 +4,46 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" - "github.com/hashicorp/go-multierror" + multierror "github.com/hashicorp/go-multierror" ) const ( - ErrCodeInvalidParameterValue = "InvalidParameterValue" + ErrCodeGatewayNotAttached = "Gateway.NotAttached" + ErrCodeInvalidAssociationIDNotFound = "InvalidAssociationID.NotFound" + ErrCodeInvalidParameter = "InvalidParameter" + ErrCodeInvalidParameterException = "InvalidParameterException" + ErrCodeInvalidParameterValue = "InvalidParameterValue" ) const ( ErrCodeInvalidCarrierGatewayIDNotFound = "InvalidCarrierGatewayID.NotFound" ) +const ( + ErrCodeClientInvalidHostIDNotFound = "Client.InvalidHostID.NotFound" + ErrCodeInvalidHostIDNotFound = "InvalidHostID.NotFound" +) + +const ( + ErrCodeInvalidNetworkInterfaceIDNotFound = "InvalidNetworkInterfaceID.NotFound" +) + const ( ErrCodeInvalidPrefixListIDNotFound = "InvalidPrefixListID.NotFound" ) const ( + ErrCodeInvalidRouteNotFound = "InvalidRoute.NotFound" + ErrCodeInvalidRouteTableIdNotFound = "InvalidRouteTableId.NotFound" ErrCodeInvalidRouteTableIDNotFound = "InvalidRouteTableID.NotFound" ) +const ( + ErrCodeInvalidTransitGatewayIDNotFound = "InvalidTransitGatewayID.NotFound" +) + const ( ErrCodeClientVpnEndpointIdNotFound = "InvalidClientVpnEndpointId.NotFound" ErrCodeClientVpnAuthorizationRuleNotFound = "InvalidClientVpnEndpointAuthorizationRuleNotFound" @@ -31,17 +51,31 @@ const ( ErrCodeClientVpnRouteNotFound = "InvalidClientVpnRouteNotFound" ) +const ( + ErrCodeInvalidInstanceIDNotFound = "InvalidInstanceID.NotFound" +) + const ( InvalidSecurityGroupIDNotFound = "InvalidSecurityGroupID.NotFound" InvalidGroupNotFound = "InvalidGroup.NotFound" ) const ( + ErrCodeInvalidSpotInstanceRequestIDNotFound = "InvalidSpotInstanceRequestID.NotFound" +) + +const ( + ErrCodeInvalidSubnetIdNotFound = "InvalidSubnetId.NotFound" ErrCodeInvalidSubnetIDNotFound = "InvalidSubnetID.NotFound" ) +const ( + ErrCodeInvalidVpcIDNotFound = "InvalidVpcID.NotFound" +) + const ( ErrCodeInvalidVpcEndpointIdNotFound = "InvalidVpcEndpointId.NotFound" + ErrCodeInvalidVpcEndpointNotFound = "InvalidVpcEndpoint.NotFound" ErrCodeInvalidVpcEndpointServiceIdNotFound = "InvalidVpcEndpointServiceId.NotFound" ) @@ -54,12 +88,30 @@ const ( InvalidVpnGatewayIDNotFound = "InvalidVpnGatewayID.NotFound" ) +const ( + ErrCodeInvalidPermissionDuplicate = "InvalidPermission.Duplicate" + ErrCodeInvalidPermissionMalformed = "InvalidPermission.Malformed" + ErrCodeInvalidPermissionNotFound = "InvalidPermission.NotFound" +) + +// See https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html#check-import-task-status +const ( + EbsSnapshotImportActive = "active" + EbsSnapshotImportDeleting = "deleting" + EbsSnapshotImportDeleted = "deleted" + EbsSnapshotImportUpdating = "updating" + EbsSnapshotImportValidating = "validating" + EbsSnapshotImportValidated = "validated" + EbsSnapshotImportConverting = "converting" + EbsSnapshotImportCompleted = "completed" +) + func UnsuccessfulItemError(apiObject *ec2.UnsuccessfulItemError) error { if apiObject == nil { return nil } - return fmt.Errorf("%s: %s", aws.StringValue(apiObject.Code), aws.StringValue(apiObject.Message)) + return awserr.New(aws.StringValue(apiObject.Code), aws.StringValue(apiObject.Message), nil) } func UnsuccessfulItemsError(apiObjects []*ec2.UnsuccessfulItem) error { diff --git a/aws/internal/service/ec2/errors_test.go b/aws/internal/service/ec2/errors_test.go new file mode 100644 index 000000000000..7bdc90fa31b5 --- /dev/null +++ b/aws/internal/service/ec2/errors_test.go @@ -0,0 +1,133 @@ +package ec2_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" +) + +func TestUnsuccessfulItemError(t *testing.T) { + unsuccessfulItemError := &ec2.UnsuccessfulItemError{ + Code: aws.String("test code"), + Message: aws.String("test message"), + } + + err := tfec2.UnsuccessfulItemError(unsuccessfulItemError) + + if !tfawserr.ErrCodeEquals(err, "test code") { + t.Errorf("tfawserr.ErrCodeEquals failed: %s", err) + } + + if !tfawserr.ErrMessageContains(err, "test code", "est mess") { + t.Errorf("tfawserr.ErrMessageContains failed: %s", err) + } +} + +func TestUnsuccessfulItemsError(t *testing.T) { + testCases := []struct { + Name string + Items []*ec2.UnsuccessfulItem + Expected bool + }{ + { + Name: "no items", + }, + { + Name: "one item no error", + Items: []*ec2.UnsuccessfulItem{ + { + ResourceId: aws.String("test resource"), + }, + }, + }, + { + Name: "one item", + Items: []*ec2.UnsuccessfulItem{ + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("test code"), + Message: aws.String("test message"), + }, + ResourceId: aws.String("test resource"), + }, + }, + Expected: true, + }, + { + Name: "two items, first no error", + Items: []*ec2.UnsuccessfulItem{ + { + ResourceId: aws.String("test resource 1"), + }, + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("test code"), + Message: aws.String("test message"), + }, + ResourceId: aws.String("test resource 2"), + }, + }, + Expected: true, + }, + { + Name: "two items, first not as expected", + Items: []*ec2.UnsuccessfulItem{ + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("not what is required"), + Message: aws.String("not what is wanted"), + }, + ResourceId: aws.String("test resource 1"), + }, + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("test code"), + Message: aws.String("test message"), + }, + ResourceId: aws.String("test resource 2"), + }, + }, + }, + { + Name: "two items, first as expected", + Items: []*ec2.UnsuccessfulItem{ + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("test code"), + Message: aws.String("test message"), + }, + ResourceId: aws.String("test resource 1"), + }, + { + Error: &ec2.UnsuccessfulItemError{ + Code: aws.String("not what is required"), + Message: aws.String("not what is wanted"), + }, + ResourceId: aws.String("test resource 2"), + }, + }, + Expected: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + err := tfec2.UnsuccessfulItemsError(testCase.Items) + + got := tfawserr.ErrCodeEquals(err, "test code") + + if got != testCase.Expected { + t.Errorf("ErrCodeEquals got %t, expected %t", got, testCase.Expected) + } + + got = tfawserr.ErrMessageContains(err, "test code", "est mess") + + if got != testCase.Expected { + t.Errorf("ErrMessageContains got %t, expected %t", got, testCase.Expected) + } + }) + } +} diff --git a/aws/internal/service/ec2/finder/finder.go b/aws/internal/service/ec2/finder/finder.go index 121a51a5defd..c567d6ecfd74 100644 --- a/aws/internal/service/ec2/finder/finder.go +++ b/aws/internal/service/ec2/finder/finder.go @@ -5,7 +5,11 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfnet "github.com/terraform-providers/terraform-provider-aws/aws/internal/net" tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // CarrierGatewayByID returns the carrier gateway corresponding to the specified identifier. @@ -76,23 +80,467 @@ func ClientVpnRouteByID(conn *ec2.EC2, routeID string) (*ec2.DescribeClientVpnRo return ClientVpnRoute(conn, endpointID, targetSubnetID, destinationCidr) } -// SecurityGroupByID looks up a security group by ID. When not found, returns nil and potentially an API error. +func HostByID(conn *ec2.EC2, id string) (*ec2.Host, error) { + input := &ec2.DescribeHostsInput{ + HostIds: aws.StringSlice([]string{id}), + } + + return Host(conn, input) +} + +func HostByIDAndFilters(conn *ec2.EC2, id string, filters []*ec2.Filter) (*ec2.Host, error) { + input := &ec2.DescribeHostsInput{} + + if id != "" { + input.HostIds = aws.StringSlice([]string{id}) + } + + if len(filters) > 0 { + input.Filter = filters + } + + return Host(conn, input) +} + +func Host(conn *ec2.EC2, input *ec2.DescribeHostsInput) (*ec2.Host, error) { + output, err := conn.DescribeHosts(input) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidHostIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Hosts) == 0 || output.Hosts[0] == nil || output.Hosts[0].HostProperties == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Hosts); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + host := output.Hosts[0] + + if state := aws.StringValue(host.State); state == ec2.AllocationStateReleased || state == ec2.AllocationStateReleasedPermanentFailure { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return host, nil +} + +// InstanceByID looks up a Instance by ID. When not found, returns nil and potentially an API error. +func InstanceByID(conn *ec2.EC2, id string) (*ec2.Instance, error) { + input := &ec2.DescribeInstancesInput{ + InstanceIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeInstances(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.Reservations) == 0 || output.Reservations[0] == nil || len(output.Reservations[0].Instances) == 0 || output.Reservations[0].Instances[0] == nil { + return nil, nil + } + + return output.Reservations[0].Instances[0], nil +} + +// NetworkAclByID looks up a NetworkAcl by ID. When not found, returns nil and potentially an API error. +func NetworkAclByID(conn *ec2.EC2, id string) (*ec2.NetworkAcl, error) { + input := &ec2.DescribeNetworkAclsInput{ + NetworkAclIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeNetworkAcls(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, networkAcl := range output.NetworkAcls { + if networkAcl == nil { + continue + } + + if aws.StringValue(networkAcl.NetworkAclId) != id { + continue + } + + return networkAcl, nil + } + + return nil, nil +} + +// NetworkAclEntry looks up a NetworkAclEntry by Network ACL ID, Egress, and Rule Number. When not found, returns nil and potentially an API error. +func NetworkAclEntry(conn *ec2.EC2, networkAclID string, egress bool, ruleNumber int) (*ec2.NetworkAclEntry, error) { + input := &ec2.DescribeNetworkAclsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("entry.egress"), + Values: aws.StringSlice([]string{fmt.Sprintf("%t", egress)}), + }, + { + Name: aws.String("entry.rule-number"), + Values: aws.StringSlice([]string{fmt.Sprintf("%d", ruleNumber)}), + }, + }, + NetworkAclIds: aws.StringSlice([]string{networkAclID}), + } + + output, err := conn.DescribeNetworkAcls(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, networkAcl := range output.NetworkAcls { + if networkAcl == nil { + continue + } + + if aws.StringValue(networkAcl.NetworkAclId) != networkAclID { + continue + } + + for _, entry := range output.NetworkAcls[0].Entries { + if entry == nil { + continue + } + + if aws.BoolValue(entry.Egress) != egress || aws.Int64Value(entry.RuleNumber) != int64(ruleNumber) { + continue + } + + return entry, nil + } + } + + return nil, nil +} + +// NetworkInterfaceByID looks up a NetworkInterface by ID. When not found, returns nil and potentially an API error. +func NetworkInterfaceByID(conn *ec2.EC2, id string) (*ec2.NetworkInterface, error) { + input := &ec2.DescribeNetworkInterfacesInput{ + NetworkInterfaceIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeNetworkInterfaces(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, networkInterface := range output.NetworkInterfaces { + if networkInterface == nil { + continue + } + + if aws.StringValue(networkInterface.NetworkInterfaceId) != id { + continue + } + + return networkInterface, nil + } + + return nil, nil +} + +// NetworkInterfaceSecurityGroup returns the associated GroupIdentifier if found +func NetworkInterfaceSecurityGroup(conn *ec2.EC2, networkInterfaceID string, securityGroupID string) (*ec2.GroupIdentifier, error) { + var result *ec2.GroupIdentifier + + networkInterface, err := NetworkInterfaceByID(conn, networkInterfaceID) + + if err != nil { + return nil, err + } + + if networkInterface == nil { + return nil, nil + } + + for _, groupIdentifier := range networkInterface.Groups { + if aws.StringValue(groupIdentifier.GroupId) == securityGroupID { + result = groupIdentifier + break + } + } + + return result, err +} + +// MainRouteTableAssociationByID returns the main route table association corresponding to the specified identifier. +// Returns NotFoundError if no route table association is found. +func MainRouteTableAssociationByID(conn *ec2.EC2, associationID string) (*ec2.RouteTableAssociation, error) { + association, err := RouteTableAssociationByID(conn, associationID) + + if err != nil { + return nil, err + } + + if !aws.BoolValue(association.Main) { + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("%s is not the association with the main route table", associationID), + } + } + + return association, err +} + +// MainRouteTableAssociationByVpcID returns the main route table association for the specified VPC. +// Returns NotFoundError if no route table association is found. +func MainRouteTableAssociationByVpcID(conn *ec2.EC2, vpcID string) (*ec2.RouteTableAssociation, error) { + routeTable, err := MainRouteTableByVpcID(conn, vpcID) + + if err != nil { + return nil, err + } + + for _, association := range routeTable.Associations { + if aws.BoolValue(association.Main) { + if state := aws.StringValue(association.AssociationState.State); state == ec2.RouteTableAssociationStateCodeDisassociated { + continue + } + + return association, nil + } + } + + return nil, &resource.NotFoundError{} +} + +// RouteTableAssociationByID returns the route table association corresponding to the specified identifier. +// Returns NotFoundError if no route table association is found. +func RouteTableAssociationByID(conn *ec2.EC2, associationID string) (*ec2.RouteTableAssociation, error) { + input := &ec2.DescribeRouteTablesInput{ + Filters: tfec2.BuildAttributeFilterList(map[string]string{ + "association.route-table-association-id": associationID, + }), + } + + routeTable, err := RouteTable(conn, input) + + if err != nil { + return nil, err + } + + for _, association := range routeTable.Associations { + if aws.StringValue(association.RouteTableAssociationId) == associationID { + if state := aws.StringValue(association.AssociationState.State); state == ec2.RouteTableAssociationStateCodeDisassociated { + return nil, &resource.NotFoundError{Message: state} + } + + return association, nil + } + } + + return nil, &resource.NotFoundError{} +} + +// MainRouteTableByVpcID returns the main route table for the specified VPC. +// Returns NotFoundError if no route table is found. +func MainRouteTableByVpcID(conn *ec2.EC2, vpcID string) (*ec2.RouteTable, error) { + input := &ec2.DescribeRouteTablesInput{ + Filters: tfec2.BuildAttributeFilterList(map[string]string{ + "association.main": "true", + "vpc-id": vpcID, + }), + } + + return RouteTable(conn, input) +} + +// RouteTableByID returns the route table corresponding to the specified identifier. +// Returns NotFoundError if no route table is found. +func RouteTableByID(conn *ec2.EC2, routeTableID string) (*ec2.RouteTable, error) { + input := &ec2.DescribeRouteTablesInput{ + RouteTableIds: aws.StringSlice([]string{routeTableID}), + } + + return RouteTable(conn, input) +} + +func RouteTable(conn *ec2.EC2, input *ec2.DescribeRouteTablesInput) (*ec2.RouteTable, error) { + output, err := conn.DescribeRouteTables(input) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteTableIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.RouteTables) == 0 || output.RouteTables[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.RouteTables[0], nil +} + +// RouteFinder returns the route corresponding to the specified destination. +// Returns NotFoundError if no route is found. +type RouteFinder func(*ec2.EC2, string, string) (*ec2.Route, error) + +// RouteByIPv4Destination returns the route corresponding to the specified IPv4 destination. +// Returns NotFoundError if no route is found. +func RouteByIPv4Destination(conn *ec2.EC2, routeTableID, destinationCidr string) (*ec2.Route, error) { + routeTable, err := RouteTableByID(conn, routeTableID) + + if err != nil { + return nil, err + } + + for _, route := range routeTable.Routes { + if tfnet.CIDRBlocksEqual(aws.StringValue(route.DestinationCidrBlock), destinationCidr) { + return route, nil + } + } + + return nil, &resource.NotFoundError{} +} + +// RouteByIPv6Destination returns the route corresponding to the specified IPv6 destination. +// Returns NotFoundError if no route is found. +func RouteByIPv6Destination(conn *ec2.EC2, routeTableID, destinationIpv6Cidr string) (*ec2.Route, error) { + routeTable, err := RouteTableByID(conn, routeTableID) + + if err != nil { + return nil, err + } + + for _, route := range routeTable.Routes { + if tfnet.CIDRBlocksEqual(aws.StringValue(route.DestinationIpv6CidrBlock), destinationIpv6Cidr) { + return route, nil + } + } + + return nil, &resource.NotFoundError{} +} + +// RouteByPrefixListIDDestination returns the route corresponding to the specified prefix list destination. +// Returns NotFoundError if no route is found. +func RouteByPrefixListIDDestination(conn *ec2.EC2, routeTableID, prefixListID string) (*ec2.Route, error) { + routeTable, err := RouteTableByID(conn, routeTableID) + if err != nil { + return nil, err + } + + for _, route := range routeTable.Routes { + if aws.StringValue(route.DestinationPrefixListId) == prefixListID { + return route, nil + } + } + + return nil, &resource.NotFoundError{} +} + +// SecurityGroupByID looks up a security group by ID. Returns a resource.NotFoundError if not found. func SecurityGroupByID(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) { - req := &ec2.DescribeSecurityGroupsInput{ + input := &ec2.DescribeSecurityGroupsInput{ GroupIds: aws.StringSlice([]string{id}), } - result, err := conn.DescribeSecurityGroups(req) + return SecurityGroup(conn, input) +} + +// SecurityGroupByNameAndVpcID looks up a security group by name and VPC ID. Returns a resource.NotFoundError if not found. +func SecurityGroupByNameAndVpcID(conn *ec2.EC2, name, vpcID string) (*ec2.SecurityGroup, error) { + input := &ec2.DescribeSecurityGroupsInput{ + Filters: tfec2.BuildAttributeFilterList( + map[string]string{ + "group-name": name, + "vpc-id": vpcID, + }, + ), + } + return SecurityGroup(conn, input) +} + +// SecurityGroup looks up a security group using an ec2.DescribeSecurityGroupsInput. Returns a resource.NotFoundError if not found. +func SecurityGroup(conn *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) (*ec2.SecurityGroup, error) { + result, err := conn.DescribeSecurityGroups(input) + if tfawserr.ErrCodeEquals(err, tfec2.InvalidSecurityGroupIDNotFound) || + tfawserr.ErrCodeEquals(err, tfec2.InvalidGroupNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } if err != nil { return nil, err } if result == nil || len(result.SecurityGroups) == 0 || result.SecurityGroups[0] == nil { - return nil, nil + return nil, tfresource.NewEmptyResultError(input) + } + + if len(result.SecurityGroups) > 1 { + return nil, tfresource.NewTooManyResultsError(len(result.SecurityGroups), input) } return result.SecurityGroups[0], nil } +// SpotInstanceRequestByID looks up a SpotInstanceRequest by ID. When not found, returns nil and potentially an API error. +func SpotInstanceRequestByID(conn *ec2.EC2, id string) (*ec2.SpotInstanceRequest, error) { + input := &ec2.DescribeSpotInstanceRequestsInput{ + SpotInstanceRequestIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeSpotInstanceRequests(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, spotInstanceRequest := range output.SpotInstanceRequests { + if spotInstanceRequest == nil { + continue + } + + if aws.StringValue(spotInstanceRequest.SpotInstanceRequestId) != id { + continue + } + + return spotInstanceRequest, nil + } + + return nil, nil +} + // SubnetByID looks up a Subnet by ID. When not found, returns nil and potentially an API error. func SubnetByID(conn *ec2.EC2, id string) (*ec2.Subnet, error) { input := &ec2.DescribeSubnetsInput{ @@ -156,6 +604,207 @@ func TransitGatewayPrefixListReferenceByID(conn *ec2.EC2, resourceID string) (*e return TransitGatewayPrefixListReference(conn, transitGatewayRouteTableID, prefixListID) } +func TransitGatewayRouteTablePropagation(conn *ec2.EC2, transitGatewayRouteTableID string, transitGatewayAttachmentID string) (*ec2.TransitGatewayRouteTablePropagation, error) { + if transitGatewayRouteTableID == "" { + return nil, nil + } + + input := &ec2.GetTransitGatewayRouteTablePropagationsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("transit-gateway-attachment-id"), + Values: aws.StringSlice([]string{transitGatewayAttachmentID}), + }, + }, + TransitGatewayRouteTableId: aws.String(transitGatewayRouteTableID), + } + + var result *ec2.TransitGatewayRouteTablePropagation + + err := conn.GetTransitGatewayRouteTablePropagationsPages(input, func(page *ec2.GetTransitGatewayRouteTablePropagationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, transitGatewayRouteTablePropagation := range page.TransitGatewayRouteTablePropagations { + if transitGatewayRouteTablePropagation == nil { + continue + } + + if aws.StringValue(transitGatewayRouteTablePropagation.TransitGatewayAttachmentId) == transitGatewayAttachmentID { + result = transitGatewayRouteTablePropagation + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +// VpcAttribute looks up a VPC attribute. +func VpcAttribute(conn *ec2.EC2, vpcID string, attribute string) (*bool, error) { + input := &ec2.DescribeVpcAttributeInput{ + Attribute: aws.String(attribute), + VpcId: aws.String(vpcID), + } + + output, err := conn.DescribeVpcAttribute(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + switch attribute { + case ec2.VpcAttributeNameEnableDnsHostnames: + if output.EnableDnsHostnames == nil { + return nil, nil + } + + return output.EnableDnsHostnames.Value, nil + case ec2.VpcAttributeNameEnableDnsSupport: + if output.EnableDnsSupport == nil { + return nil, nil + } + + return output.EnableDnsSupport.Value, nil + } + + return nil, fmt.Errorf("unimplemented VPC attribute: %s", attribute) +} + +// VpcByID looks up a Vpc by ID. When not found, returns nil and potentially an API error. +func VpcByID(conn *ec2.EC2, id string) (*ec2.Vpc, error) { + input := &ec2.DescribeVpcsInput{ + VpcIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeVpcs(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, vpc := range output.Vpcs { + if vpc == nil { + continue + } + + if aws.StringValue(vpc.VpcId) != id { + continue + } + + return vpc, nil + } + + return nil, nil +} + +// VpcEndpointByID returns the VPC endpoint corresponding to the specified identifier. +// Returns NotFoundError if no VPC endpoint is found. +func VpcEndpointByID(conn *ec2.EC2, vpcEndpointID string) (*ec2.VpcEndpoint, error) { + input := &ec2.DescribeVpcEndpointsInput{ + VpcEndpointIds: aws.StringSlice([]string{vpcEndpointID}), + } + + vpcEndpoint, err := VpcEndpoint(conn, input) + + if err != nil { + return nil, err + } + + if state := aws.StringValue(vpcEndpoint.State); state == tfec2.VpcEndpointStateDeleted { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + // Eventual consistency check. + if aws.StringValue(vpcEndpoint.VpcEndpointId) != vpcEndpointID { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return vpcEndpoint, nil +} + +func VpcEndpoint(conn *ec2.EC2, input *ec2.DescribeVpcEndpointsInput) (*ec2.VpcEndpoint, error) { + output, err := conn.DescribeVpcEndpoints(input) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidVpcEndpointIdNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.VpcEndpoints) == 0 || output.VpcEndpoints[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.VpcEndpoints[0], nil +} + +// VpcEndpointRouteTableAssociationExists returns NotFoundError if no association for the specified VPC endpoint and route table IDs is found. +func VpcEndpointRouteTableAssociationExists(conn *ec2.EC2, vpcEndpointID string, routeTableID string) error { + vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID) + + if err != nil { + return err + } + + for _, vpcEndpointRouteTableID := range vpcEndpoint.RouteTableIds { + if aws.StringValue(vpcEndpointRouteTableID) == routeTableID { + return nil + } + } + + return &resource.NotFoundError{ + LastError: fmt.Errorf("VPC Endpoint Route Table Association (%s/%s) not found", vpcEndpointID, routeTableID), + } +} + +// VpcEndpointSubnetAssociationExists returns NotFoundError if no association for the specified VPC endpoint and subnet IDs is found. +func VpcEndpointSubnetAssociationExists(conn *ec2.EC2, vpcEndpointID string, subnetID string) error { + vpcEndpoint, err := VpcEndpointByID(conn, vpcEndpointID) + + if err != nil { + return err + } + + for _, vpcEndpointSubnetID := range vpcEndpoint.SubnetIds { + if aws.StringValue(vpcEndpointSubnetID) == subnetID { + return nil + } + } + + return &resource.NotFoundError{ + LastError: fmt.Errorf("VPC Endpoint Subnet Association (%s/%s) not found", vpcEndpointID, subnetID), + } +} + // VpcPeeringConnectionByID returns the VPC peering connection corresponding to the specified identifier. // Returns nil and potentially an error if no VPC peering connection is found. func VpcPeeringConnectionByID(conn *ec2.EC2, id string) (*ec2.VpcPeeringConnection, error) { @@ -221,13 +870,87 @@ func ManagedPrefixListByID(conn *ec2.EC2, id string) (*ec2.ManagedPrefixList, er } output, err := conn.DescribeManagedPrefixLists(input) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidPrefixListIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } - if output == nil || len(output.PrefixLists) == 0 { - return nil, nil + if output == nil || len(output.PrefixLists) == 0 || output.PrefixLists[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.PrefixLists); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + prefixList := output.PrefixLists[0] + + if state := aws.StringValue(prefixList.State); state == ec2.PrefixListStateDeleteComplete { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return prefixList, nil +} + +func ManagedPrefixListEntriesByID(conn *ec2.EC2, id string) ([]*ec2.PrefixListEntry, error) { + input := &ec2.GetManagedPrefixListEntriesInput{ + PrefixListId: aws.String(id), + } + + var prefixListEntries []*ec2.PrefixListEntry + + err := conn.GetManagedPrefixListEntriesPages(input, func(page *ec2.GetManagedPrefixListEntriesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, entry := range page.Entries { + if entry == nil { + continue + } + + prefixListEntries = append(prefixListEntries, entry) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidPrefixListIDNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return prefixListEntries, nil +} + +func ManagedPrefixListEntryByIDAndCIDR(conn *ec2.EC2, id, cidr string) (*ec2.PrefixListEntry, error) { + prefixListEntries, err := ManagedPrefixListEntriesByID(conn, id) + + if err != nil { + return nil, err + } + + for _, entry := range prefixListEntries { + if aws.StringValue(entry.Cidr) == cidr { + return entry, nil + } } - return output.PrefixLists[0], nil + return nil, &resource.NotFoundError{} } diff --git a/aws/internal/service/ec2/id.go b/aws/internal/service/ec2/id.go index 3eb2a65c3e1a..30728ae105c3 100644 --- a/aws/internal/service/ec2/id.go +++ b/aws/internal/service/ec2/id.go @@ -71,6 +71,29 @@ func ClientVpnRouteParseID(id string) (string, string, string, error) { "target-subnet-id"+clientVpnRouteIDSeparator+"destination-cidr-block", id) } +const managedPrefixListEntryIDSeparator = "," + +func ManagedPrefixListEntryCreateID(prefixListID, cidrBlock string) string { + parts := []string{prefixListID, cidrBlock} + id := strings.Join(parts, managedPrefixListEntryIDSeparator) + return id +} + +func ManagedPrefixListEntryParseID(id string) (string, string, error) { + parts := strings.Split(id, managedPrefixListEntryIDSeparator) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", + fmt.Errorf("unexpected format for ID (%q), expected prefix-list-id"+managedPrefixListEntryIDSeparator+"cidr-block", id) +} + +// RouteCreateID returns a route resource ID. +func RouteCreateID(routeTableID, destination string) string { + return fmt.Sprintf("r-%s%d", routeTableID, hashcode.String(destination)) +} + const transitGatewayPrefixListReferenceSeparator = "_" func TransitGatewayPrefixListReferenceCreateID(transitGatewayRouteTableID string, prefixListID string) string { @@ -90,6 +113,14 @@ func TransitGatewayPrefixListReferenceParseID(id string) (string, string, error) return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected transit-gateway-route-table-id%[2]sprefix-list-id", id, transitGatewayPrefixListReferenceSeparator) } +func VpcEndpointRouteTableAssociationCreateID(vpcEndpointID, routeTableID string) string { + return fmt.Sprintf("a-%s%d", vpcEndpointID, hashcode.String(routeTableID)) +} + +func VpcEndpointSubnetAssociationCreateID(vpcEndpointID, subnetID string) string { + return fmt.Sprintf("a-%s%d", vpcEndpointID, hashcode.String(subnetID)) +} + func VpnGatewayVpcAttachmentCreateID(vpnGatewayID, vpcID string) string { return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcID, vpnGatewayID))) } diff --git a/aws/internal/service/ec2/waiter/status.go b/aws/internal/service/ec2/waiter/status.go index 9f00de1e99a4..006f8727fe69 100644 --- a/aws/internal/service/ec2/waiter/status.go +++ b/aws/internal/service/ec2/waiter/status.go @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" + tfiam "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -210,6 +212,96 @@ func ClientVpnRouteStatus(conn *ec2.EC2, routeID string) resource.StateRefreshFu } } +// InstanceIamInstanceProfile fetches the Instance and its IamInstanceProfile +// +// The EC2 API accepts a name and always returns an ARN, so it is converted +// back to the name to prevent unexpected differences. +func InstanceIamInstanceProfile(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + instance, err := finder.InstanceByID(conn, id) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidInstanceIDNotFound) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if instance == nil { + return nil, "", nil + } + + if instance.IamInstanceProfile == nil || instance.IamInstanceProfile.Arn == nil { + return instance, "", nil + } + + name, err := tfiam.InstanceProfileARNToName(aws.StringValue(instance.IamInstanceProfile.Arn)) + + if err != nil { + return instance, "", err + } + + return instance, name, nil + } +} + +const ( + RouteStatusReady = "ready" +) + +func RouteStatus(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := routeFinder(conn, routeTableID, destination) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, RouteStatusReady, nil + } +} + +const ( + RouteTableStatusReady = "ready" +) + +func RouteTableStatus(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.RouteTableByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, RouteTableStatusReady, nil + } +} + +func RouteTableAssociationState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.RouteTableAssociationByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output.AssociationState, aws.StringValue(output.AssociationState.State), nil + } +} + const ( SecurityGroupStatusCreated = "Created" @@ -222,18 +314,13 @@ const ( func SecurityGroupStatus(conn *ec2.EC2, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { group, err := finder.SecurityGroupByID(conn, id) - if tfawserr.ErrCodeEquals(err, tfec2.InvalidSecurityGroupIDNotFound) || - tfawserr.ErrCodeEquals(err, tfec2.InvalidGroupNotFound) { + if tfresource.NotFound(err) { return nil, SecurityGroupStatusNotFound, nil } if err != nil { return nil, SecurityGroupStatusUnknown, err } - if group == nil { - return nil, SecurityGroupStatusNotFound, nil - } - return group, SecurityGroupStatusCreated, nil } } @@ -296,6 +383,43 @@ func TransitGatewayPrefixListReferenceState(conn *ec2.EC2, transitGatewayRouteTa } } +func TransitGatewayRouteTablePropagationState(conn *ec2.EC2, transitGatewayRouteTableID string, transitGatewayAttachmentID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + transitGatewayRouteTablePropagation, err := finder.TransitGatewayRouteTablePropagation(conn, transitGatewayRouteTableID, transitGatewayAttachmentID) + + if err != nil { + return nil, "", err + } + + if transitGatewayRouteTablePropagation == nil { + return nil, "", nil + } + + return transitGatewayRouteTablePropagation, aws.StringValue(transitGatewayRouteTablePropagation.State), nil + } +} + +// VpcAttribute fetches the Vpc and its attribute value +func VpcAttribute(conn *ec2.EC2, id string, attribute string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + attributeValue, err := finder.VpcAttribute(conn, id, attribute) + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidVpcIDNotFound) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if attributeValue == nil { + return nil, "", nil + } + + return attributeValue, strconv.FormatBool(aws.BoolValue(attributeValue)), nil + } +} + const ( vpcPeeringConnectionStatusNotFound = "NotFound" vpcPeeringConnectionStatusUnknown = "Unknown" @@ -357,21 +481,70 @@ func VpnGatewayVpcAttachmentState(conn *ec2.EC2, vpnGatewayID, vpcID string) res } } +func HostState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.HostByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func ManagedPrefixListState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ManagedPrefixListByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func VpcEndpointState(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.VpcEndpointByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + const ( - managedPrefixListStateNotFound = "NotFound" - managedPrefixListStateUnknown = "Unknown" + VpcEndpointRouteTableAssociationStatusReady = "ready" ) -func ManagedPrefixListState(conn *ec2.EC2, prefixListId string) resource.StateRefreshFunc { +func VpcEndpointRouteTableAssociationStatus(conn *ec2.EC2, vpcEndpointID, routeTableID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - managedPrefixList, err := finder.ManagedPrefixListByID(conn, prefixListId) - if err != nil { - return nil, managedPrefixListStateUnknown, err + err := finder.VpcEndpointRouteTableAssociationExists(conn, vpcEndpointID, routeTableID) + + if tfresource.NotFound(err) { + return nil, "", nil } - if managedPrefixList == nil { - return nil, managedPrefixListStateNotFound, nil + + if err != nil { + return nil, "", err } - return managedPrefixList, aws.StringValue(managedPrefixList.State), nil + return "", VpcEndpointRouteTableAssociationStatusReady, nil } } diff --git a/aws/internal/service/ec2/waiter/waiter.go b/aws/internal/service/ec2/waiter/waiter.go index 44a202f01cd6..826a9aa3d8e0 100644 --- a/aws/internal/service/ec2/waiter/waiter.go +++ b/aws/internal/service/ec2/waiter/waiter.go @@ -1,13 +1,18 @@ package waiter import ( + "errors" + "fmt" "strconv" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -234,6 +239,181 @@ func ClientVpnRouteDeleted(conn *ec2.EC2, routeID string) (*ec2.ClientVpnRoute, return nil, err } +func InstanceIamInstanceProfileUpdated(conn *ec2.EC2, instanceID string, expectedValue string) (*ec2.Instance, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{expectedValue}, + Refresh: InstanceIamInstanceProfile(conn, instanceID), + Timeout: InstanceAttributePropagationTimeout, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Instance); ok { + return output, err + } + + return nil, err +} + +const ManagedPrefixListEntryCreateTimeout = 5 * time.Minute + +const ( + NetworkAclPropagationTimeout = 2 * time.Minute + NetworkAclEntryPropagationTimeout = 5 * time.Minute +) + +func RouteDeleted(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string) (*ec2.Route, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{RouteStatusReady}, + Target: []string{}, + Refresh: RouteStatus(conn, routeFinder, routeTableID, destination), + Timeout: PropagationTimeout, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Route); ok { + return output, err + } + + return nil, err +} + +func RouteReady(conn *ec2.EC2, routeFinder finder.RouteFinder, routeTableID, destination string) (*ec2.Route, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{RouteStatusReady}, + Refresh: RouteStatus(conn, routeFinder, routeTableID, destination), + Timeout: PropagationTimeout, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Route); ok { + return output, err + } + + return nil, err +} + +const ( + RouteTableAssociationPropagationTimeout = 5 * time.Minute + + RouteTableAssociationCreatedTimeout = 5 * time.Minute + RouteTableAssociationUpdatedTimeout = 5 * time.Minute + RouteTableAssociationDeletedTimeout = 5 * time.Minute + + RouteTableReadyTimeout = 10 * time.Minute + RouteTableDeletedTimeout = 5 * time.Minute + RouteTableUpdatedTimeout = 5 * time.Minute + + RouteTableNotFoundChecks = 40 + RouteTableAssociationCreatedNotFoundChecks = 40 +) + +func RouteTableReady(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{RouteTableStatusReady}, + Refresh: RouteTableStatus(conn, id), + Timeout: RouteTableReadyTimeout, + NotFoundChecks: RouteTableNotFoundChecks, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.RouteTable); ok { + return output, err + } + + return nil, err +} + +func RouteTableDeleted(conn *ec2.EC2, id string) (*ec2.RouteTable, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{RouteTableStatusReady}, + Target: []string{}, + Refresh: RouteTableStatus(conn, id), + Timeout: RouteTableDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.RouteTable); ok { + return output, err + } + + return nil, err +} + +func RouteTableAssociationCreated(conn *ec2.EC2, id string) (*ec2.RouteTableAssociationState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.RouteTableAssociationStateCodeAssociating}, + Target: []string{ec2.RouteTableAssociationStateCodeAssociated}, + Refresh: RouteTableAssociationState(conn, id), + Timeout: RouteTableAssociationCreatedTimeout, + NotFoundChecks: RouteTableAssociationCreatedNotFoundChecks, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.RouteTableAssociationState); ok { + if state := aws.StringValue(output.State); state == ec2.RouteTableAssociationStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +func RouteTableAssociationDeleted(conn *ec2.EC2, id string) (*ec2.RouteTableAssociationState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.RouteTableAssociationStateCodeDisassociating, ec2.RouteTableAssociationStateCodeAssociated}, + Target: []string{}, + Refresh: RouteTableAssociationState(conn, id), + Timeout: RouteTableAssociationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.RouteTableAssociationState); ok { + if state := aws.StringValue(output.State); state == ec2.RouteTableAssociationStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +func RouteTableAssociationUpdated(conn *ec2.EC2, id string) (*ec2.RouteTableAssociationState, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.RouteTableAssociationStateCodeAssociating}, + Target: []string{ec2.RouteTableAssociationStateCodeAssociated}, + Refresh: RouteTableAssociationState(conn, id), + Timeout: RouteTableAssociationUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.RouteTableAssociationState); ok { + if state := aws.StringValue(output.State); state == ec2.RouteTableAssociationStateCodeFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + func SecurityGroupCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.SecurityGroup, error) { stateConf := &resource.StateChangeConf{ Pending: []string{SecurityGroupStatusNotFound}, @@ -252,6 +432,7 @@ func SecurityGroupCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2 } const ( + SubnetPropagationTimeout = 2 * time.Minute SubnetAttributePropagationTimeout = 5 * time.Minute ) @@ -350,6 +531,71 @@ func TransitGatewayPrefixListReferenceStateUpdated(conn *ec2.EC2, transitGateway return nil, err } +const ( + TransitGatewayRouteTablePropagationTimeout = 5 * time.Minute +) + +func TransitGatewayRouteTablePropagationStateEnabled(conn *ec2.EC2, transitGatewayRouteTableID string, transitGatewayAttachmentID string) (*ec2.TransitGatewayRouteTablePropagation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.TransitGatewayPropagationStateEnabling}, + Target: []string{ec2.TransitGatewayPropagationStateEnabled}, + Timeout: TransitGatewayRouteTablePropagationTimeout, + Refresh: TransitGatewayRouteTablePropagationState(conn, transitGatewayRouteTableID, transitGatewayAttachmentID), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.TransitGatewayRouteTablePropagation); ok { + return output, err + } + + return nil, err +} + +func TransitGatewayRouteTablePropagationStateDisabled(conn *ec2.EC2, transitGatewayRouteTableID string, transitGatewayAttachmentID string) (*ec2.TransitGatewayRouteTablePropagation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.TransitGatewayPropagationStateDisabling}, + Target: []string{}, + Timeout: TransitGatewayRouteTablePropagationTimeout, + Refresh: TransitGatewayRouteTablePropagationState(conn, transitGatewayRouteTableID, transitGatewayAttachmentID), + } + + outputRaw, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidRouteTableIDNotFound) { + return nil, nil + } + + if output, ok := outputRaw.(*ec2.TransitGatewayRouteTablePropagation); ok { + return output, err + } + + return nil, err +} + +const ( + VpcPropagationTimeout = 2 * time.Minute + VpcAttributePropagationTimeout = 5 * time.Minute +) + +func VpcAttributeUpdated(conn *ec2.EC2, vpcID string, attribute string, expectedValue bool) (*ec2.Vpc, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(expectedValue)}, + Refresh: VpcAttribute(conn, vpcID, attribute), + Timeout: VpcAttributePropagationTimeout, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Vpc); ok { + return output, err + } + + return nil, err +} + const ( VpnGatewayVpcAttachmentAttachedTimeout = 15 * time.Minute @@ -390,61 +636,219 @@ func VpnGatewayVpcAttachmentDetached(conn *ec2.EC2, vpnGatewayID, vpcID string) return nil, err } +const ( + HostCreatedTimeout = 10 * time.Minute + HostUpdatedTimeout = 10 * time.Minute + HostDeletedTimeout = 20 * time.Minute +) + +func HostCreated(conn *ec2.EC2, id string) (*ec2.Host, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.AllocationStatePending}, + Target: []string{ec2.AllocationStateAvailable}, + Timeout: HostCreatedTimeout, + Refresh: HostState(conn, id), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Host); ok { + return output, err + } + + return nil, err +} + +func HostUpdated(conn *ec2.EC2, id string) (*ec2.Host, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.AllocationStatePending}, + Target: []string{ec2.AllocationStateAvailable}, + Timeout: HostUpdatedTimeout, + Refresh: HostState(conn, id), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Host); ok { + return output, err + } + + return nil, err +} + +func HostDeleted(conn *ec2.EC2, id string) (*ec2.Host, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.AllocationStateAvailable}, + Target: []string{}, + Timeout: HostDeletedTimeout, + Refresh: HostState(conn, id), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.Host); ok { + return output, err + } + + return nil, err +} + const ( ManagedPrefixListTimeout = 15 * time.Minute ) -func ManagedPrefixListCreated(conn *ec2.EC2, prefixListId string) (*ec2.ManagedPrefixList, error) { +func ManagedPrefixListCreated(conn *ec2.EC2, id string) (*ec2.ManagedPrefixList, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.PrefixListStateCreateInProgress}, Target: []string{ec2.PrefixListStateCreateComplete}, Timeout: ManagedPrefixListTimeout, - Refresh: ManagedPrefixListState(conn, prefixListId), + Refresh: ManagedPrefixListState(conn, id), } outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*ec2.ManagedPrefixList); ok { + if state := aws.StringValue(output.State); state == ec2.PrefixListStateCreateFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateMessage))) + } + return output, err } return nil, err } -func ManagedPrefixListModified(conn *ec2.EC2, prefixListId string) (*ec2.ManagedPrefixList, error) { +func ManagedPrefixListModified(conn *ec2.EC2, id string) (*ec2.ManagedPrefixList, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.PrefixListStateModifyInProgress}, Target: []string{ec2.PrefixListStateModifyComplete}, Timeout: ManagedPrefixListTimeout, - Refresh: ManagedPrefixListState(conn, prefixListId), + Refresh: ManagedPrefixListState(conn, id), } outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*ec2.ManagedPrefixList); ok { + if state := aws.StringValue(output.State); state == ec2.PrefixListStateModifyFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateMessage))) + } + return output, err } return nil, err } -func ManagedPrefixListDeleted(conn *ec2.EC2, prefixListId string) error { +func ManagedPrefixListDeleted(conn *ec2.EC2, id string) (*ec2.ManagedPrefixList, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.PrefixListStateDeleteInProgress}, - Target: []string{ec2.PrefixListStateDeleteComplete}, + Target: []string{}, Timeout: ManagedPrefixListTimeout, - Refresh: ManagedPrefixListState(conn, prefixListId), + Refresh: ManagedPrefixListState(conn, id), } - _, err := stateConf.WaitForState() + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.ManagedPrefixList); ok { + if state := aws.StringValue(output.State); state == ec2.PrefixListStateDeleteFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StateMessage))) + } - if tfawserr.ErrCodeEquals(err, "InvalidPrefixListID.NotFound") { - return nil + return output, err } - if err != nil { - return err + return nil, err +} + +func VpcEndpointAccepted(conn *ec2.EC2, vpcEndpointID string, timeout time.Duration) (*ec2.VpcEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfec2.VpcEndpointStatePendingAcceptance}, + Target: []string{tfec2.VpcEndpointStateAvailable}, + Timeout: timeout, + Refresh: VpcEndpointState(conn, vpcEndpointID), + Delay: 5 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcEndpoint); ok { + if state, lastError := aws.StringValue(output.State), output.LastError; state == tfec2.VpcEndpointStateFailed && lastError != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(lastError.Code), aws.StringValue(lastError.Message))) + } + + return output, err } - return nil + return nil, err +} + +func VpcEndpointAvailable(conn *ec2.EC2, vpcEndpointID string, timeout time.Duration) (*ec2.VpcEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfec2.VpcEndpointStatePending}, + Target: []string{tfec2.VpcEndpointStateAvailable, tfec2.VpcEndpointStatePendingAcceptance}, + Timeout: timeout, + Refresh: VpcEndpointState(conn, vpcEndpointID), + Delay: 5 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcEndpoint); ok { + if state, lastError := aws.StringValue(output.State), output.LastError; state == tfec2.VpcEndpointStateFailed && lastError != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(lastError.Code), aws.StringValue(lastError.Message))) + } + + return output, err + } + + return nil, err +} + +func VpcEndpointDeleted(conn *ec2.EC2, vpcEndpointID string, timeout time.Duration) (*ec2.VpcEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfec2.VpcEndpointStateDeleting}, + Target: []string{}, + Timeout: timeout, + Refresh: VpcEndpointState(conn, vpcEndpointID), + Delay: 5 * time.Second, + MinTimeout: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.VpcEndpoint); ok { + return output, err + } + + return nil, err +} + +func VpcEndpointRouteTableAssociationDeleted(conn *ec2.EC2, vpcEndpointID, routeTableID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{VpcEndpointRouteTableAssociationStatusReady}, + Target: []string{}, + Refresh: VpcEndpointRouteTableAssociationStatus(conn, vpcEndpointID, routeTableID), + Timeout: PropagationTimeout, + ContinuousTargetOccurence: 2, + } + + _, err := stateConf.WaitForState() + + return err +} + +func VpcEndpointRouteTableAssociationReady(conn *ec2.EC2, vpcEndpointID, routeTableID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{VpcEndpointRouteTableAssociationStatusReady}, + Refresh: VpcEndpointRouteTableAssociationStatus(conn, vpcEndpointID, routeTableID), + Timeout: PropagationTimeout, + ContinuousTargetOccurence: 2, + } + + _, err := stateConf.WaitForState() + + return err } diff --git a/aws/internal/service/ecr/waiter/waiter.go b/aws/internal/service/ecr/waiter/waiter.go new file mode 100644 index 000000000000..33e7247ad368 --- /dev/null +++ b/aws/internal/service/ecr/waiter/waiter.go @@ -0,0 +1,10 @@ +package waiter + +import ( + "time" +) + +const ( + // Maximum amount of time to wait for ECR changes to propagate + PropagationTimeout = 2 * time.Minute +) diff --git a/aws/internal/service/ecs/finder/finder.go b/aws/internal/service/ecs/finder/finder.go new file mode 100644 index 000000000000..823c231aa343 --- /dev/null +++ b/aws/internal/service/ecs/finder/finder.go @@ -0,0 +1,67 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func CapacityProviderByARN(conn *ecs.ECS, arn string) (*ecs.CapacityProvider, error) { + input := &ecs.DescribeCapacityProvidersInput{ + CapacityProviders: aws.StringSlice([]string{arn}), + Include: aws.StringSlice([]string{ecs.CapacityProviderFieldTags}), + } + + output, err := conn.DescribeCapacityProviders(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.CapacityProviders) == 0 || output.CapacityProviders[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + capacityProvider := output.CapacityProviders[0] + + if status := aws.StringValue(capacityProvider.Status); status == ecs.CapacityProviderStatusInactive { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return capacityProvider, nil +} + +func ClusterByARN(conn *ecs.ECS, arn string) (*ecs.DescribeClustersOutput, error) { + input := &ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(arn)}, + Include: []*string{ + aws.String(ecs.ClusterFieldTags), + aws.String(ecs.ClusterFieldConfigurations), + aws.String(ecs.ClusterFieldSettings), + }, + } + + output, err := conn.DescribeClusters(input) + + if err != nil { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/ecs/lister/list.go b/aws/internal/service/ecs/lister/list.go new file mode 100644 index 000000000000..d57d33e2bde6 --- /dev/null +++ b/aws/internal/service/ecs/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeCapacityProviders -paginator=NextToken github.com/aws/aws-sdk-go/service/ecs + +package lister diff --git a/aws/internal/service/ecs/lister/list_pages_gen.go b/aws/internal/service/ecs/lister/list_pages_gen.go new file mode 100644 index 000000000000..60deeacadad9 --- /dev/null +++ b/aws/internal/service/ecs/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeCapacityProviders -paginator=NextToken github.com/aws/aws-sdk-go/service/ecs"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" +) + +func DescribeCapacityProvidersPages(conn *ecs.ECS, input *ecs.DescribeCapacityProvidersInput, fn func(*ecs.DescribeCapacityProvidersOutput, bool) bool) error { + return DescribeCapacityProvidersPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeCapacityProvidersPagesWithContext(ctx context.Context, conn *ecs.ECS, input *ecs.DescribeCapacityProvidersInput, fn func(*ecs.DescribeCapacityProvidersOutput, bool) bool) error { + for { + output, err := conn.DescribeCapacityProvidersWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/ecs/waiter/status.go b/aws/internal/service/ecs/waiter/status.go index 9b1ba797bb25..2572a04dca1c 100644 --- a/aws/internal/service/ecs/waiter/status.go +++ b/aws/internal/service/ecs/waiter/status.go @@ -1,36 +1,103 @@ package waiter import ( + "log" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ecs/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - // EventSubscription NotFound - CapacityProviderStatusNotFound = "NotFound" + // AWS will likely add consts for these at some point + ServiceStatusInactive = "INACTIVE" + ServiceStatusActive = "ACTIVE" + ServiceStatusDraining = "DRAINING" + + ServiceStatusError = "ERROR" + ServiceStatusNone = "NONE" - // EventSubscription Unknown - CapacityProviderStatusUnknown = "Unknown" + ClusterStatusError = "ERROR" + ClusterStatusNone = "NONE" ) -// CapacityProviderStatus fetches the Capacity Provider and its Status -func CapacityProviderStatus(conn *ecs.ECS, capacityProvider string) resource.StateRefreshFunc { +func CapacityProviderStatus(conn *ecs.ECS, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.CapacityProviderByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func CapacityProviderUpdateStatus(conn *ecs.ECS, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.CapacityProviderByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.UpdateStatus), nil + } +} + +func ServiceStatus(conn *ecs.ECS, id, cluster string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &ecs.DescribeCapacityProvidersInput{ - CapacityProviders: aws.StringSlice([]string{capacityProvider}), + input := &ecs.DescribeServicesInput{ + Services: aws.StringSlice([]string{id}), + Cluster: aws.String(cluster), + } + + output, err := conn.DescribeServices(input) + + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeServiceNotFoundException) { + return nil, ServiceStatusNone, nil + } + + if err != nil { + return nil, ServiceStatusError, err + } + + if len(output.Services) == 0 { + return nil, ServiceStatusNone, nil } - output, err := conn.DescribeCapacityProviders(input) + log.Printf("[DEBUG] ECS service (%s) is currently %q", id, *output.Services[0].Status) + return output, aws.StringValue(output.Services[0].Status), err + } +} + +func ClusterStatus(conn *ecs.ECS, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterByARN(conn, arn) + + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) { + return nil, ClusterStatusNone, nil + } if err != nil { - return nil, CapacityProviderStatusUnknown, err + return nil, ClusterStatusError, err } - if len(output.CapacityProviders) == 0 { - return nil, CapacityProviderStatusNotFound, nil + if len(output.Clusters) == 0 { + return nil, ClusterStatusNone, nil } - return output.CapacityProviders[0], aws.StringValue(output.CapacityProviders[0].Status), nil + return output, aws.StringValue(output.Clusters[0].Status), err } } diff --git a/aws/internal/service/ecs/waiter/waiter.go b/aws/internal/service/ecs/waiter/waiter.go index dd4a3ca4895f..8c4a462047d4 100644 --- a/aws/internal/service/ecs/waiter/waiter.go +++ b/aws/internal/service/ecs/waiter/waiter.go @@ -3,22 +3,32 @@ package waiter import ( "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) const ( - // Maximum amount of time to wait for a Capacity Provider to return INACTIVE - CapacityProviderInactiveTimeout = 20 * time.Minute + CapacityProviderDeleteTimeout = 20 * time.Minute + CapacityProviderUpdateTimeout = 10 * time.Minute + + ServiceCreateTimeout = 2 * time.Minute + ServiceInactiveTimeout = 10 * time.Minute + ServiceInactiveTimeoutMin = 1 * time.Second + ServiceDescribeTimeout = 2 * time.Minute + ServiceUpdateTimeout = 2 * time.Minute + + ClusterAvailableTimeout = 10 * time.Minute + ClusterDeleteTimeout = 10 * time.Minute + ClusterAvailableDelay = 10 * time.Second ) -// CapacityProviderInactive waits for a Capacity Provider to return INACTIVE -func CapacityProviderInactive(conn *ecs.ECS, capacityProvider string) (*ecs.CapacityProvider, error) { +func CapacityProviderDeleted(conn *ecs.ECS, arn string) (*ecs.CapacityProvider, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ecs.CapacityProviderStatusActive}, - Target: []string{ecs.CapacityProviderStatusInactive}, - Refresh: CapacityProviderStatus(conn, capacityProvider), - Timeout: CapacityProviderInactiveTimeout, + Target: []string{}, + Refresh: CapacityProviderStatus(conn, arn), + Timeout: CapacityProviderDeleteTimeout, } outputRaw, err := stateConf.WaitForState() @@ -29,3 +39,117 @@ func CapacityProviderInactive(conn *ecs.ECS, capacityProvider string) (*ecs.Capa return nil, err } + +func CapacityProviderUpdated(conn *ecs.ECS, arn string) (*ecs.CapacityProvider, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ecs.CapacityProviderUpdateStatusUpdateInProgress}, + Target: []string{ecs.CapacityProviderUpdateStatusUpdateComplete}, + Refresh: CapacityProviderUpdateStatus(conn, arn), + Timeout: CapacityProviderUpdateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ecs.CapacityProvider); ok { + return v, err + } + + return nil, err +} + +func ServiceStable(conn *ecs.ECS, id, cluster string) error { + input := &ecs.DescribeServicesInput{ + Services: aws.StringSlice([]string{id}), + } + + if cluster != "" { + input.Cluster = aws.String(cluster) + } + + if err := conn.WaitUntilServicesStable(input); err != nil { + return err + } + return nil +} + +func ServiceInactive(conn *ecs.ECS, id, cluster string) error { + input := &ecs.DescribeServicesInput{ + Services: aws.StringSlice([]string{id}), + } + + if cluster != "" { + input.Cluster = aws.String(cluster) + } + + if err := conn.WaitUntilServicesInactive(input); err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ServiceStatusActive, ServiceStatusDraining}, + Target: []string{ServiceStatusInactive, ServiceStatusNone}, + Refresh: ServiceStatus(conn, id, cluster), + Timeout: ServiceInactiveTimeout, + MinTimeout: ServiceInactiveTimeoutMin, + } + + _, err := stateConf.WaitForState() + + if err != nil { + return err + } + + return nil +} + +func ServiceDescribeReady(conn *ecs.ECS, id, cluster string) (*ecs.DescribeServicesOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ServiceStatusInactive, ServiceStatusDraining, ServiceStatusNone}, + Target: []string{ServiceStatusActive}, + Refresh: ServiceStatus(conn, id, cluster), + Timeout: ServiceDescribeTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ecs.DescribeServicesOutput); ok { + return v, err + } + + return nil, err +} + +func ClusterAvailable(conn *ecs.ECS, arn string) (*ecs.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PROVISIONING"}, + Target: []string{"ACTIVE"}, + Refresh: ClusterStatus(conn, arn), + Timeout: ClusterAvailableTimeout, + Delay: ClusterAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ecs.Cluster); ok { + return v, err + } + + return nil, err +} + +func ClusterDeleted(conn *ecs.ECS, arn string) (*ecs.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"ACTIVE", "DEPROVISIONING"}, + Target: []string{"INACTIVE"}, + Refresh: ClusterStatus(conn, arn), + Timeout: ClusterDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ecs.Cluster); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/efs/finder/finder.go b/aws/internal/service/efs/finder/finder.go new file mode 100644 index 000000000000..98f8199aeb5a --- /dev/null +++ b/aws/internal/service/efs/finder/finder.go @@ -0,0 +1,84 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/efs" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func BackupPolicyByID(conn *efs.EFS, id string) (*efs.BackupPolicy, error) { + input := &efs.DescribeBackupPolicyInput{ + FileSystemId: aws.String(id), + } + + output, err := conn.DescribeBackupPolicy(input) + + if tfawserr.ErrCodeEquals(err, efs.ErrCodeFileSystemNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.BackupPolicy == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.BackupPolicy, nil +} + +func FileSystemByID(conn *efs.EFS, id string) (*efs.FileSystemDescription, error) { + input := &efs.DescribeFileSystemsInput{ + FileSystemId: aws.String(id), + } + + output, err := conn.DescribeFileSystems(input) + + if tfawserr.ErrCodeEquals(err, efs.ErrCodeFileSystemNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.FileSystems == nil || len(output.FileSystems) == 0 || output.FileSystems[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.FileSystems[0], nil +} + +func FileSystemPolicyByID(conn *efs.EFS, id string) (*efs.DescribeFileSystemPolicyOutput, error) { + input := &efs.DescribeFileSystemPolicyInput{ + FileSystemId: aws.String(id), + } + + output, err := conn.DescribeFileSystemPolicy(input) + + if tfawserr.ErrCodeEquals(err, efs.ErrCodeFileSystemNotFound) || tfawserr.ErrCodeEquals(err, efs.ErrCodePolicyNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} diff --git a/aws/internal/service/efs/waiter/status.go b/aws/internal/service/efs/waiter/status.go index d48fb58c92b5..5c597578596d 100644 --- a/aws/internal/service/efs/waiter/status.go +++ b/aws/internal/service/efs/waiter/status.go @@ -4,6 +4,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/efs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/efs/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // AccessPointLifeCycleState fetches the Access Point and its LifecycleState @@ -28,3 +30,35 @@ func AccessPointLifeCycleState(conn *efs.EFS, accessPointId string) resource.Sta return mt, aws.StringValue(mt.LifeCycleState), nil } } + +func BackupPolicyStatus(conn *efs.EFS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.BackupPolicyByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func FileSystemLifeCycleState(conn *efs.EFS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.FileSystemByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.LifeCycleState), nil + } +} diff --git a/aws/internal/service/efs/waiter/waiter.go b/aws/internal/service/efs/waiter/waiter.go index 71db067975ae..ed1398cfa609 100644 --- a/aws/internal/service/efs/waiter/waiter.go +++ b/aws/internal/service/efs/waiter/waiter.go @@ -9,8 +9,17 @@ import ( const ( // Maximum amount of time to wait for an Operation to return Success - AccessPointCreatedTimeout = 10 * time.Minute - AccessPointDeletedTimeout = 10 * time.Minute + AccessPointCreatedTimeout = 10 * time.Minute + AccessPointDeletedTimeout = 10 * time.Minute + FileSystemAvailableTimeout = 10 * time.Minute + FileSystemAvailableDelayTimeout = 2 * time.Second + FileSystemAvailableMinTimeout = 3 * time.Second + FileSystemDeletedTimeout = 10 * time.Minute + FileSystemDeletedDelayTimeout = 2 * time.Second + FileSystemDeletedMinTimeout = 3 * time.Second + + BackupPolicyDisabledTimeout = 10 * time.Minute + BackupPolicyEnabledTimeout = 10 * time.Minute ) // AccessPointCreated waits for an Operation to return Success @@ -31,7 +40,7 @@ func AccessPointCreated(conn *efs.EFS, accessPointId string) (*efs.AccessPointDe return nil, err } -// AccessPointDelete waits for an Access Point to return Deleted +// AccessPointDeleted waits for an Access Point to return Deleted func AccessPointDeleted(conn *efs.EFS, accessPointId string) (*efs.AccessPointDescription, error) { stateConf := &resource.StateChangeConf{ Pending: []string{efs.LifeCycleStateAvailable, efs.LifeCycleStateDeleting, efs.LifeCycleStateDeleted}, @@ -48,3 +57,75 @@ func AccessPointDeleted(conn *efs.EFS, accessPointId string) (*efs.AccessPointDe return nil, err } + +func FileSystemAvailable(conn *efs.EFS, fileSystemID string) (*efs.FileSystemDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{efs.LifeCycleStateCreating, efs.LifeCycleStateUpdating}, + Target: []string{efs.LifeCycleStateAvailable}, + Refresh: FileSystemLifeCycleState(conn, fileSystemID), + Timeout: FileSystemAvailableTimeout, + Delay: FileSystemAvailableDelayTimeout, + MinTimeout: FileSystemAvailableMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*efs.FileSystemDescription); ok { + return output, err + } + + return nil, err +} + +func FileSystemDeleted(conn *efs.EFS, fileSystemID string) (*efs.FileSystemDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{efs.LifeCycleStateAvailable, efs.LifeCycleStateDeleting}, + Target: []string{}, + Refresh: FileSystemLifeCycleState(conn, fileSystemID), + Timeout: FileSystemDeletedTimeout, + Delay: FileSystemDeletedDelayTimeout, + MinTimeout: FileSystemDeletedMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*efs.FileSystemDescription); ok { + return output, err + } + + return nil, err +} + +func BackupPolicyDisabled(conn *efs.EFS, id string) (*efs.BackupPolicy, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{efs.StatusDisabling}, + Target: []string{efs.StatusDisabled}, + Refresh: BackupPolicyStatus(conn, id), + Timeout: BackupPolicyDisabledTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*efs.BackupPolicy); ok { + return output, err + } + + return nil, err +} + +func BackupPolicyEnabled(conn *efs.EFS, id string) (*efs.BackupPolicy, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{efs.StatusEnabling}, + Target: []string{efs.StatusEnabled}, + Refresh: BackupPolicyStatus(conn, id), + Timeout: BackupPolicyEnabledTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*efs.BackupPolicy); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/eks/enum.go b/aws/internal/service/eks/enum.go new file mode 100644 index 000000000000..e21b72d590bf --- /dev/null +++ b/aws/internal/service/eks/enum.go @@ -0,0 +1,15 @@ +package eks + +const ( + IdentityProviderConfigTypeOidc = "oidc" +) + +const ( + ResourcesSecrets = "secrets" +) + +func Resources_Values() []string { + return []string{ + ResourcesSecrets, + } +} diff --git a/aws/internal/service/eks/errors.go b/aws/internal/service/eks/errors.go new file mode 100644 index 000000000000..6bf3e10d1ffa --- /dev/null +++ b/aws/internal/service/eks/errors.go @@ -0,0 +1,89 @@ +package eks + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/eks" + multierror "github.com/hashicorp/go-multierror" +) + +func AddonIssueError(apiObject *eks.AddonIssue) error { + if apiObject == nil { + return nil + } + + return awserr.New(aws.StringValue(apiObject.Code), aws.StringValue(apiObject.Message), nil) +} + +func AddonIssuesError(apiObjects []*eks.AddonIssue) error { + var errors *multierror.Error + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + err := AddonIssueError(apiObject) + + if err != nil { + errors = multierror.Append(errors, fmt.Errorf("%s: %w", strings.Join(aws.StringValueSlice(apiObject.ResourceIds), ", "), err)) + } + } + + return errors.ErrorOrNil() +} + +func ErrorDetailError(apiObject *eks.ErrorDetail) error { + if apiObject == nil { + return nil + } + + return awserr.New(aws.StringValue(apiObject.ErrorCode), aws.StringValue(apiObject.ErrorMessage), nil) +} + +func ErrorDetailsError(apiObjects []*eks.ErrorDetail) error { + var errors *multierror.Error + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + err := ErrorDetailError(apiObject) + + if err != nil { + errors = multierror.Append(errors, fmt.Errorf("%s: %w", strings.Join(aws.StringValueSlice(apiObject.ResourceIds), ", "), err)) + } + } + + return errors.ErrorOrNil() +} + +func IssueError(apiObject *eks.Issue) error { + if apiObject == nil { + return nil + } + + return awserr.New(aws.StringValue(apiObject.Code), aws.StringValue(apiObject.Message), nil) +} + +func IssuesError(apiObjects []*eks.Issue) error { + var errors *multierror.Error + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + err := IssueError(apiObject) + + if err != nil { + errors = multierror.Append(errors, fmt.Errorf("%s: %w", strings.Join(aws.StringValueSlice(apiObject.ResourceIds), ", "), err)) + } + } + + return errors.ErrorOrNil() +} diff --git a/aws/internal/service/eks/finder/finder.go b/aws/internal/service/eks/finder/finder.go new file mode 100644 index 000000000000..3334e44c6eb6 --- /dev/null +++ b/aws/internal/service/eks/finder/finder.go @@ -0,0 +1,249 @@ +package finder + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" +) + +func AddonByClusterNameAndAddonName(ctx context.Context, conn *eks.EKS, clusterName, addonName string) (*eks.Addon, error) { + input := &eks.DescribeAddonInput{ + AddonName: aws.String(addonName), + ClusterName: aws.String(clusterName), + } + + output, err := conn.DescribeAddonWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Addon == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Addon, nil +} + +func AddonUpdateByClusterNameAddonNameAndID(ctx context.Context, conn *eks.EKS, clusterName, addonName, id string) (*eks.Update, error) { + input := &eks.DescribeUpdateInput{ + AddonName: aws.String(addonName), + Name: aws.String(clusterName), + UpdateId: aws.String(id), + } + + output, err := conn.DescribeUpdateWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Update == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Update, nil +} + +func ClusterByName(conn *eks.EKS, name string) (*eks.Cluster, error) { + input := &eks.DescribeClusterInput{ + Name: aws.String(name), + } + + output, err := conn.DescribeCluster(input) + + // Sometimes the EKS API returns the ResourceNotFound error in this form: + // ClientException: No cluster found for name: tf-acc-test-0o1f8 + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) || tfawserr.ErrMessageContains(err, eks.ErrCodeClientException, "No cluster found for name:") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Cluster == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Cluster, nil +} + +func ClusterUpdateByNameAndID(conn *eks.EKS, name, id string) (*eks.Update, error) { + input := &eks.DescribeUpdateInput{ + Name: aws.String(name), + UpdateId: aws.String(id), + } + + output, err := conn.DescribeUpdate(input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Update == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Update, nil +} + +func FargateProfileByClusterNameAndFargateProfileName(conn *eks.EKS, clusterName, fargateProfileName string) (*eks.FargateProfile, error) { + input := &eks.DescribeFargateProfileInput{ + ClusterName: aws.String(clusterName), + FargateProfileName: aws.String(fargateProfileName), + } + + output, err := conn.DescribeFargateProfile(input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.FargateProfile == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.FargateProfile, nil +} + +func NodegroupByClusterNameAndNodegroupName(conn *eks.EKS, clusterName, nodeGroupName string) (*eks.Nodegroup, error) { + input := &eks.DescribeNodegroupInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(nodeGroupName), + } + + output, err := conn.DescribeNodegroup(input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Nodegroup == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Nodegroup, nil +} + +func NodegroupUpdateByClusterNameNodegroupNameAndID(conn *eks.EKS, clusterName, nodeGroupName, id string) (*eks.Update, error) { + input := &eks.DescribeUpdateInput{ + Name: aws.String(clusterName), + NodegroupName: aws.String(nodeGroupName), + UpdateId: aws.String(id), + } + + output, err := conn.DescribeUpdate(input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Update == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Update, nil +} + +func OidcIdentityProviderConfigByClusterNameAndConfigName(ctx context.Context, conn *eks.EKS, clusterName, configName string) (*eks.OidcIdentityProviderConfig, error) { + input := &eks.DescribeIdentityProviderConfigInput{ + ClusterName: aws.String(clusterName), + IdentityProviderConfig: &eks.IdentityProviderConfig{ + Name: aws.String(configName), + Type: aws.String(tfeks.IdentityProviderConfigTypeOidc), + }, + } + + output, err := conn.DescribeIdentityProviderConfigWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, eks.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.IdentityProviderConfig == nil || output.IdentityProviderConfig.Oidc == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.IdentityProviderConfig.Oidc, nil +} diff --git a/aws/internal/service/eks/id.go b/aws/internal/service/eks/id.go new file mode 100644 index 000000000000..66c86d64104b --- /dev/null +++ b/aws/internal/service/eks/id.go @@ -0,0 +1,82 @@ +package eks + +import ( + "fmt" + "strings" +) + +const addonResourceIDSeparator = ":" + +func AddonCreateResourceID(clusterName, addonName string) string { + parts := []string{clusterName, addonName} + id := strings.Join(parts, addonResourceIDSeparator) + + return id +} + +func AddonParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, addonResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]saddon-name", id, addonResourceIDSeparator) +} + +const fargateProfileResourceIDSeparator = ":" + +func FargateProfileCreateResourceID(clusterName, fargateProfileName string) string { + parts := []string{clusterName, fargateProfileName} + id := strings.Join(parts, fargateProfileResourceIDSeparator) + + return id +} + +func FargateProfileParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, fargateProfileResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]sfargate-profile-name", id, fargateProfileResourceIDSeparator) +} + +const identityProviderConfigResourceIDSeparator = ":" + +func IdentityProviderConfigCreateResourceID(clusterName, configName string) string { + parts := []string{clusterName, configName} + id := strings.Join(parts, identityProviderConfigResourceIDSeparator) + + return id +} + +func IdentityProviderConfigParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, identityProviderConfigResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]sconfig-name", id, identityProviderConfigResourceIDSeparator) +} + +const nodeGroupResourceIDSeparator = ":" + +func NodeGroupCreateResourceID(clusterName, nodeGroupName string) string { + parts := []string{clusterName, nodeGroupName} + id := strings.Join(parts, nodeGroupResourceIDSeparator) + + return id +} + +func NodeGroupParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, nodeGroupResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected cluster-name%[2]snode-group-name", id, nodeGroupResourceIDSeparator) +} diff --git a/aws/internal/service/eks/token/token.go b/aws/internal/service/eks/token/token.go index 2ab7e0cb5a97..f7e1742c36ec 100644 --- a/aws/internal/service/eks/token/token.go +++ b/aws/internal/service/eks/token/token.go @@ -10,6 +10,7 @@ With the following modifications: - Hard copy and use local Canonicalize implementation instead of "sigs.k8s.io/aws-iam-authenticator/pkg/arn" - Fix staticcheck reports - Ignore errorlint reports + - Refactor deprecated io/ioutil in Go 1.16 */ /* @@ -34,7 +35,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "regexp" @@ -316,7 +317,7 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) { } defer response.Body.Close() - responseBody, err := ioutil.ReadAll(response.Body) + responseBody, err := io.ReadAll(response.Body) if err != nil { return nil, NewSTSError(fmt.Sprintf("error reading HTTP result: %v", err)) } diff --git a/aws/internal/service/eks/token/token_test.go b/aws/internal/service/eks/token/token_test.go index 114db9f156d3..f871637b37cc 100644 --- a/aws/internal/service/eks/token/token_test.go +++ b/aws/internal/service/eks/token/token_test.go @@ -6,6 +6,7 @@ With the following modifications: - Fix staticcheck reports - Ignore errorlint reports + - Refactor deprecated io/ioutil in Go 1.16 */ package token @@ -17,7 +18,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "strings" "testing" @@ -69,7 +69,7 @@ func toToken(url string) string { func newVerifier(statusCode int, body string, err error) Verifier { var rc io.ReadCloser if body != "" { - rc = ioutil.NopCloser(bytes.NewReader([]byte(body))) + rc = io.NopCloser(bytes.NewReader([]byte(body))) } return tokenVerifier{ client: &http.Client{ diff --git a/aws/internal/service/eks/waiter/status.go b/aws/internal/service/eks/waiter/status.go new file mode 100644 index 000000000000..c0b4920ef1cd --- /dev/null +++ b/aws/internal/service/eks/waiter/status.go @@ -0,0 +1,139 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func AddonStatus(ctx context.Context, conn *eks.EKS, clusterName, addonName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.AddonByClusterNameAndAddonName(ctx, conn, clusterName, addonName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func AddonUpdateStatus(ctx context.Context, conn *eks.EKS, clusterName, addonName, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.AddonUpdateByClusterNameAddonNameAndID(ctx, conn, clusterName, addonName, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func ClusterStatus(conn *eks.EKS, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func ClusterUpdateStatus(conn *eks.EKS, name, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterUpdateByNameAndID(conn, name, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func FargateProfileStatus(conn *eks.EKS, clusterName, fargateProfileName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.FargateProfileByClusterNameAndFargateProfileName(conn, clusterName, fargateProfileName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func NodegroupStatus(conn *eks.EKS, clusterName, nodeGroupName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.NodegroupByClusterNameAndNodegroupName(conn, clusterName, nodeGroupName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func NodegroupUpdateStatus(conn *eks.EKS, clusterName, nodeGroupName, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.NodegroupUpdateByClusterNameNodegroupNameAndID(conn, clusterName, nodeGroupName, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func OidcIdentityProviderConfigStatus(ctx context.Context, conn *eks.EKS, clusterName, configName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.OidcIdentityProviderConfigByClusterNameAndConfigName(ctx, conn, clusterName, configName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/eks/waiter/waiter.go b/aws/internal/service/eks/waiter/waiter.go new file mode 100644 index 000000000000..58e91148fa5a --- /dev/null +++ b/aws/internal/service/eks/waiter/waiter.go @@ -0,0 +1,267 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfeks "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/eks" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + AddonCreatedTimeout = 20 * time.Minute + AddonUpdatedTimeout = 20 * time.Minute + AddonDeletedTimeout = 40 * time.Minute +) + +func AddonCreated(ctx context.Context, conn *eks.EKS, clusterName, addonName string) (*eks.Addon, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.AddonStatusCreating, eks.AddonStatusDegraded}, + Target: []string{eks.AddonStatusActive}, + Refresh: AddonStatus(ctx, conn, clusterName, addonName), + Timeout: AddonCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.Addon); ok { + if status, health := aws.StringValue(output.Status), output.Health; status == eks.AddonStatusCreateFailed && health != nil { + tfresource.SetLastError(err, tfeks.AddonIssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func AddonDeleted(ctx context.Context, conn *eks.EKS, clusterName, addonName string) (*eks.Addon, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.AddonStatusActive, eks.AddonStatusDeleting}, + Target: []string{}, + Refresh: AddonStatus(ctx, conn, clusterName, addonName), + Timeout: AddonDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.Addon); ok { + if status, health := aws.StringValue(output.Status), output.Health; status == eks.AddonStatusDeleteFailed && health != nil { + tfresource.SetLastError(err, tfeks.AddonIssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func AddonUpdateSuccessful(ctx context.Context, conn *eks.EKS, clusterName, addonName, id string) (*eks.Update, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.UpdateStatusInProgress}, + Target: []string{eks.UpdateStatusSuccessful}, + Refresh: AddonUpdateStatus(ctx, conn, clusterName, addonName, id), + Timeout: AddonUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.Update); ok { + if status := aws.StringValue(output.Status); status == eks.UpdateStatusCancelled || status == eks.UpdateStatusFailed { + tfresource.SetLastError(err, tfeks.ErrorDetailsError(output.Errors)) + } + + return output, err + } + + return nil, err +} + +func ClusterCreated(conn *eks.EKS, name string, timeout time.Duration) (*eks.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.ClusterStatusCreating}, + Target: []string{eks.ClusterStatusActive}, + Refresh: ClusterStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.Cluster); ok { + return output, err + } + + return nil, err +} + +func ClusterDeleted(conn *eks.EKS, name string, timeout time.Duration) (*eks.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.ClusterStatusActive, eks.ClusterStatusDeleting}, + Target: []string{}, + Refresh: ClusterStatus(conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.Cluster); ok { + return output, err + } + + return nil, err +} + +func ClusterUpdateSuccessful(conn *eks.EKS, name, id string, timeout time.Duration) (*eks.Update, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.UpdateStatusInProgress}, + Target: []string{eks.UpdateStatusSuccessful}, + Refresh: ClusterUpdateStatus(conn, name, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.Update); ok { + if status := aws.StringValue(output.Status); status == eks.UpdateStatusCancelled || status == eks.UpdateStatusFailed { + tfresource.SetLastError(err, tfeks.ErrorDetailsError(output.Errors)) + } + + return output, err + } + + return nil, err +} + +func FargateProfileCreated(conn *eks.EKS, clusterName, fargateProfileName string, timeout time.Duration) (*eks.FargateProfile, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.FargateProfileStatusCreating}, + Target: []string{eks.FargateProfileStatusActive}, + Refresh: FargateProfileStatus(conn, clusterName, fargateProfileName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.FargateProfile); ok { + return output, err + } + + return nil, err +} + +func FargateProfileDeleted(conn *eks.EKS, clusterName, fargateProfileName string, timeout time.Duration) (*eks.FargateProfile, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.FargateProfileStatusActive, eks.FargateProfileStatusDeleting}, + Target: []string{}, + Refresh: FargateProfileStatus(conn, clusterName, fargateProfileName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.FargateProfile); ok { + return output, err + } + + return nil, err +} + +func NodegroupCreated(ctx context.Context, conn *eks.EKS, clusterName, nodeGroupName string, timeout time.Duration) (*eks.Nodegroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.NodegroupStatusCreating}, + Target: []string{eks.NodegroupStatusActive}, + Refresh: NodegroupStatus(conn, clusterName, nodeGroupName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.Nodegroup); ok { + if status, health := aws.StringValue(output.Status), output.Health; status == eks.NodegroupStatusCreateFailed && health != nil { + tfresource.SetLastError(err, tfeks.IssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func NodegroupDeleted(ctx context.Context, conn *eks.EKS, clusterName, nodeGroupName string, timeout time.Duration) (*eks.Nodegroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.NodegroupStatusActive, eks.NodegroupStatusDeleting}, + Target: []string{}, + Refresh: NodegroupStatus(conn, clusterName, nodeGroupName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.Nodegroup); ok { + if status, health := aws.StringValue(output.Status), output.Health; status == eks.NodegroupStatusDeleteFailed && health != nil { + tfresource.SetLastError(err, tfeks.IssuesError(health.Issues)) + } + + return output, err + } + + return nil, err +} + +func NodegroupUpdateSuccessful(ctx context.Context, conn *eks.EKS, clusterName, nodeGroupName, id string, timeout time.Duration) (*eks.Update, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{eks.UpdateStatusInProgress}, + Target: []string{eks.UpdateStatusSuccessful}, + Refresh: NodegroupUpdateStatus(conn, clusterName, nodeGroupName, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*eks.Update); ok { + if status := aws.StringValue(output.Status); status == eks.UpdateStatusCancelled || status == eks.UpdateStatusFailed { + tfresource.SetLastError(err, tfeks.ErrorDetailsError(output.Errors)) + } + + return output, err + } + + return nil, err +} + +func OidcIdentityProviderConfigCreated(ctx context.Context, conn *eks.EKS, clusterName, configName string, timeout time.Duration) (*eks.OidcIdentityProviderConfig, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.ConfigStatusCreating}, + Target: []string{eks.ConfigStatusActive}, + Refresh: OidcIdentityProviderConfigStatus(ctx, conn, clusterName, configName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.OidcIdentityProviderConfig); ok { + return output, err + } + + return nil, err +} + +func OidcIdentityProviderConfigDeleted(ctx context.Context, conn *eks.EKS, clusterName, configName string, timeout time.Duration) (*eks.OidcIdentityProviderConfig, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{eks.ConfigStatusActive, eks.ConfigStatusDeleting}, + Target: []string{}, + Refresh: OidcIdentityProviderConfigStatus(ctx, conn, clusterName, configName), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*eks.OidcIdentityProviderConfig); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/elasticache/finder/finder.go b/aws/internal/service/elasticache/finder/finder.go index 6491028c7d13..fbf21abbc40c 100644 --- a/aws/internal/service/elasticache/finder/finder.go +++ b/aws/internal/service/elasticache/finder/finder.go @@ -1,6 +1,8 @@ package finder import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/aws-sdk-go-base/tfawserr" @@ -12,7 +14,7 @@ func ReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticach input := &elasticache.DescribeReplicationGroupsInput{ ReplicationGroupId: aws.String(id), } - result, err := conn.DescribeReplicationGroups(input) + output, err := conn.DescribeReplicationGroups(input) if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeReplicationGroupNotFoundFault) { return nil, &resource.NotFoundError{ LastError: err, @@ -23,23 +25,21 @@ func ReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticach return nil, err } - if result == nil || len(result.ReplicationGroups) == 0 || result.ReplicationGroups[0] == nil { + if output == nil || len(output.ReplicationGroups) == 0 || output.ReplicationGroups[0] == nil { return nil, &resource.NotFoundError{ - Message: "Empty result", + Message: "empty result", LastRequest: input, } } - return result.ReplicationGroups[0], nil + return output.ReplicationGroups[0], nil } // ReplicationGroupMemberClustersByID retrieves all of an ElastiCache Replication Group's MemberClusters by the id of the Replication Group. func ReplicationGroupMemberClustersByID(conn *elasticache.ElastiCache, id string) ([]*elasticache.CacheCluster, error) { - var results []*elasticache.CacheCluster - rg, err := ReplicationGroupByID(conn, id) if err != nil { - return results, err + return nil, err } clusters, err := CacheClustersByID(conn, aws.StringValueSlice(rg.MemberClusters)) @@ -48,7 +48,7 @@ func ReplicationGroupMemberClustersByID(conn *elasticache.ElastiCache, id string } if len(clusters) == 0 { return clusters, &resource.NotFoundError{ - Message: "No Member Clusters found", + Message: fmt.Sprintf("No Member Clusters found in Replication Group (%s)", id), } } @@ -87,7 +87,7 @@ func CacheCluster(conn *elasticache.ElastiCache, input *elasticache.DescribeCach if result == nil || len(result.CacheClusters) == 0 || result.CacheClusters[0] == nil { return nil, &resource.NotFoundError{ - Message: "Empty result", + Message: "empty result", LastRequest: input, } } @@ -125,3 +125,104 @@ func CacheClustersByID(conn *elasticache.ElastiCache, idList []string) ([]*elast return results, err } + +// GlobalReplicationGroupByID() retrieves an ElastiCache Global Replication Group by id. +func GlobalReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticache.GlobalReplicationGroup, error) { + input := &elasticache.DescribeGlobalReplicationGroupsInput{ + GlobalReplicationGroupId: aws.String(id), + ShowMemberInfo: aws.Bool(true), + } + output, err := conn.DescribeGlobalReplicationGroups(input) + if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + if output == nil || len(output.GlobalReplicationGroups) == 0 || output.GlobalReplicationGroups[0] == nil { + return nil, &resource.NotFoundError{ + Message: "empty result", + LastRequest: input, + } + } + + return output.GlobalReplicationGroups[0], nil +} + +// GlobalReplicationGroupMemberByID retrieves a member Replication Group by id from a Global Replication Group. +func GlobalReplicationGroupMemberByID(conn *elasticache.ElastiCache, globalReplicationGroupID string, id string) (*elasticache.GlobalReplicationGroupMember, error) { + globalReplicationGroup, err := GlobalReplicationGroupByID(conn, globalReplicationGroupID) + if err != nil { + return nil, &resource.NotFoundError{ + Message: "unable to retrieve enclosing Global Replication Group", + LastError: err, + } + } + + if globalReplicationGroup == nil || len(globalReplicationGroup.Members) == 0 { + return nil, &resource.NotFoundError{ + Message: "empty result", + } + } + + for _, member := range globalReplicationGroup.Members { + if aws.StringValue(member.ReplicationGroupId) == id { + return member, nil + } + } + + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("Replication Group (%s) not found in Global Replication Group (%s)", id, globalReplicationGroupID), + } +} + +func ElastiCacheUserById(conn *elasticache.ElastiCache, userID string) (*elasticache.User, error) { + input := &elasticache.DescribeUsersInput{ + UserId: aws.String(userID), + } + out, err := conn.DescribeUsers(input) + + if err != nil { + return nil, err + } + + switch len(out.Users) { + case 0: + return nil, &resource.NotFoundError{ + Message: "empty result", + } + case 1: + return out.Users[0], nil + default: + return nil, &resource.NotFoundError{ + Message: "too many results", + } + } +} + +func ElastiCacheUserGroupById(conn *elasticache.ElastiCache, groupID string) (*elasticache.UserGroup, error) { + input := &elasticache.DescribeUserGroupsInput{ + UserGroupId: aws.String(groupID), + } + out, err := conn.DescribeUserGroups(input) + if err != nil { + return nil, err + } + + switch len(out.UserGroups) { + case 0: + return nil, &resource.NotFoundError{ + Message: "empty result", + } + case 1: + return out.UserGroups[0], nil + default: + return nil, &resource.NotFoundError{ + Message: "too many results", + } + } +} diff --git a/aws/internal/service/elasticache/service.go b/aws/internal/service/elasticache/service.go new file mode 100644 index 000000000000..4cdc87d0e1bd --- /dev/null +++ b/aws/internal/service/elasticache/service.go @@ -0,0 +1,14 @@ +package elasticache + +const ( + EngineMemcached = "memcached" + EngineRedis = "redis" +) + +// Engine_Values returns all elements of the Engine enum +func Engine_Values() []string { + return []string{ + EngineMemcached, + EngineRedis, + } +} diff --git a/aws/internal/service/elasticache/waiter/status.go b/aws/internal/service/elasticache/waiter/status.go index d9bd1e44258e..2199b1853847 100644 --- a/aws/internal/service/elasticache/waiter/status.go +++ b/aws/internal/service/elasticache/waiter/status.go @@ -15,9 +15,13 @@ const ( ReplicationGroupStatusDeleting = "deleting" ReplicationGroupStatusCreateFailed = "create-failed" ReplicationGroupStatusSnapshotting = "snapshotting" + + UserStatusActive = "active" + UserStatusDeleting = "deleting" + UserStatusModifying = "modifying" ) -// ReplicationGroupStatus fetches the ReplicationGroup and its Status +// ReplicationGroupStatus fetches the Replication Group and its Status func ReplicationGroupStatus(conn *elasticache.ElastiCache, replicationGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { rg, err := finder.ReplicationGroupByID(conn, replicationGroupID) @@ -32,7 +36,7 @@ func ReplicationGroupStatus(conn *elasticache.ElastiCache, replicationGroupID st } } -// ReplicationGroupMemberClustersStatus fetches the ReplicationGroup's Member Clusters and either "available" or the first non-"available" status. +// ReplicationGroupMemberClustersStatus fetches the Replication Group's Member Clusters and either "available" or the first non-"available" status. // NOTE: This function assumes that the intended end-state is to have all member clusters in "available" status. func ReplicationGroupMemberClustersStatus(conn *elasticache.ElastiCache, replicationGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { @@ -68,7 +72,7 @@ const ( CacheClusterStatusSnapshotting = "snapshotting" ) -// CacheClusterStatus fetches the CacheCluster and its Status +// CacheClusterStatus fetches the Cache Cluster and its Status func CacheClusterStatus(conn *elasticache.ElastiCache, cacheClusterID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { c, err := finder.CacheClusterByID(conn, cacheClusterID) @@ -82,3 +86,63 @@ func CacheClusterStatus(conn *elasticache.ElastiCache, cacheClusterID string) re return c, aws.StringValue(c.CacheClusterStatus), nil } } + +const ( + GlobalReplicationGroupStatusAvailable = "available" + GlobalReplicationGroupStatusCreating = "creating" + GlobalReplicationGroupStatusModifying = "modifying" + GlobalReplicationGroupStatusPrimaryOnly = "primary-only" + GlobalReplicationGroupStatusDeleting = "deleting" + GlobalReplicationGroupStatusDeleted = "deleted" +) + +// GlobalReplicationGroupStatus fetches the Global Replication Group and its Status +func GlobalReplicationGroupStatus(conn *elasticache.ElastiCache, globalReplicationGroupID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + grg, err := finder.GlobalReplicationGroupByID(conn, globalReplicationGroupID) + if tfresource.NotFound(err) { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + + return grg, aws.StringValue(grg.Status), nil + } +} + +const ( + GlobalReplicationGroupMemberStatusAssociated = "associated" +) + +// GlobalReplicationGroupStatus fetches the Global Replication Group and its Status +func GlobalReplicationGroupMemberStatus(conn *elasticache.ElastiCache, globalReplicationGroupID, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + member, err := finder.GlobalReplicationGroupMemberByID(conn, globalReplicationGroupID, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + + return member, aws.StringValue(member.Status), nil + } +} + +// UserStatus fetches the ElastiCache user and its Status +func UserStatus(conn *elasticache.ElastiCache, userId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + user, err := finder.ElastiCacheUserById(conn, userId) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return user, aws.StringValue(user.Status), nil + } +} diff --git a/aws/internal/service/elasticache/waiter/waiter.go b/aws/internal/service/elasticache/waiter/waiter.go index 83c95788e2db..12a218e462de 100644 --- a/aws/internal/service/elasticache/waiter/waiter.go +++ b/aws/internal/service/elasticache/waiter/waiter.go @@ -17,6 +17,9 @@ const ( replicationGroupDeletedMinTimeout = 10 * time.Second replicationGroupDeletedDelay = 30 * time.Second + + UserActiveTimeout = 5 * time.Minute + UserDeletedTimeout = 5 * time.Minute ) // ReplicationGroupAvailable waits for a ReplicationGroup to return Available @@ -97,8 +100,8 @@ const ( cacheClusterDeletedDelay = 30 * time.Second ) -// CacheClusterAvailable waits for a ReplicationGroup to return Available -func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.ReplicationGroup, error) { +// CacheClusterAvailable waits for a Cache Cluster to return Available +func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.CacheCluster, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ CacheClusterStatusCreating, @@ -114,14 +117,14 @@ func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*elasticache.ReplicationGroup); ok { + if v, ok := outputRaw.(*elasticache.CacheCluster); ok { return v, err } return nil, err } -// CacheClusterDeleted waits for a ReplicationGroup to be deleted -func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.ReplicationGroup, error) { +// CacheClusterDeleted waits for a Cache Cluster to be deleted +func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.CacheCluster, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ CacheClusterStatusCreating, @@ -140,8 +143,121 @@ func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, t } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*elasticache.ReplicationGroup); ok { + if v, ok := outputRaw.(*elasticache.CacheCluster); ok { + return v, err + } + return nil, err +} + +const ( + GlobalReplicationGroupDefaultCreatedTimeout = 20 * time.Minute + GlobalReplicationGroupDefaultUpdatedTimeout = 40 * time.Minute + GlobalReplicationGroupDefaultDeletedTimeout = 20 * time.Minute + + globalReplicationGroupAvailableMinTimeout = 10 * time.Second + globalReplicationGroupAvailableDelay = 30 * time.Second + + globalReplicationGroupDeletedMinTimeout = 10 * time.Second + globalReplicationGroupDeletedDelay = 30 * time.Second +) + +// GlobalReplicationGroupAvailable waits for a Global Replication Group to be available, +// with status either "available" or "primary-only" +func GlobalReplicationGroupAvailable(conn *elasticache.ElastiCache, globalReplicationGroupID string, timeout time.Duration) (*elasticache.GlobalReplicationGroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{GlobalReplicationGroupStatusCreating, GlobalReplicationGroupStatusModifying}, + Target: []string{GlobalReplicationGroupStatusAvailable, GlobalReplicationGroupStatusPrimaryOnly}, + Refresh: GlobalReplicationGroupStatus(conn, globalReplicationGroupID), + Timeout: timeout, + MinTimeout: globalReplicationGroupAvailableMinTimeout, + Delay: globalReplicationGroupAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + if v, ok := outputRaw.(*elasticache.GlobalReplicationGroup); ok { + return v, err + } + return nil, err +} + +// GlobalReplicationGroupDeleted waits for a Global Replication Group to be deleted +func GlobalReplicationGroupDeleted(conn *elasticache.ElastiCache, globalReplicationGroupID string) (*elasticache.GlobalReplicationGroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + GlobalReplicationGroupStatusAvailable, + GlobalReplicationGroupStatusPrimaryOnly, + GlobalReplicationGroupStatusModifying, + GlobalReplicationGroupStatusDeleting, + }, + Target: []string{}, + Refresh: GlobalReplicationGroupStatus(conn, globalReplicationGroupID), + Timeout: GlobalReplicationGroupDefaultDeletedTimeout, + MinTimeout: globalReplicationGroupDeletedMinTimeout, + Delay: globalReplicationGroupDeletedDelay, + } + + outputRaw, err := stateConf.WaitForState() + if v, ok := outputRaw.(*elasticache.GlobalReplicationGroup); ok { return v, err } return nil, err } + +const ( + // GlobalReplicationGroupDisassociationReadyTimeout specifies how long to wait for a global replication group + // to be in a valid state before disassociating + GlobalReplicationGroupDisassociationReadyTimeout = 45 * time.Minute + + // globalReplicationGroupDisassociationTimeout specifies how long to wait for the actual disassociation + globalReplicationGroupDisassociationTimeout = 20 * time.Minute + + globalReplicationGroupDisassociationMinTimeout = 10 * time.Second + globalReplicationGroupDisassociationDelay = 30 * time.Second +) + +func GlobalReplicationGroupMemberDetached(conn *elasticache.ElastiCache, globalReplicationGroupID, id string) (*elasticache.GlobalReplicationGroupMember, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + GlobalReplicationGroupMemberStatusAssociated, + }, + Target: []string{}, + Refresh: GlobalReplicationGroupMemberStatus(conn, globalReplicationGroupID, id), + Timeout: globalReplicationGroupDisassociationTimeout, + MinTimeout: globalReplicationGroupDisassociationMinTimeout, + Delay: globalReplicationGroupDisassociationDelay, + } + + outputRaw, err := stateConf.WaitForState() + if v, ok := outputRaw.(*elasticache.GlobalReplicationGroupMember); ok { + return v, err + } + return nil, err +} + +// UserActive waits for an ElastiCache user to reach an active state after modifications +func UserActive(conn *elasticache.ElastiCache, userId string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{UserStatusModifying}, + Target: []string{UserStatusActive}, + Refresh: UserStatus(conn, userId), + Timeout: UserActiveTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +// UserDeleted waits for an ElastiCache user to be deleted +func UserDeleted(conn *elasticache.ElastiCache, userId string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{UserStatusDeleting}, + Target: []string{}, + Refresh: UserStatus(conn, userId), + Timeout: UserDeletedTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/internal/service/elbv2/finder/finder.go b/aws/internal/service/elbv2/finder/finder.go new file mode 100644 index 000000000000..f5e033244fcb --- /dev/null +++ b/aws/internal/service/elbv2/finder/finder.go @@ -0,0 +1,94 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" +) + +func ListenerByARN(conn *elbv2.ELBV2, arn string) (*elbv2.Listener, error) { + input := &elbv2.DescribeListenersInput{ + ListenerArns: aws.StringSlice([]string{arn}), + } + + var result *elbv2.Listener + + err := conn.DescribeListenersPages(input, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, l := range page.Listeners { + if l == nil { + continue + } + + if aws.StringValue(l.ListenerArn) == arn { + result = l + return false + } + } + + return !lastPage + }) + + return result, err +} + +func LoadBalancerByARN(conn *elbv2.ELBV2, arn string) (*elbv2.LoadBalancer, error) { + input := &elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: aws.StringSlice([]string{arn}), + } + + var result *elbv2.LoadBalancer + + err := conn.DescribeLoadBalancersPages(input, func(page *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, lb := range page.LoadBalancers { + if lb == nil { + continue + } + + if aws.StringValue(lb.LoadBalancerArn) == arn { + result = lb + return false + } + } + + return !lastPage + }) + + return result, err +} + +func TargetGroupByARN(conn *elbv2.ELBV2, arn string) (*elbv2.TargetGroup, error) { + input := &elbv2.DescribeTargetGroupsInput{ + TargetGroupArns: aws.StringSlice([]string{arn}), + } + + output, err := conn.DescribeTargetGroups(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, targetGroup := range output.TargetGroups { + if targetGroup == nil { + continue + } + + if aws.StringValue(targetGroup.TargetGroupArn) != arn { + continue + } + + return targetGroup, nil + } + + return nil, nil +} diff --git a/aws/internal/service/elbv2/id.go b/aws/internal/service/elbv2/id.go index 34ff840e6763..288dfeaf7460 100644 --- a/aws/internal/service/elbv2/id.go +++ b/aws/internal/service/elbv2/id.go @@ -8,7 +8,7 @@ import ( const listenerCertificateIDSeparator = "_" func ListenerCertificateParseID(id string) (string, string, error) { - parts := strings.Split(id, listenerCertificateIDSeparator) + parts := strings.SplitN(id, listenerCertificateIDSeparator, 2) if len(parts) == 2 && parts[0] != "" && parts[1] != "" { return parts[0], parts[1], nil } diff --git a/aws/internal/service/elbv2/waiter/waiter.go b/aws/internal/service/elbv2/waiter/waiter.go index 3811e6dc7edf..cf8745750134 100644 --- a/aws/internal/service/elbv2/waiter/waiter.go +++ b/aws/internal/service/elbv2/waiter/waiter.go @@ -16,6 +16,21 @@ const ( // Default maximum amount of time to wait for a Load Balancer to be deleted LoadBalancerDeleteTimeout = 10 * time.Minute + + // Default maximum amount of time to wait for Tag Propagation for a Load Balancer + LoadBalancerTagPropagationTimeout = 2 * time.Minute + + // Default maximum amount of time to wait for target group to delete + TargetGroupDeleteTimeout = 2 * time.Minute + + // Default maximum amount of time to wait for network interfaces to propagate + LoadBalancerNetworkInterfaceDetachTimeout = 5 * time.Minute + + LoadBalancerListenerCreateTimeout = 5 * time.Minute + LoadBalancerListenerReadTimeout = 2 * time.Minute + LoadBalancerListenerUpdateTimeout = 5 * time.Minute + + PropagationTimeout = 2 * time.Minute ) // LoadBalancerActive waits for a Load Balancer to return active diff --git a/aws/internal/service/firehose/finder/finder.go b/aws/internal/service/firehose/finder/finder.go new file mode 100644 index 000000000000..a618e82ce3bb --- /dev/null +++ b/aws/internal/service/firehose/finder/finder.go @@ -0,0 +1,48 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/firehose" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func DeliveryStreamByName(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamDescription, error) { + input := &firehose.DescribeDeliveryStreamInput{ + DeliveryStreamName: aws.String(name), + } + + output, err := conn.DescribeDeliveryStream(input) + + if tfawserr.ErrCodeEquals(err, firehose.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.DeliveryStreamDescription == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.DeliveryStreamDescription, nil +} + +func DeliveryStreamEncryptionConfigurationByName(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamEncryptionConfiguration, error) { + output, err := DeliveryStreamByName(conn, name) + + if err != nil { + return nil, err + } + + if output.DeliveryStreamEncryptionConfiguration == nil { + return nil, tfresource.NewEmptyResultError(nil) + } + + return output.DeliveryStreamEncryptionConfiguration, nil +} diff --git a/aws/internal/service/firehose/waiter/status.go b/aws/internal/service/firehose/waiter/status.go new file mode 100644 index 000000000000..33f744aca434 --- /dev/null +++ b/aws/internal/service/firehose/waiter/status.go @@ -0,0 +1,41 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/firehose" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/firehose/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func DeliveryStreamStatus(conn *firehose.Firehose, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DeliveryStreamByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.DeliveryStreamStatus), nil + } +} + +func DeliveryStreamEncryptionConfigurationStatus(conn *firehose.Firehose, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DeliveryStreamEncryptionConfigurationByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/firehose/waiter/waiter.go b/aws/internal/service/firehose/waiter/waiter.go new file mode 100644 index 000000000000..229d01e4511d --- /dev/null +++ b/aws/internal/service/firehose/waiter/waiter.go @@ -0,0 +1,103 @@ +package waiter + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/firehose" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + DeliveryStreamCreatedTimeout = 20 * time.Minute + DeliveryStreamDeletedTimeout = 20 * time.Minute + + DeliveryStreamEncryptionEnabledTimeout = 10 * time.Minute + DeliveryStreamEncryptionDisabledTimeout = 10 * time.Minute +) + +func DeliveryStreamCreated(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{firehose.DeliveryStreamStatusCreating}, + Target: []string{firehose.DeliveryStreamStatusActive}, + Refresh: DeliveryStreamStatus(conn, name), + Timeout: DeliveryStreamCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*firehose.DeliveryStreamDescription); ok { + if status, failureDescription := aws.StringValue(output.DeliveryStreamStatus), output.FailureDescription; status == firehose.DeliveryStreamStatusCreatingFailed && failureDescription != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(failureDescription.Type), aws.StringValue(failureDescription.Details))) + } + + return output, err + } + + return nil, err +} + +func DeliveryStreamDeleted(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{firehose.DeliveryStreamStatusDeleting}, + Target: []string{}, + Refresh: DeliveryStreamStatus(conn, name), + Timeout: DeliveryStreamDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*firehose.DeliveryStreamDescription); ok { + if status, failureDescription := aws.StringValue(output.DeliveryStreamStatus), output.FailureDescription; status == firehose.DeliveryStreamStatusDeletingFailed && failureDescription != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(failureDescription.Type), aws.StringValue(failureDescription.Details))) + } + + return output, err + } + + return nil, err +} + +func DeliveryStreamEncryptionEnabled(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamEncryptionConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{firehose.DeliveryStreamEncryptionStatusEnabling}, + Target: []string{firehose.DeliveryStreamEncryptionStatusEnabled}, + Refresh: DeliveryStreamEncryptionConfigurationStatus(conn, name), + Timeout: DeliveryStreamEncryptionEnabledTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*firehose.DeliveryStreamEncryptionConfiguration); ok { + if status, failureDescription := aws.StringValue(output.Status), output.FailureDescription; status == firehose.DeliveryStreamEncryptionStatusEnablingFailed && failureDescription != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(failureDescription.Type), aws.StringValue(failureDescription.Details))) + } + + return output, err + } + + return nil, err +} + +func DeliveryStreamEncryptionDisabled(conn *firehose.Firehose, name string) (*firehose.DeliveryStreamEncryptionConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{firehose.DeliveryStreamEncryptionStatusDisabling}, + Target: []string{firehose.DeliveryStreamEncryptionStatusDisabled}, + Refresh: DeliveryStreamEncryptionConfigurationStatus(conn, name), + Timeout: DeliveryStreamEncryptionDisabledTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*firehose.DeliveryStreamEncryptionConfiguration); ok { + if status, failureDescription := aws.StringValue(output.Status), output.FailureDescription; status == firehose.DeliveryStreamEncryptionStatusDisablingFailed && failureDescription != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(failureDescription.Type), aws.StringValue(failureDescription.Details))) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/fsx/finder/finder.go b/aws/internal/service/fsx/finder/finder.go new file mode 100644 index 000000000000..6869fde7a96c --- /dev/null +++ b/aws/internal/service/fsx/finder/finder.go @@ -0,0 +1,94 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func AdministrativeActionByFileSystemIDAndActionType(conn *fsx.FSx, fsID, actionType string) (*fsx.AdministrativeAction, error) { + fileSystem, err := FileSystemByID(conn, fsID) + + if err != nil { + return nil, err + } + + for _, administrativeAction := range fileSystem.AdministrativeActions { + if administrativeAction == nil { + continue + } + + if aws.StringValue(administrativeAction.AdministrativeActionType) == actionType { + return administrativeAction, nil + } + } + + // If the administrative action isn't found, assume it's complete. + return &fsx.AdministrativeAction{Status: aws.String(fsx.StatusCompleted)}, nil +} + +func BackupByID(conn *fsx.FSx, id string) (*fsx.Backup, error) { + input := &fsx.DescribeBackupsInput{ + BackupIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeBackups(input) + + if tfawserr.ErrCodeEquals(err, fsx.ErrCodeFileSystemNotFound) || tfawserr.ErrCodeEquals(err, fsx.ErrCodeBackupNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Backups) == 0 || output.Backups[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Backups[0], nil +} + +func FileSystemByID(conn *fsx.FSx, id string) (*fsx.FileSystem, error) { + input := &fsx.DescribeFileSystemsInput{ + FileSystemIds: []*string{aws.String(id)}, + } + + var filesystems []*fsx.FileSystem + + err := conn.DescribeFileSystemsPages(input, func(page *fsx.DescribeFileSystemsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + filesystems = append(filesystems, page.FileSystems...) + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, fsx.ErrCodeFileSystemNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if len(filesystems) == 0 || filesystems[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(filesystems); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return filesystems[0], nil +} diff --git a/aws/internal/service/fsx/waiter/status.go b/aws/internal/service/fsx/waiter/status.go new file mode 100644 index 000000000000..6a73dfaf7771 --- /dev/null +++ b/aws/internal/service/fsx/waiter/status.go @@ -0,0 +1,57 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/fsx/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func AdministrativeActionStatus(conn *fsx.FSx, fsID, actionType string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.AdministrativeActionByFileSystemIDAndActionType(conn, fsID, actionType) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func BackupStatus(conn *fsx.FSx, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.BackupByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Lifecycle), nil + } +} + +func FileSystemStatus(conn *fsx.FSx, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.FileSystemByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Lifecycle), nil + } +} diff --git a/aws/internal/service/fsx/waiter/waiter.go b/aws/internal/service/fsx/waiter/waiter.go new file mode 100644 index 000000000000..3754da70cbb8 --- /dev/null +++ b/aws/internal/service/fsx/waiter/waiter.go @@ -0,0 +1,138 @@ +package waiter + +import ( + "errors" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + BackupAvailableTimeout = 10 * time.Minute + BackupDeletedTimeout = 10 * time.Minute +) + +func AdministrativeActionCompleted(conn *fsx.FSx, fsID, actionType string, timeout time.Duration) (*fsx.AdministrativeAction, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.StatusInProgress, fsx.StatusPending}, + Target: []string{fsx.StatusCompleted, fsx.StatusUpdatedOptimizing}, + Refresh: AdministrativeActionStatus(conn, fsID, actionType), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.AdministrativeAction); ok { + if status, details := aws.StringValue(output.Status), output.FailureDetails; status == fsx.StatusFailed && details != nil { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureDetails.Message))) + } + + return output, err + } + + return nil, err +} + +func BackupAvailable(conn *fsx.FSx, id string) (*fsx.Backup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.BackupLifecycleCreating, fsx.BackupLifecyclePending, fsx.BackupLifecycleTransferring}, + Target: []string{fsx.BackupLifecycleAvailable}, + Refresh: BackupStatus(conn, id), + Timeout: BackupAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.Backup); ok { + return output, err + } + + return nil, err +} + +func BackupDeleted(conn *fsx.FSx, id string) (*fsx.Backup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.FileSystemLifecycleDeleting}, + Target: []string{}, + Refresh: BackupStatus(conn, id), + Timeout: BackupDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.Backup); ok { + return output, err + } + + return nil, err +} + +func FileSystemCreated(conn *fsx.FSx, id string, timeout time.Duration) (*fsx.FileSystem, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.FileSystemLifecycleCreating}, + Target: []string{fsx.FileSystemLifecycleAvailable}, + Refresh: FileSystemStatus(conn, id), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.FileSystem); ok { + if status, details := aws.StringValue(output.Lifecycle), output.FailureDetails; status == fsx.FileSystemLifecycleFailed && details != nil { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureDetails.Message))) + } + + return output, err + } + + return nil, err +} + +func FileSystemUpdated(conn *fsx.FSx, id string, timeout time.Duration) (*fsx.FileSystem, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.FileSystemLifecycleUpdating}, + Target: []string{fsx.FileSystemLifecycleAvailable}, + Refresh: FileSystemStatus(conn, id), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.FileSystem); ok { + if status, details := aws.StringValue(output.Lifecycle), output.FailureDetails; status == fsx.FileSystemLifecycleFailed && details != nil { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureDetails.Message))) + } + + return output, err + } + + return nil, err +} + +func FileSystemDeleted(conn *fsx.FSx, id string, timeout time.Duration) (*fsx.FileSystem, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{fsx.FileSystemLifecycleAvailable, fsx.FileSystemLifecycleDeleting}, + Target: []string{}, + Refresh: FileSystemStatus(conn, id), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*fsx.FileSystem); ok { + if status, details := aws.StringValue(output.Lifecycle), output.FailureDetails; status == fsx.FileSystemLifecycleFailed && details != nil { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureDetails.Message))) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/globalaccelerator/arn.go b/aws/internal/service/globalaccelerator/arn.go new file mode 100644 index 000000000000..7c32a18f9d39 --- /dev/null +++ b/aws/internal/service/globalaccelerator/arn.go @@ -0,0 +1,73 @@ +package globalaccelerator + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +const ( + ARNSeparator = "/" + ARNService = "globalaccelerator" +) + +// EndpointGroupARNToListenerARN converts an endpoint group ARN to a listener ARN. +// See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsglobalaccelerator.html#awsglobalaccelerator-resources-for-iam-policies. +func EndpointGroupARNToListenerARN(inputARN string) (string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + resourceParts := strings.Split(parsedARN.Resource, ARNSeparator) + + if actual, expected := len(resourceParts), 6; actual < expected { + return "", fmt.Errorf("expected at least %d resource parts in ARN (%s), got: %d", expected, inputARN, actual) + } + + outputARN := arn.ARN{ + Partition: parsedARN.Partition, + Service: parsedARN.Service, + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + Resource: strings.Join(resourceParts[0:4], ARNSeparator), + }.String() + + return outputARN, nil +} + +// ListenerOrEndpointGroupARNToAcceleratorARN converts a listener or endpoint group ARN to an accelerator ARN. +// See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsglobalaccelerator.html#awsglobalaccelerator-resources-for-iam-policies. +func ListenerOrEndpointGroupARNToAcceleratorARN(inputARN string) (string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + resourceParts := strings.Split(parsedARN.Resource, ARNSeparator) + + if actual, expected := len(resourceParts), 4; actual < expected { + return "", fmt.Errorf("expected at least %d resource parts in ARN (%s), got: %d", expected, inputARN, actual) + } + + outputARN := arn.ARN{ + Partition: parsedARN.Partition, + Service: parsedARN.Service, + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + Resource: strings.Join(resourceParts[0:2], ARNSeparator), + }.String() + + return outputARN, nil +} diff --git a/aws/internal/service/globalaccelerator/arn_test.go b/aws/internal/service/globalaccelerator/arn_test.go new file mode 100644 index 000000000000..083306e85c40 --- /dev/null +++ b/aws/internal/service/globalaccelerator/arn_test.go @@ -0,0 +1,127 @@ +package globalaccelerator_test + +import ( + "regexp" + "testing" + + tfglobalaccelerator "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/globalaccelerator" +) + +func TestEndpointGroupARNToListenerARN(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedARN string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2::123456789012:accelerator/a-123/listener/l-456/endpoint-group/eg-789", + ExpectedError: regexp.MustCompile(`expected service globalaccelerator`), + }, + { + TestName: "invalid ARN resource parts", + InputARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123/listener/l-456", + ExpectedError: regexp.MustCompile(`expected at least 6 resource parts`), + }, + { + TestName: "valid ARN", + InputARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123/listener/l-456/endpoint-group/eg-789", + ExpectedARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123/listener/l-456", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfglobalaccelerator.EndpointGroupARNToListenerARN(testCase.InputARN) + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if got != testCase.ExpectedARN { + t.Errorf("got %s, expected %s", got, testCase.ExpectedARN) + } + }) + } +} + +func TestListenerOrEndpointGroupARNToAcceleratorARN(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedARN string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2::123456789012:accelerator/a-123/listener/l-456", + ExpectedError: regexp.MustCompile(`expected service globalaccelerator`), + }, + { + TestName: "invalid ARN resource parts", + InputARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123", + ExpectedError: regexp.MustCompile(`expected at least 4 resource parts`), + }, + { + TestName: "valid listener ARN", + InputARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123/listener/l-456", + ExpectedARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123", + }, + { + TestName: "valid endpoint group ARN", + InputARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123/listener/l-456/endpoint-group/eg-789", + ExpectedARN: "arn:aws:globalaccelerator::123456789012:accelerator/a-123", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfglobalaccelerator.ListenerOrEndpointGroupARNToAcceleratorARN(testCase.InputARN) + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if got != testCase.ExpectedARN { + t.Errorf("got %s, expected %s", got, testCase.ExpectedARN) + } + }) + } +} diff --git a/aws/internal/service/globalaccelerator/finder/finder.go b/aws/internal/service/globalaccelerator/finder/finder.go index 53429676fc88..4483ca03f878 100644 --- a/aws/internal/service/globalaccelerator/finder/finder.go +++ b/aws/internal/service/globalaccelerator/finder/finder.go @@ -3,18 +3,150 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/globalaccelerator" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +// AcceleratorByARN returns the accelerator corresponding to the specified ARN. +// Returns NotFoundError if no accelerator is found. +func AcceleratorByARN(conn *globalaccelerator.GlobalAccelerator, arn string) (*globalaccelerator.Accelerator, error) { + input := &globalaccelerator.DescribeAcceleratorInput{ + AcceleratorArn: aws.String(arn), + } + + return Accelerator(conn, input) +} + +// Accelerator returns the accelerator corresponding to the specified input. +// Returns NotFoundError if no accelerator is found. +func Accelerator(conn *globalaccelerator.GlobalAccelerator, input *globalaccelerator.DescribeAcceleratorInput) (*globalaccelerator.Accelerator, error) { + output, err := conn.DescribeAccelerator(input) + + if tfawserr.ErrCodeEquals(err, globalaccelerator.ErrCodeAcceleratorNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Accelerator == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Accelerator, nil +} + +// AcceleratorAttributesByARN returns the accelerator attributes corresponding to the specified ARN. +// Returns NotFoundError if no accelerator is found. +func AcceleratorAttributesByARN(conn *globalaccelerator.GlobalAccelerator, arn string) (*globalaccelerator.AcceleratorAttributes, error) { + input := &globalaccelerator.DescribeAcceleratorAttributesInput{ + AcceleratorArn: aws.String(arn), + } + + return AcceleratorAttributes(conn, input) +} + +// AcceleratorAttributes returns the accelerator attributes corresponding to the specified input. +// Returns NotFoundError if no accelerator is found. +func AcceleratorAttributes(conn *globalaccelerator.GlobalAccelerator, input *globalaccelerator.DescribeAcceleratorAttributesInput) (*globalaccelerator.AcceleratorAttributes, error) { + output, err := conn.DescribeAcceleratorAttributes(input) + + if tfawserr.ErrCodeEquals(err, globalaccelerator.ErrCodeAcceleratorNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AcceleratorAttributes == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.AcceleratorAttributes, nil +} + // EndpointGroupByARN returns the endpoint group corresponding to the specified ARN. +// Returns NotFoundError if no endpoint group is found. func EndpointGroupByARN(conn *globalaccelerator.GlobalAccelerator, arn string) (*globalaccelerator.EndpointGroup, error) { input := &globalaccelerator.DescribeEndpointGroupInput{ EndpointGroupArn: aws.String(arn), } + return EndpointGroup(conn, input) +} + +// EndpointGroup returns the endpoint group corresponding to the specified input. +// Returns NotFoundError if no endpoint group is found. +func EndpointGroup(conn *globalaccelerator.GlobalAccelerator, input *globalaccelerator.DescribeEndpointGroupInput) (*globalaccelerator.EndpointGroup, error) { output, err := conn.DescribeEndpointGroup(input) + + if tfawserr.ErrCodeEquals(err, globalaccelerator.ErrCodeEndpointGroupNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } + if output == nil || output.EndpointGroup == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + return output.EndpointGroup, nil } + +// ListenerByARN returns the listener corresponding to the specified ARN. +// Returns NotFoundError if no listener is found. +func ListenerByARN(conn *globalaccelerator.GlobalAccelerator, arn string) (*globalaccelerator.Listener, error) { + input := &globalaccelerator.DescribeListenerInput{ + ListenerArn: aws.String(arn), + } + + return Listener(conn, input) +} + +// Listener returns the listener corresponding to the specified input. +// Returns NotFoundError if no listener is found. +func Listener(conn *globalaccelerator.GlobalAccelerator, input *globalaccelerator.DescribeListenerInput) (*globalaccelerator.Listener, error) { + output, err := conn.DescribeListener(input) + + if tfawserr.ErrCodeEquals(err, globalaccelerator.ErrCodeListenerNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Listener == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Listener, nil +} diff --git a/aws/internal/service/globalaccelerator/waiter/status.go b/aws/internal/service/globalaccelerator/waiter/status.go new file mode 100644 index 000000000000..47763b8f8781 --- /dev/null +++ b/aws/internal/service/globalaccelerator/waiter/status.go @@ -0,0 +1,26 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/globalaccelerator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/globalaccelerator/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +// AcceleratorStatus fetches the Accelerator and its Status +func AcceleratorStatus(conn *globalaccelerator.GlobalAccelerator, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + accelerator, err := finder.AcceleratorByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return accelerator, aws.StringValue(accelerator.Status), nil + } +} diff --git a/aws/internal/service/globalaccelerator/waiter/waiter.go b/aws/internal/service/globalaccelerator/waiter/waiter.go new file mode 100644 index 000000000000..1fe97d639778 --- /dev/null +++ b/aws/internal/service/globalaccelerator/waiter/waiter.go @@ -0,0 +1,26 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/globalaccelerator" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// AcceleratorDeployed waits for an Accelerator to return Deployed +func AcceleratorDeployed(conn *globalaccelerator.GlobalAccelerator, arn string, timeout time.Duration) (*globalaccelerator.Accelerator, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{globalaccelerator.AcceleratorStatusInProgress}, + Target: []string{globalaccelerator.AcceleratorStatusDeployed}, + Refresh: AcceleratorStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*globalaccelerator.Accelerator); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/glue/enum.go b/aws/internal/service/glue/enum.go new file mode 100644 index 000000000000..ce6a74772833 --- /dev/null +++ b/aws/internal/service/glue/enum.go @@ -0,0 +1,8 @@ +package glue + +const ( + DevEndpointStatusFailed = "FAILED" + DevEndpointStatusProvisioning = "PROVISIONING" + DevEndpointStatusReady = "READY" + DevEndpointStatusTerminating = "TERMINATING" +) diff --git a/aws/internal/service/glue/finder/finder.go b/aws/internal/service/glue/finder/finder.go index f42b80a098b7..dae681830170 100644 --- a/aws/internal/service/glue/finder/finder.go +++ b/aws/internal/service/glue/finder/finder.go @@ -3,9 +3,39 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/glue" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfglue "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue" ) +func DevEndpointByName(conn *glue.Glue, name string) (*glue.DevEndpoint, error) { + input := &glue.GetDevEndpointInput{ + EndpointName: aws.String(name), + } + + output, err := conn.GetDevEndpoint(input) + + if tfawserr.ErrCodeEquals(err, glue.ErrCodeEntityNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.DevEndpoint == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.DevEndpoint, nil +} + // TableByName returns the Table corresponding to the specified name. func TableByName(conn *glue.Glue, catalogID, dbName, name string) (*glue.GetTableOutput, error) { input := &glue.GetTableInput{ @@ -22,6 +52,20 @@ func TableByName(conn *glue.Glue, catalogID, dbName, name string) (*glue.GetTabl return output, nil } +// TriggerByName returns the Trigger corresponding to the specified name. +func TriggerByName(conn *glue.Glue, name string) (*glue.GetTriggerOutput, error) { + input := &glue.GetTriggerInput{ + Name: aws.String(name), + } + + output, err := conn.GetTrigger(input) + if err != nil { + return nil, err + } + + return output, nil +} + // RegistryByID returns the Registry corresponding to the specified ID. func RegistryByID(conn *glue.Glue, id string) (*glue.GetRegistryOutput, error) { input := &glue.GetRegistryInput{ diff --git a/aws/internal/service/glue/waiter/status.go b/aws/internal/service/glue/waiter/status.go index ed80b5ed589f..3262cffb8053 100644 --- a/aws/internal/service/glue/waiter/status.go +++ b/aws/internal/service/glue/waiter/status.go @@ -1,13 +1,11 @@ package waiter import ( - "fmt" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/glue" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -110,26 +108,16 @@ func TriggerStatus(conn *glue.Glue, triggerName string) resource.StateRefreshFun func GlueDevEndpointStatus(conn *glue.Glue, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - getDevEndpointInput := &glue.GetDevEndpointInput{ - EndpointName: aws.String(name), - } - endpoint, err := conn.GetDevEndpoint(getDevEndpointInput) - if err != nil { - if tfawserr.ErrCodeEquals(err, glue.ErrCodeEntityNotFoundException) { - return nil, "", nil - } + output, err := finder.DevEndpointByName(conn, name) - return nil, "", err - } - - if endpoint == nil || endpoint.DevEndpoint == nil { + if tfresource.NotFound(err) { return nil, "", nil } - if aws.StringValue(endpoint.DevEndpoint.Status) == "FAILED" && endpoint.DevEndpoint.FailureReason != nil { - return endpoint, aws.StringValue(endpoint.DevEndpoint.Status), fmt.Errorf("%s", aws.StringValue(endpoint.DevEndpoint.FailureReason)) + if err != nil { + return nil, "", err } - return endpoint, aws.StringValue(endpoint.DevEndpoint.Status), nil + return output, aws.StringValue(output.Status), nil } } diff --git a/aws/internal/service/glue/waiter/waiter.go b/aws/internal/service/glue/waiter/waiter.go index aea848fa40e9..8c5158d3f03a 100644 --- a/aws/internal/service/glue/waiter/waiter.go +++ b/aws/internal/service/glue/waiter/waiter.go @@ -1,10 +1,14 @@ package waiter import ( + "errors" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/glue" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfglue "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/glue" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -14,8 +18,8 @@ const ( SchemaAvailableTimeout = 2 * time.Minute SchemaDeleteTimeout = 2 * time.Minute SchemaVersionAvailableTimeout = 2 * time.Minute - TriggerCreateTimeout = 2 * time.Minute - TriggerDeleteTimeout = 2 * time.Minute + TriggerCreateTimeout = 5 * time.Minute + TriggerDeleteTimeout = 5 * time.Minute ) // MLTransformDeleted waits for an MLTransform to return Deleted @@ -114,6 +118,7 @@ func TriggerCreated(conn *glue.Glue, triggerName string) (*glue.GetTriggerOutput Pending: []string{ glue.TriggerStateActivating, glue.TriggerStateCreating, + glue.TriggerStateUpdating, }, Target: []string{ glue.TriggerStateActivated, @@ -150,38 +155,42 @@ func TriggerDeleted(conn *glue.Glue, triggerName string) (*glue.GetTriggerOutput return nil, err } -// GlueDevEndpointCreated waits for a Glue Dev Endpoint to become available. -func GlueDevEndpointCreated(conn *glue.Glue, devEndpointId string) (*glue.GetDevEndpointOutput, error) { +func GlueDevEndpointCreated(conn *glue.Glue, name string) (*glue.DevEndpoint, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{ - "PROVISIONING", - }, - Target: []string{"READY"}, - Refresh: GlueDevEndpointStatus(conn, devEndpointId), + Pending: []string{tfglue.DevEndpointStatusProvisioning}, + Target: []string{tfglue.DevEndpointStatusReady}, + Refresh: GlueDevEndpointStatus(conn, name), Timeout: 15 * time.Minute, } outputRaw, err := stateConf.WaitForState() - if output, ok := outputRaw.(*glue.GetDevEndpointOutput); ok { + if output, ok := outputRaw.(*glue.DevEndpoint); ok { + if status := aws.StringValue(output.Status); status == tfglue.DevEndpointStatusFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureReason))) + } + return output, err } return nil, err } -// GlueDevEndpointDeleted waits for a Glue Dev Endpoint to become terminated. -func GlueDevEndpointDeleted(conn *glue.Glue, devEndpointId string) (*glue.GetDevEndpointOutput, error) { +func GlueDevEndpointDeleted(conn *glue.Glue, name string) (*glue.DevEndpoint, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{"TERMINATING"}, + Pending: []string{tfglue.DevEndpointStatusTerminating}, Target: []string{}, - Refresh: GlueDevEndpointStatus(conn, devEndpointId), + Refresh: GlueDevEndpointStatus(conn, name), Timeout: 15 * time.Minute, } outputRaw, err := stateConf.WaitForState() - if output, ok := outputRaw.(*glue.GetDevEndpointOutput); ok { + if output, ok := outputRaw.(*glue.DevEndpoint); ok { + if status := aws.StringValue(output.Status); status == tfglue.DevEndpointStatusFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureReason))) + } + return output, err } diff --git a/aws/internal/service/iam/arn.go b/aws/internal/service/iam/arn.go new file mode 100644 index 000000000000..a42bf6e7db0b --- /dev/null +++ b/aws/internal/service/iam/arn.go @@ -0,0 +1,40 @@ +package iam + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +const ( + ARNSeparator = "/" + ARNService = "iam" + + InstanceProfileResourcePrefix = "instance-profile" +) + +// InstanceProfileARNToName converts Amazon Resource Name (ARN) to Name. +func InstanceProfileARNToName(inputARN string) (string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + resourceParts := strings.Split(parsedARN.Resource, ARNSeparator) + + if actual, expected := len(resourceParts), 2; actual < expected { + return "", fmt.Errorf("expected at least %d resource parts in ARN (%s), got: %d", expected, inputARN, actual) + } + + if actual, expected := resourceParts[0], InstanceProfileResourcePrefix; actual != expected { + return "", fmt.Errorf("expected resource prefix %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + return resourceParts[len(resourceParts)-1], nil +} diff --git a/aws/internal/service/iam/arn_test.go b/aws/internal/service/iam/arn_test.go new file mode 100644 index 000000000000..1b07bf444239 --- /dev/null +++ b/aws/internal/service/iam/arn_test.go @@ -0,0 +1,75 @@ +package iam_test + +import ( + "regexp" + "testing" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam" +) + +func TestInstanceProfileARNToName(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedName string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678", + ExpectedError: regexp.MustCompile(`expected service iam`), + }, + { + TestName: "invalid ARN resource parts", + InputARN: "arn:aws:iam:us-east-1:123456789012:name", + ExpectedError: regexp.MustCompile(`expected at least 2 resource parts`), + }, + { + TestName: "invalid ARN resource prefix", + InputARN: "arn:aws:iam:us-east-1:123456789012:role/name", + ExpectedError: regexp.MustCompile(`expected resource prefix instance-profile`), + }, + { + TestName: "valid ARN", + InputARN: "arn:aws:iam:us-east-1:123456789012:instance-profile/name", + ExpectedName: "name", + }, + { + TestName: "valid ARN with multiple parts", + InputARN: "arn:aws:iam:us-east-1:123456789012:instance-profile/path/name", + ExpectedName: "name", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := iam.InstanceProfileARNToName(testCase.InputARN) + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if got != testCase.ExpectedName { + t.Errorf("got %s, expected %s", got, testCase.ExpectedName) + } + }) + } +} diff --git a/aws/internal/service/iam/finder/finder.go b/aws/internal/service/iam/finder/finder.go new file mode 100644 index 000000000000..7626dc2003af --- /dev/null +++ b/aws/internal/service/iam/finder/finder.go @@ -0,0 +1,139 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +// GroupAttachedPolicy returns the AttachedPolicy corresponding to the specified group and policy ARN. +func GroupAttachedPolicy(conn *iam.IAM, groupName string, policyARN string) (*iam.AttachedPolicy, error) { + input := &iam.ListAttachedGroupPoliciesInput{ + GroupName: aws.String(groupName), + } + + var result *iam.AttachedPolicy + + err := conn.ListAttachedGroupPoliciesPages(input, func(page *iam.ListAttachedGroupPoliciesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, attachedPolicy := range page.AttachedPolicies { + if attachedPolicy == nil { + continue + } + + if aws.StringValue(attachedPolicy.PolicyArn) == policyARN { + result = attachedPolicy + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +// UserAttachedPolicy returns the AttachedPolicy corresponding to the specified user and policy ARN. +func UserAttachedPolicy(conn *iam.IAM, userName string, policyARN string) (*iam.AttachedPolicy, error) { + input := &iam.ListAttachedUserPoliciesInput{ + UserName: aws.String(userName), + } + + var result *iam.AttachedPolicy + + err := conn.ListAttachedUserPoliciesPages(input, func(page *iam.ListAttachedUserPoliciesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, attachedPolicy := range page.AttachedPolicies { + if attachedPolicy == nil { + continue + } + + if aws.StringValue(attachedPolicy.PolicyArn) == policyARN { + result = attachedPolicy + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +// Policies returns the Policies corresponding to the specified ARN, name, and/or path-prefix. +func Policies(conn *iam.IAM, arn, name, pathPrefix string) ([]*iam.Policy, error) { + input := &iam.ListPoliciesInput{} + + if pathPrefix != "" { + input.PathPrefix = aws.String(pathPrefix) + } + + var results []*iam.Policy + + err := conn.ListPoliciesPages(input, func(page *iam.ListPoliciesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, p := range page.Policies { + if p == nil { + continue + } + + if arn != "" && arn != aws.StringValue(p.Arn) { + continue + } + + if name != "" && name != aws.StringValue(p.PolicyName) { + continue + } + + results = append(results, p) + } + + return !lastPage + }) + + return results, err +} + +func RoleByName(conn *iam.IAM, name string) (*iam.Role, error) { + input := &iam.GetRoleInput{ + RoleName: aws.String(name), + } + + output, err := conn.GetRole(input) + + if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Role == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Role, nil +} diff --git a/aws/internal/service/iot/finder/iot.go b/aws/internal/service/iot/finder/iot.go new file mode 100644 index 000000000000..cfde1ca65971 --- /dev/null +++ b/aws/internal/service/iot/finder/iot.go @@ -0,0 +1,34 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iot" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func AuthorizerByName(conn *iot.IoT, name string) (*iot.AuthorizerDescription, error) { + input := &iot.DescribeAuthorizerInput{ + AuthorizerName: aws.String(name), + } + + output, err := conn.DescribeAuthorizer(input) + + if tfawserr.ErrCodeEquals(err, iot.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AuthorizerDescription == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.AuthorizerDescription, nil +} diff --git a/aws/internal/service/kafka/enum.go b/aws/internal/service/kafka/enum.go new file mode 100644 index 000000000000..042106a2b873 --- /dev/null +++ b/aws/internal/service/kafka/enum.go @@ -0,0 +1,8 @@ +package kafka + +const ( + ClusterOperationStatePending = "PENDING" + ClusterOperationStateUpdateComplete = "UPDATE_COMPLETE" + ClusterOperationStateUpdateFailed = "UPDATE_FAILED" + ClusterOperationStateUpdateInProgress = "UPDATE_IN_PROGRESS" +) diff --git a/aws/internal/service/kafka/finder/finder.go b/aws/internal/service/kafka/finder/finder.go new file mode 100644 index 000000000000..7cf54d92a922 --- /dev/null +++ b/aws/internal/service/kafka/finder/finder.go @@ -0,0 +1,84 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kafka" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ClusterByARN(conn *kafka.Kafka, arn string) (*kafka.ClusterInfo, error) { + input := &kafka.DescribeClusterInput{ + ClusterArn: aws.String(arn), + } + + output, err := conn.DescribeCluster(input) + + if tfawserr.ErrCodeEquals(err, kafka.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ClusterInfo == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ClusterInfo, nil +} + +func ClusterOperationByARN(conn *kafka.Kafka, arn string) (*kafka.ClusterOperationInfo, error) { + input := &kafka.DescribeClusterOperationInput{ + ClusterOperationArn: aws.String(arn), + } + + output, err := conn.DescribeClusterOperation(input) + + if tfawserr.ErrCodeEquals(err, kafka.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ClusterOperationInfo == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ClusterOperationInfo, nil +} + +func ConfigurationByARN(conn *kafka.Kafka, arn string) (*kafka.DescribeConfigurationOutput, error) { + input := &kafka.DescribeConfigurationInput{ + Arn: aws.String(arn), + } + + output, err := conn.DescribeConfiguration(input) + + if tfawserr.ErrMessageContains(err, kafka.ErrCodeBadRequestException, "Configuration ARN does not exist") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} diff --git a/aws/internal/service/kafka/sort.go b/aws/internal/service/kafka/sort.go new file mode 100644 index 000000000000..90ac9d9158da --- /dev/null +++ b/aws/internal/service/kafka/sort.go @@ -0,0 +1,17 @@ +package kafka + +import ( + "sort" + "strings" +) + +const ( + endpointSeparator = "," +) + +// SortEndpointsString sorts a comma-separated list of endpoints. +func SortEndpointsString(s string) string { + parts := strings.Split(s, endpointSeparator) + sort.Strings(parts) + return strings.Join(parts, endpointSeparator) +} diff --git a/aws/internal/service/kafka/sort_test.go b/aws/internal/service/kafka/sort_test.go new file mode 100644 index 000000000000..ee4b0bc6f9b9 --- /dev/null +++ b/aws/internal/service/kafka/sort_test.go @@ -0,0 +1,41 @@ +package kafka_test + +import ( + "testing" + + tfkafka "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kafka" +) + +func TestSortEndpointsString(t *testing.T) { + testCases := []struct { + TestName string + Input string + Expected string + }{ + { + TestName: "empty", + Input: "", + Expected: "", + }, + { + TestName: "one endpoint", + Input: "this:123", + Expected: "this:123", + }, + { + TestName: "three endpoints", + Input: "this:123,is:147,just.a.test:443", + Expected: "is:147,just.a.test:443,this:123", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got := tfkafka.SortEndpointsString(testCase.Input) + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} diff --git a/aws/internal/service/kafka/waiter/status.go b/aws/internal/service/kafka/waiter/status.go index 52186f45f3f4..11760b71b190 100644 --- a/aws/internal/service/kafka/waiter/status.go +++ b/aws/internal/service/kafka/waiter/status.go @@ -3,34 +3,53 @@ package waiter import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kafka" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kafka/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -const ( - ConfigurationStateDeleted = "Deleted" - ConfigurationStateUnknown = "Unknown" -) - -// ConfigurationState fetches the Operation and its Status -func ConfigurationState(conn *kafka.Kafka, arn string) resource.StateRefreshFunc { +func ClusterState(conn *kafka.Kafka, arn string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &kafka.DescribeConfigurationInput{ - Arn: aws.String(arn), + output, err := finder.ClusterByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil } - output, err := conn.DescribeConfiguration(input) + if err != nil { + return nil, "", err + } - if tfawserr.ErrMessageContains(err, kafka.ErrCodeBadRequestException, "Configuration ARN does not exist") { - return output, ConfigurationStateDeleted, nil + return output, aws.StringValue(output.State), nil + } +} + +func ClusterOperationState(conn *kafka.Kafka, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterOperationByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return output, ConfigurationStateUnknown, err + return nil, "", err + } + + return output, aws.StringValue(output.OperationState), nil + } +} + +func ConfigurationState(conn *kafka.Kafka, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ConfigurationByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil } - if output == nil { - return output, ConfigurationStateUnknown, nil + if err != nil { + return nil, "", err } return output, aws.StringValue(output.State), nil diff --git a/aws/internal/service/kafka/waiter/waiter.go b/aws/internal/service/kafka/waiter/waiter.go index 2f84656adfe7..3fb99e45d855 100644 --- a/aws/internal/service/kafka/waiter/waiter.go +++ b/aws/internal/service/kafka/waiter/waiter.go @@ -1,22 +1,87 @@ package waiter import ( + "fmt" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kafka" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfkafka "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kafka" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - // Maximum amount of time to wait for an Configuration to return Deleted ConfigurationDeletedTimeout = 5 * time.Minute ) -// ConfigurationDeleted waits for an Configuration to return Deleted +func ClusterCreated(conn *kafka.Kafka, arn string, timeout time.Duration) (*kafka.ClusterInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kafka.ClusterStateCreating}, + Target: []string{kafka.ClusterStateActive}, + Refresh: ClusterState(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*kafka.ClusterInfo); ok { + if state, stateInfo := aws.StringValue(output.State), output.StateInfo; state == kafka.ClusterStateFailed && stateInfo != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(stateInfo.Code), aws.StringValue(stateInfo.Message))) + } + + return output, err + } + + return nil, err +} + +func ClusterDeleted(conn *kafka.Kafka, arn string, timeout time.Duration) (*kafka.ClusterInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kafka.ClusterStateDeleting}, + Target: []string{}, + Refresh: ClusterState(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*kafka.ClusterInfo); ok { + if state, stateInfo := aws.StringValue(output.State), output.StateInfo; state == kafka.ClusterStateFailed && stateInfo != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(stateInfo.Code), aws.StringValue(stateInfo.Message))) + } + + return output, err + } + + return nil, err +} + +func ClusterOperationCompleted(conn *kafka.Kafka, arn string, timeout time.Duration) (*kafka.ClusterOperationInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfkafka.ClusterOperationStatePending, tfkafka.ClusterOperationStateUpdateInProgress}, + Target: []string{tfkafka.ClusterOperationStateUpdateComplete}, + Refresh: ClusterOperationState(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*kafka.ClusterOperationInfo); ok { + if state, errorInfo := aws.StringValue(output.OperationState), output.ErrorInfo; state == tfkafka.ClusterOperationStateUpdateFailed && errorInfo != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(errorInfo.ErrorCode), aws.StringValue(errorInfo.ErrorString))) + } + + return output, err + } + + return nil, err +} + func ConfigurationDeleted(conn *kafka.Kafka, arn string) (*kafka.DescribeConfigurationOutput, error) { stateConf := &resource.StateChangeConf{ Pending: []string{kafka.ConfigurationStateDeleting}, - Target: []string{ConfigurationStateDeleted}, + Target: []string{}, Refresh: ConfigurationState(conn, arn), Timeout: ConfigurationDeletedTimeout, } diff --git a/aws/internal/service/kinesis/finder/finder.go b/aws/internal/service/kinesis/finder/finder.go new file mode 100644 index 000000000000..ac13b1b0d059 --- /dev/null +++ b/aws/internal/service/kinesis/finder/finder.go @@ -0,0 +1,25 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kinesis" +) + +// StreamConsumerByARN returns the stream consumer corresponding to the specified ARN. +// Returns nil if no stream consumer is found. +func StreamConsumerByARN(conn *kinesis.Kinesis, arn string) (*kinesis.ConsumerDescription, error) { + input := &kinesis.DescribeStreamConsumerInput{ + ConsumerARN: aws.String(arn), + } + + output, err := conn.DescribeStreamConsumer(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.ConsumerDescription, nil +} diff --git a/aws/internal/service/kinesis/waiter/status.go b/aws/internal/service/kinesis/waiter/status.go new file mode 100644 index 000000000000..b8403ac6e27d --- /dev/null +++ b/aws/internal/service/kinesis/waiter/status.go @@ -0,0 +1,30 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kinesis/finder" +) + +const ( + StreamConsumerStatusNotFound = "NotFound" + StreamConsumerStatusUnknown = "Unknown" +) + +// StreamConsumerStatus fetches the StreamConsumer and its Status +func StreamConsumerStatus(conn *kinesis.Kinesis, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + consumer, err := finder.StreamConsumerByARN(conn, arn) + + if err != nil { + return nil, StreamConsumerStatusUnknown, err + } + + if consumer == nil { + return nil, StreamConsumerStatusNotFound, nil + } + + return consumer, aws.StringValue(consumer.ConsumerStatus), nil + } +} diff --git a/aws/internal/service/kinesis/waiter/waiter.go b/aws/internal/service/kinesis/waiter/waiter.go new file mode 100644 index 000000000000..1e3814bcd656 --- /dev/null +++ b/aws/internal/service/kinesis/waiter/waiter.go @@ -0,0 +1,49 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + StreamConsumerCreatedTimeout = 5 * time.Minute + StreamConsumerDeletedTimeout = 5 * time.Minute +) + +// StreamConsumerCreated waits for an Stream Consumer to return Active +func StreamConsumerCreated(conn *kinesis.Kinesis, arn string) (*kinesis.ConsumerDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesis.ConsumerStatusCreating}, + Target: []string{kinesis.ConsumerStatusActive}, + Refresh: StreamConsumerStatus(conn, arn), + Timeout: StreamConsumerCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesis.ConsumerDescription); ok { + return v, err + } + + return nil, err +} + +// StreamConsumerDeleted waits for a Stream Consumer to be deleted +func StreamConsumerDeleted(conn *kinesis.Kinesis, arn string) (*kinesis.ConsumerDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesis.ConsumerStatusDeleting}, + Target: []string{}, + Refresh: StreamConsumerStatus(conn, arn), + Timeout: StreamConsumerDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesis.ConsumerDescription); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/kinesisanalytics/finder/finder.go b/aws/internal/service/kinesisanalytics/finder/finder.go index 1cbe195c57fb..9f6de54f0a01 100644 --- a/aws/internal/service/kinesisanalytics/finder/finder.go +++ b/aws/internal/service/kinesisanalytics/finder/finder.go @@ -3,18 +3,42 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kinesisanalytics" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -// ApplicationByName returns the application corresponding to the specified name. -func ApplicationByName(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { +// ApplicationDetailByName returns the application corresponding to the specified name. +// Returns NotFoundError if no application is found. +func ApplicationDetailByName(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { input := &kinesisanalytics.DescribeApplicationInput{ ApplicationName: aws.String(name), } + return ApplicationDetail(conn, input) +} + +// ApplicationDetail returns the application details corresponding to the specified name. +// Returns NotFoundError if no application is found. +func ApplicationDetail(conn *kinesisanalytics.KinesisAnalytics, input *kinesisanalytics.DescribeApplicationInput) (*kinesisanalytics.ApplicationDetail, error) { output, err := conn.DescribeApplication(input) + + if tfawserr.ErrCodeEquals(err, kinesisanalytics.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } + if output == nil || output.ApplicationDetail == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + return output.ApplicationDetail, nil } diff --git a/aws/internal/service/kinesisanalytics/waiter/status.go b/aws/internal/service/kinesisanalytics/waiter/status.go index 0a326c79d213..a8d537b0296d 100644 --- a/aws/internal/service/kinesisanalytics/waiter/status.go +++ b/aws/internal/service/kinesisanalytics/waiter/status.go @@ -3,29 +3,24 @@ package waiter import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kinesisanalytics" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kinesisanalytics/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -const ( - applicationStatusNotFound = "NotFound" - applicationStatusUnknown = "Unknown" -) - -// ApplicationStatus fetches the Application and its Status +// ApplicationStatus fetches the ApplicationDetail and its Status func ApplicationStatus(conn *kinesisanalytics.KinesisAnalytics, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - application, err := finder.ApplicationByName(conn, name) + applicationDetail, err := finder.ApplicationDetailByName(conn, name) - if tfawserr.ErrCodeEquals(err, kinesisanalytics.ErrCodeResourceNotFoundException) { - return nil, applicationStatusNotFound, nil + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return nil, applicationStatusUnknown, err + return nil, "", err } - return application, aws.StringValue(application.ApplicationStatus), nil + return applicationDetail, aws.StringValue(applicationDetail.ApplicationStatus), nil } } diff --git a/aws/internal/service/kinesisanalytics/waiter/waiter.go b/aws/internal/service/kinesisanalytics/waiter/waiter.go index 0c9b9e290e52..79cc43ad6f79 100644 --- a/aws/internal/service/kinesisanalytics/waiter/waiter.go +++ b/aws/internal/service/kinesisanalytics/waiter/waiter.go @@ -10,18 +10,79 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) +const ( + ApplicationDeletedTimeout = 5 * time.Minute + ApplicationStartedTimeout = 5 * time.Minute + ApplicationStoppedTimeout = 5 * time.Minute + ApplicationUpdatedTimeout = 5 * time.Minute +) + // ApplicationDeleted waits for an Application to return Deleted -func ApplicationDeleted(conn *kinesisanalytics.KinesisAnalytics, name string, timeout time.Duration) (*kinesisanalytics.ApplicationSummary, error) { +func ApplicationDeleted(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{kinesisanalytics.ApplicationStatusRunning, kinesisanalytics.ApplicationStatusDeleting}, + Pending: []string{kinesisanalytics.ApplicationStatusDeleting}, Target: []string{}, Refresh: ApplicationStatus(conn, name), - Timeout: timeout, + Timeout: ApplicationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalytics.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationStarted waits for an Application to start +func ApplicationStarted(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalytics.ApplicationStatusStarting}, + Target: []string{kinesisanalytics.ApplicationStatusRunning}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationStartedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalytics.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationStopped waits for an Application to stop +func ApplicationStopped(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalytics.ApplicationStatusStopping}, + Target: []string{kinesisanalytics.ApplicationStatusReady}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationStoppedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalytics.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationUpdated waits for an Application to update +func ApplicationUpdated(conn *kinesisanalytics.KinesisAnalytics, name string) (*kinesisanalytics.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalytics.ApplicationStatusUpdating}, + Target: []string{kinesisanalytics.ApplicationStatusReady, kinesisanalytics.ApplicationStatusRunning}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationUpdatedTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*kinesisanalytics.ApplicationSummary); ok { + if v, ok := outputRaw.(*kinesisanalytics.ApplicationDetail); ok { return v, err } diff --git a/aws/internal/service/kinesisanalyticsv2/finder/finder.go b/aws/internal/service/kinesisanalyticsv2/finder/finder.go index c49650be6283..7e43a1605c8c 100644 --- a/aws/internal/service/kinesisanalyticsv2/finder/finder.go +++ b/aws/internal/service/kinesisanalyticsv2/finder/finder.go @@ -3,18 +3,86 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kinesisanalyticsv2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -// ApplicationByName returns the application corresponding to the specified name. -func ApplicationByName(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { +// ApplicationDetailByName returns the application corresponding to the specified name. +// Returns NotFoundError if no application is found. +func ApplicationDetailByName(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { input := &kinesisanalyticsv2.DescribeApplicationInput{ ApplicationName: aws.String(name), } + return ApplicationDetail(conn, input) +} + +// ApplicationDetail returns the application details corresponding to the specified input. +// Returns NotFoundError if no application is found. +func ApplicationDetail(conn *kinesisanalyticsv2.KinesisAnalyticsV2, input *kinesisanalyticsv2.DescribeApplicationInput) (*kinesisanalyticsv2.ApplicationDetail, error) { output, err := conn.DescribeApplication(input) + + if tfawserr.ErrCodeEquals(err, kinesisanalyticsv2.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } + if output == nil || output.ApplicationDetail == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + return output.ApplicationDetail, nil } + +// SnapshotDetailsByApplicationAndSnapshotNames returns the application snapshot details corresponding to the specified application and snapshot names. +// Returns NotFoundError if no application snapshot is found. +func SnapshotDetailsByApplicationAndSnapshotNames(conn *kinesisanalyticsv2.KinesisAnalyticsV2, applicationName, snapshotName string) (*kinesisanalyticsv2.SnapshotDetails, error) { + input := &kinesisanalyticsv2.DescribeApplicationSnapshotInput{ + ApplicationName: aws.String(applicationName), + SnapshotName: aws.String(snapshotName), + } + + return SnapshotDetails(conn, input) +} + +// SnapshotDetails returns the application snapshot details corresponding to the specified input. +// Returns NotFoundError if no application snapshot is found. +func SnapshotDetails(conn *kinesisanalyticsv2.KinesisAnalyticsV2, input *kinesisanalyticsv2.DescribeApplicationSnapshotInput) (*kinesisanalyticsv2.SnapshotDetails, error) { + output, err := conn.DescribeApplicationSnapshot(input) + + if tfawserr.ErrCodeEquals(err, kinesisanalyticsv2.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if tfawserr.ErrMessageContains(err, kinesisanalyticsv2.ErrCodeInvalidArgumentException, "does not exist") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.SnapshotDetails == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.SnapshotDetails, nil +} diff --git a/aws/internal/service/kinesisanalyticsv2/id.go b/aws/internal/service/kinesisanalyticsv2/id.go new file mode 100644 index 000000000000..dbe2545f27ff --- /dev/null +++ b/aws/internal/service/kinesisanalyticsv2/id.go @@ -0,0 +1,25 @@ +package kinesisanalyticsv2 + +import ( + "fmt" + "strings" +) + +const applicationSnapshotIDSeparator = "/" + +func ApplicationSnapshotCreateID(applicationName, snapshotName string) string { + parts := []string{applicationName, snapshotName} + id := strings.Join(parts, applicationSnapshotIDSeparator) + + return id +} + +func ApplicationSnapshotParseID(id string) (string, string, error) { + parts := strings.Split(id, applicationSnapshotIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%q), expected application-name%ssnapshot-name", id, applicationSnapshotIDSeparator) +} diff --git a/aws/internal/service/kinesisanalyticsv2/waiter/status.go b/aws/internal/service/kinesisanalyticsv2/waiter/status.go index d093d235ed16..108104dec7e8 100644 --- a/aws/internal/service/kinesisanalyticsv2/waiter/status.go +++ b/aws/internal/service/kinesisanalyticsv2/waiter/status.go @@ -3,29 +3,41 @@ package waiter import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kinesisanalyticsv2" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kinesisanalyticsv2/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -const ( - applicationStatusNotFound = "NotFound" - applicationStatusUnknown = "Unknown" -) - -// ApplicationStatus fetches the Application and its Status +// ApplicationStatus fetches the ApplicationDetail and its Status func ApplicationStatus(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - application, err := finder.ApplicationByName(conn, name) + applicationDetail, err := finder.ApplicationDetailByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return applicationDetail, aws.StringValue(applicationDetail.ApplicationStatus), nil + } +} + +// SnapshotDetailsStatus fetches the SnapshotDetails and its Status +func SnapshotDetailsStatus(conn *kinesisanalyticsv2.KinesisAnalyticsV2, applicationName, snapshotName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + snapshotDetails, err := finder.SnapshotDetailsByApplicationAndSnapshotNames(conn, applicationName, snapshotName) - if tfawserr.ErrCodeEquals(err, kinesisanalyticsv2.ErrCodeResourceNotFoundException) { - return nil, applicationStatusNotFound, nil + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return nil, applicationStatusUnknown, err + return nil, "", err } - return application, aws.StringValue(application.ApplicationStatus), nil + return snapshotDetails, aws.StringValue(snapshotDetails.SnapshotStatus), nil } } diff --git a/aws/internal/service/kinesisanalyticsv2/waiter/waiter.go b/aws/internal/service/kinesisanalyticsv2/waiter/waiter.go index 2a50cd605a2b..a0cc0faa1a34 100644 --- a/aws/internal/service/kinesisanalyticsv2/waiter/waiter.go +++ b/aws/internal/service/kinesisanalyticsv2/waiter/waiter.go @@ -10,13 +10,77 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) +const ( + ApplicationDeletedTimeout = 5 * time.Minute + ApplicationStartedTimeout = 5 * time.Minute + ApplicationStoppedTimeout = 5 * time.Minute + ApplicationUpdatedTimeout = 5 * time.Minute + + SnapshotCreatedTimeout = 5 * time.Minute + SnapshotDeletedTimeout = 5 * time.Minute +) + // ApplicationDeleted waits for an Application to return Deleted -func ApplicationDeleted(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string, timeout time.Duration) (*kinesisanalyticsv2.ApplicationDetail, error) { +func ApplicationDeleted(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { stateConf := &resource.StateChangeConf{ Pending: []string{kinesisanalyticsv2.ApplicationStatusDeleting}, Target: []string{}, Refresh: ApplicationStatus(conn, name), - Timeout: timeout, + Timeout: ApplicationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalyticsv2.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationStarted waits for an Application to start +func ApplicationStarted(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalyticsv2.ApplicationStatusStarting}, + Target: []string{kinesisanalyticsv2.ApplicationStatusRunning}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationStartedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalyticsv2.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationStopped waits for an Application to stop +func ApplicationStopped(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalyticsv2.ApplicationStatusForceStopping, kinesisanalyticsv2.ApplicationStatusStopping}, + Target: []string{kinesisanalyticsv2.ApplicationStatusReady}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationStoppedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalyticsv2.ApplicationDetail); ok { + return v, err + } + + return nil, err +} + +// ApplicationUpdated waits for an Application to return Deleted +func ApplicationUpdated(conn *kinesisanalyticsv2.KinesisAnalyticsV2, name string) (*kinesisanalyticsv2.ApplicationDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalyticsv2.ApplicationStatusUpdating}, + Target: []string{kinesisanalyticsv2.ApplicationStatusReady, kinesisanalyticsv2.ApplicationStatusRunning}, + Refresh: ApplicationStatus(conn, name), + Timeout: ApplicationUpdatedTimeout, } outputRaw, err := stateConf.WaitForState() @@ -75,3 +139,39 @@ func IAMPropagation(f func() (interface{}, error)) (interface{}, error) { return output, nil } + +// SnapshotCreated waits for a Snapshot to return Created +func SnapshotCreated(conn *kinesisanalyticsv2.KinesisAnalyticsV2, applicationName, snapshotName string) (*kinesisanalyticsv2.SnapshotDetails, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalyticsv2.SnapshotStatusCreating}, + Target: []string{kinesisanalyticsv2.SnapshotStatusReady}, + Refresh: SnapshotDetailsStatus(conn, applicationName, snapshotName), + Timeout: SnapshotCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalyticsv2.SnapshotDetails); ok { + return v, err + } + + return nil, err +} + +// SnapshotDeleted waits for a Snapshot to return Deleted +func SnapshotDeleted(conn *kinesisanalyticsv2.KinesisAnalyticsV2, applicationName, snapshotName string) (*kinesisanalyticsv2.SnapshotDetails, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kinesisanalyticsv2.SnapshotStatusDeleting}, + Target: []string{}, + Refresh: SnapshotDetailsStatus(conn, applicationName, snapshotName), + Timeout: SnapshotDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*kinesisanalyticsv2.SnapshotDetails); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/kms/arn.go b/aws/internal/service/kms/arn.go new file mode 100644 index 000000000000..d54fd3bb334b --- /dev/null +++ b/aws/internal/service/kms/arn.go @@ -0,0 +1,60 @@ +package kms + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +const ( + ARNSeparator = "/" + ARNService = "kms" +) + +// AliasARNToKeyARN converts an alias ARN to a CMK ARN. +func AliasARNToKeyARN(inputARN, keyID string) (string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + outputARN := arn.ARN{ + Partition: parsedARN.Partition, + Service: parsedARN.Service, + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + Resource: strings.Join([]string{"key", keyID}, ARNSeparator), + }.String() + + return outputARN, nil +} + +// KeyARNOrIDEqual returns whether two CMK ARNs or IDs are equal. +func KeyARNOrIDEqual(arnOrID1, arnOrID2 string) bool { + if arnOrID1 == arnOrID2 { + return true + } + + // Key ARN: arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab + // Key ID: 1234abcd-12ab-34cd-56ef-1234567890ab + arn1, err := arn.Parse(arnOrID1) + firstIsARN := err == nil + arn2, err := arn.Parse(arnOrID2) + secondIsARN := err == nil + + if firstIsARN && !secondIsARN { + return arn1.Resource == "key/"+arnOrID2 + } + + if secondIsARN && !firstIsARN { + return arn2.Resource == "key/"+arnOrID1 + } + + return false +} diff --git a/aws/internal/service/kms/arn_test.go b/aws/internal/service/kms/arn_test.go new file mode 100644 index 000000000000..24766949025b --- /dev/null +++ b/aws/internal/service/kms/arn_test.go @@ -0,0 +1,135 @@ +package kms_test + +import ( + "regexp" + "testing" + + tfkms "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kms" +) + +func TestAliasARNToKeyARN(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedARN string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2:us-west-2:123456789012:alias/test-alias", + ExpectedError: regexp.MustCompile(`expected service kms`), + }, + { + TestName: "valid ARN", + InputARN: "arn:aws:kms:us-west-2:123456789012:alias/test-alias", + ExpectedARN: "arn:aws:kms:us-west-2:123456789012:key/test-key", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfkms.AliasARNToKeyARN(testCase.InputARN, "test-key") + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if got != testCase.ExpectedARN { + t.Errorf("got %s, expected %s", got, testCase.ExpectedARN) + } + }) + } +} + +func TestKeyARNOrIDEqual(t *testing.T) { + testCases := []struct { + name string + first string + second string + want bool + }{ + { + name: "empty", + first: "", + second: "", + want: true, + }, + { + name: "equal IDs", + first: "1234abcd-12ab-34cd-56ef-1234567890ab", + second: "1234abcd-12ab-34cd-56ef-1234567890ab", + want: true, + }, + { + name: "not equal IDs", + first: "1234abcd-12ab-34cd-56ef-1234567890ab", + second: "1234abcd-12ab-34cd-56ef-1234567890ac", + }, + { + name: "equal ARNs", + first: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + second: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + want: true, + }, + { + name: "not equal ARNs", + first: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + second: "arn:aws:kms:us-east-2:111122224444:key/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "equal first ID, second ARN", + first: "1234abcd-12ab-34cd-56ef-1234567890ab", + second: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + want: true, + }, + { + name: "equal first ARN, second ID", + first: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + second: "1234abcd-12ab-34cd-56ef-1234567890ab", + want: true, + }, + { + name: "not equal first ID, second ARN", + first: "1234abcd-12ab-34cd-56ef-1234567890ab", + second: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ac", + }, + { + name: "not equal first ARN, second ID", + first: "arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + second: "1234abcd-12ab-34cd-56ef-1234567890ac", + }, + { + name: "not equal first ID, second incorrect ARN", + first: "1234abcd-12ab-34cd-56ef-1234567890ab", + second: "arn:aws:kms:us-east-2:111122223333:alias/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := tfkms.KeyARNOrIDEqual(testCase.first, testCase.second) + + if got != testCase.want { + t.Errorf("unexpected Equal: %t", got) + } + }) + } +} diff --git a/aws/internal/service/kms/enum.go b/aws/internal/service/kms/enum.go new file mode 100644 index 000000000000..a7e255d21bf5 --- /dev/null +++ b/aws/internal/service/kms/enum.go @@ -0,0 +1,9 @@ +package kms + +const ( + AliasNamePrefix = "alias/" +) + +const ( + PolicyNameDefault = "default" +) diff --git a/aws/internal/service/kms/finder/finder.go b/aws/internal/service/kms/finder/finder.go new file mode 100644 index 000000000000..a07d04594039 --- /dev/null +++ b/aws/internal/service/kms/finder/finder.go @@ -0,0 +1,134 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func AliasByName(conn *kms.KMS, name string) (*kms.AliasListEntry, error) { + input := &kms.ListAliasesInput{} + var output *kms.AliasListEntry + + err := conn.ListAliasesPages(input, func(page *kms.ListAliasesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, alias := range page.Aliases { + if aws.StringValue(alias.AliasName) == name { + output = alias + + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{} + } + + return output, nil +} + +func KeyByID(conn *kms.KMS, id string) (*kms.KeyMetadata, error) { + input := &kms.DescribeKeyInput{ + KeyId: aws.String(id), + } + + output, err := conn.DescribeKey(input) + + if tfawserr.ErrCodeEquals(err, kms.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.KeyMetadata == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + keyMetadata := output.KeyMetadata + + // Once the CMK is in the pending deletion state Terraform considers it logically deleted. + if state := aws.StringValue(keyMetadata.KeyState); state == kms.KeyStatePendingDeletion { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return keyMetadata, nil +} + +func KeyPolicyByKeyIDAndPolicyName(conn *kms.KMS, keyID, policyName string) (*string, error) { + input := &kms.GetKeyPolicyInput{ + KeyId: aws.String(keyID), + PolicyName: aws.String(policyName), + } + + output, err := conn.GetKeyPolicy(input) + + if tfawserr.ErrCodeEquals(err, kms.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Policy, nil +} + +func KeyRotationEnabledByKeyID(conn *kms.KMS, keyID string) (*bool, error) { + input := &kms.GetKeyRotationStatusInput{ + KeyId: aws.String(keyID), + } + + output, err := conn.GetKeyRotationStatus(input) + + if tfawserr.ErrCodeEquals(err, kms.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.KeyRotationEnabled, nil +} diff --git a/aws/internal/service/kms/waiter/status.go b/aws/internal/service/kms/waiter/status.go index 194f73a93476..dbef1ad726d5 100644 --- a/aws/internal/service/kms/waiter/status.go +++ b/aws/internal/service/kms/waiter/status.go @@ -4,25 +4,22 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kms" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kms/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -// KeyState fetches the Key and its State -func KeyState(conn *kms.KMS, keyID string) resource.StateRefreshFunc { +func KeyState(conn *kms.KMS, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &kms.DescribeKeyInput{ - KeyId: aws.String(keyID), - } - - output, err := conn.DescribeKey(input) + output, err := finder.KeyByID(conn, id) - if err != nil { - return nil, kms.KeyStateUnavailable, err + if tfresource.NotFound(err) { + return nil, "", nil } - if output == nil || output.KeyMetadata == nil { - return output, kms.KeyStateUnavailable, nil + if err != nil { + return nil, "", err } - return output, aws.StringValue(output.KeyMetadata.KeyState), nil + return output, aws.StringValue(output.KeyState), nil } } diff --git a/aws/internal/service/kms/waiter/waiter.go b/aws/internal/service/kms/waiter/waiter.go index b1fbc22155c0..ea745a433312 100644 --- a/aws/internal/service/kms/waiter/waiter.go +++ b/aws/internal/service/kms/waiter/waiter.go @@ -3,32 +3,212 @@ package waiter import ( "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kms" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + awspolicy "github.com/jen20/awspolicyequivalence" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + tfkms "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kms" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/kms/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( // Maximum amount of time to wait for KeyState to return PendingDeletion KeyStatePendingDeletionTimeout = 20 * time.Minute + + KeyDeletedTimeout = 20 * time.Minute + KeyDescriptionPropagationTimeout = 5 * time.Minute + KeyMaterialImportedTimeout = 10 * time.Minute + KeyPolicyPropagationTimeout = 5 * time.Minute + KeyRotationUpdatedTimeout = 10 * time.Minute + KeyStatePropagationTimeout = 20 * time.Minute + KeyTagsPropagationTimeout = 5 * time.Minute + KeyValidToPropagationTimeout = 5 * time.Minute + + PropagationTimeout = 2 * time.Minute ) -// KeyStatePendingDeletion waits for KeyState to return PendingDeletion -func KeyStatePendingDeletion(conn *kms.KMS, keyID string) (*kms.DescribeKeyOutput, error) { +// IAMPropagation retries the specified function if the returned error indicates an IAM eventual consistency issue. +// If the retries time out the specified function is called one last time. +func IAMPropagation(f func() (interface{}, error)) (interface{}, error) { + return tfresource.RetryWhenAwsErrCodeEquals(iamwaiter.PropagationTimeout, f, kms.ErrCodeMalformedPolicyDocumentException) +} + +func KeyDeleted(conn *kms.KMS, id string) (*kms.KeyMetadata, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{ - kms.KeyStateDisabled, - kms.KeyStateEnabled, - }, - Target: []string{kms.KeyStatePendingDeletion}, - Refresh: KeyState(conn, keyID), - Timeout: KeyStatePendingDeletionTimeout, + Pending: []string{kms.KeyStateDisabled, kms.KeyStateEnabled}, + Target: []string{}, + Refresh: KeyState(conn, id), + Timeout: KeyDeletedTimeout, } outputRaw, err := stateConf.WaitForState() - if output, ok := outputRaw.(*kms.DescribeKeyOutput); ok { + if output, ok := outputRaw.(*kms.KeyMetadata); ok { return output, err } return nil, err } + +func KeyDescriptionPropagated(conn *kms.KMS, id string, description string) error { + checkFunc := func() (bool, error) { + output, err := finder.KeyByID(conn, id) + + if tfresource.NotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + return aws.StringValue(output.Description) == description, nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 5, + MinTimeout: 2 * time.Second, + } + + return tfresource.WaitUntil(KeyDescriptionPropagationTimeout, checkFunc, opts) +} + +func KeyMaterialImported(conn *kms.KMS, id string) (*kms.KeyMetadata, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{kms.KeyStatePendingImport}, + Target: []string{kms.KeyStateDisabled, kms.KeyStateEnabled}, + Refresh: KeyState(conn, id), + Timeout: KeyMaterialImportedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*kms.KeyMetadata); ok { + return output, err + } + + return nil, err +} + +func KeyPolicyPropagated(conn *kms.KMS, id, policy string) error { + checkFunc := func() (bool, error) { + output, err := finder.KeyPolicyByKeyIDAndPolicyName(conn, id, tfkms.PolicyNameDefault) + + if tfresource.NotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + equivalent, err := awspolicy.PoliciesAreEquivalent(aws.StringValue(output), policy) + + if err != nil { + return false, err + } + + return equivalent, nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 5, + MinTimeout: 1 * time.Second, + } + + return tfresource.WaitUntil(KeyPolicyPropagationTimeout, checkFunc, opts) +} + +func KeyRotationEnabledPropagated(conn *kms.KMS, id string, enabled bool) error { + checkFunc := func() (bool, error) { + output, err := finder.KeyRotationEnabledByKeyID(conn, id) + + if tfresource.NotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + return aws.BoolValue(output) == enabled, nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 5, + MinTimeout: 1 * time.Second, + } + + return tfresource.WaitUntil(KeyRotationUpdatedTimeout, checkFunc, opts) +} + +func KeyStatePropagated(conn *kms.KMS, id string, enabled bool) error { + checkFunc := func() (bool, error) { + output, err := finder.KeyByID(conn, id) + + if tfresource.NotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + return aws.BoolValue(output.Enabled) == enabled, nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 15, + MinTimeout: 2 * time.Second, + } + + return tfresource.WaitUntil(KeyStatePropagationTimeout, checkFunc, opts) +} + +func KeyValidToPropagated(conn *kms.KMS, id string, validTo string) error { + checkFunc := func() (bool, error) { + output, err := finder.KeyByID(conn, id) + + if tfresource.NotFound(err) { + return false, nil + } + + if err != nil { + return false, err + } + + if output.ValidTo != nil { + return aws.TimeValue(output.ValidTo).Format(time.RFC3339) == validTo, nil + } + + return validTo == "", nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 5, + MinTimeout: 2 * time.Second, + } + + return tfresource.WaitUntil(KeyValidToPropagationTimeout, checkFunc, opts) +} + +func TagsPropagated(conn *kms.KMS, id string, tags keyvaluetags.KeyValueTags) error { + checkFunc := func() (bool, error) { + output, err := keyvaluetags.KmsListTags(conn, id) + + if tfawserr.ErrCodeEquals(err, kms.ErrCodeNotFoundException) { + return false, nil + } + + if err != nil { + return false, err + } + + return output.Equal(tags), nil + } + opts := tfresource.WaitOpts{ + ContinuousTargetOccurence: 5, + MinTimeout: 1 * time.Second, + } + + return tfresource.WaitUntil(KeyTagsPropagationTimeout, checkFunc, opts) +} diff --git a/aws/internal/service/lakeformation/enum.go b/aws/internal/service/lakeformation/enum.go new file mode 100644 index 000000000000..d6be8ccf03f7 --- /dev/null +++ b/aws/internal/service/lakeformation/enum.go @@ -0,0 +1,8 @@ +package lakeformation + +const ( + TableNameAllTables = "ALL_TABLES" + TableTypeTable = "Table" + TableTypeTableWithColumns = "TableWithColumns" + IAMAllowedPrincipals = "IAM_ALLOWED_PRINCIPALS" +) diff --git a/aws/internal/service/lakeformation/filter.go b/aws/internal/service/lakeformation/filter.go new file mode 100644 index 000000000000..1d53fb8fd6d5 --- /dev/null +++ b/aws/internal/service/lakeformation/filter.go @@ -0,0 +1,195 @@ +package lakeformation + +import ( + "reflect" + "sort" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lakeformation" +) + +func FilterPermissions(input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // For most Lake Formation resources, filtering within the provider is unnecessary. The input + // contains everything for AWS to give you back exactly what you want. However, many challenges + // arise with tables and tables with columns. Perhaps the two biggest problems (so far) are as + // follows: + // 1. SELECT - when you grant SELECT, it may be part of a list of permissions. However, when + // listing permissions, SELECT comes back in a separate permission. + // 2. Tables with columns. The ListPermissionsInput does not allow you to include a tables with + // columns resource. This means you might get back more permissions than actually pertain to + // the current situation. The table may have separate permissions that also come back. + // + // Thus, for most cases this is just a pass through filter but attempts to clean out + // permissions in the special cases to avoid extra permissions being included. + + if input.Resource.Catalog != nil { + return FilterLakeFormationCatalogPermissions(input.Principal.DataLakePrincipalIdentifier, allPermissions) + } + + if input.Resource.DataLocation != nil { + return FilterLakeFormationDataLocationPermissions(input.Principal.DataLakePrincipalIdentifier, allPermissions) + } + + if input.Resource.Database != nil { + return FilterLakeFormationDatabasePermissions(input.Principal.DataLakePrincipalIdentifier, allPermissions) + } + + if tableType == TableTypeTableWithColumns { + return FilterLakeFormationTableWithColumnsPermissions(input.Principal.DataLakePrincipalIdentifier, input.Resource.Table, columnNames, excludedColumnNames, columnWildcard, allPermissions) + } + + if input.Resource.Table != nil || tableType == TableTypeTable { + return FilterLakeFormationTablePermissions(input.Principal.DataLakePrincipalIdentifier, input.Resource.Table, allPermissions) + } + + return nil +} + +func FilterLakeFormationTablePermissions(principal *string, table *lakeformation.TableResource, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, Name = (Table Name) + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) + // LIST PERMS (out) = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, TableWildcard + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, TableWildcard, Name = ALL_TABLES + // LIST PERMS (out) = SELECT on TableWithColumns, Name = ALL_TABLES, ColumnWildcard + + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if aws.StringValue(principal) != aws.StringValue(perm.Principal.DataLakePrincipalIdentifier) { + continue + } + + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil { + if aws.StringValue(perm.Resource.TableWithColumns.Name) == aws.StringValue(table.Name) || (table.TableWildcard != nil && aws.StringValue(perm.Resource.TableWithColumns.Name) == TableNameAllTables) { + if len(perm.Permissions) > 0 && aws.StringValue(perm.Permissions[0]) == lakeformation.PermissionSelect { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if len(perm.PermissionsWithGrantOption) > 0 && aws.StringValue(perm.PermissionsWithGrantOption[0]) == lakeformation.PermissionSelect { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + } + + if perm.Resource.Table != nil && aws.StringValue(perm.Resource.Table.DatabaseName) == aws.StringValue(table.DatabaseName) { + if aws.StringValue(perm.Resource.Table.Name) == aws.StringValue(table.Name) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if perm.Resource.Table.TableWildcard != nil && table.TableWildcard != nil { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + continue + } + + return cleanPermissions +} + +func FilterLakeFormationTableWithColumnsPermissions(principal *string, twc *lakeformation.TableResource, columnNames []*string, excludedColumnNames []*string, columnWildcard bool, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) + // LIST PERMS (out) = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if aws.StringValue(principal) != aws.StringValue(perm.Principal.DataLakePrincipalIdentifier) { + continue + } + + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnNames != nil { + if StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnNames, columnNames) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil && (columnWildcard || len(excludedColumnNames) > 0) { + if perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames == nil && len(excludedColumnNames) == 0 { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if len(excludedColumnNames) > 0 && StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames, excludedColumnNames) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + if perm.Resource.Table != nil && aws.StringValue(perm.Resource.Table.Name) == aws.StringValue(twc.Name) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + return cleanPermissions +} + +func FilterLakeFormationCatalogPermissions(principal *string, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if aws.StringValue(principal) != aws.StringValue(perm.Principal.DataLakePrincipalIdentifier) { + continue + } + + if perm.Resource.Catalog != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func FilterLakeFormationDataLocationPermissions(principal *string, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if aws.StringValue(principal) != aws.StringValue(perm.Principal.DataLakePrincipalIdentifier) { + continue + } + + if perm.Resource.DataLocation != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func FilterLakeFormationDatabasePermissions(principal *string, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if aws.StringValue(principal) != aws.StringValue(perm.Principal.DataLakePrincipalIdentifier) { + continue + } + + if perm.Resource.Database != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func StringSlicesEqualIgnoreOrder(s1, s2 []*string) bool { + if len(s1) != len(s2) { + return false + } + + v1 := aws.StringValueSlice(s1) + v2 := aws.StringValueSlice(s2) + + sort.Strings(v1) + sort.Strings(v2) + + return reflect.DeepEqual(v1, v2) +} diff --git a/aws/internal/service/lakeformation/filter_test.go b/aws/internal/service/lakeformation/filter_test.go new file mode 100644 index 000000000000..d44b4cf38777 --- /dev/null +++ b/aws/internal/service/lakeformation/filter_test.go @@ -0,0 +1,546 @@ +package lakeformation + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lakeformation" +) + +func TestFilterPermissions(t *testing.T) { + // primitives to make test cases easier + accountID := "481516234248" + dbName := "Hiliji" + altDBName := "Hiuhbum" + tableName := "Ladocmoc" + + principal := &lakeformation.DataLakePrincipal{ + DataLakePrincipalIdentifier: aws.String(fmt.Sprintf("arn:aws-us-gov:iam::%s:role/Zepotiz-Bulgaria", accountID)), + } + + testCases := []struct { + Name string + Input *lakeformation.ListPermissionsInput + TableType string + ColumnNames []*string + ExcludedColumnNames []*string + ColumnWildcard bool + All []*lakeformation.PrincipalResourcePermissions + ExpectedClean []*lakeformation.PrincipalResourcePermissions + }{ + { + Name: "empty", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{}, + }, + All: nil, + ExpectedClean: nil, + }, + { + Name: "emptyWithInput", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: nil, + ExpectedClean: nil, + }, + { + Name: "wrongTableResource", // this may not actually be possible but we account for it + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(altDBName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: nil, + }, + { + Name: "tableResource", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "tableResourceSelectPerm", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + }, + }, + { + Name: "tableResourceSelectPermGrant", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionSelect}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionSelect}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + }, + }, + { + Name: "twcBasic", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnNames: aws.StringSlice([]string{"value"}), + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"value"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"value"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "twcWildcard", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnWildcard: true, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "twcWildcardExcluded", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnWildcard: true, + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{ + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + }, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{ + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + }, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := FilterPermissions(testCase.Input, testCase.TableType, testCase.ColumnNames, testCase.ExcludedColumnNames, testCase.ColumnWildcard, testCase.All) + + if !reflect.DeepEqual(testCase.ExpectedClean, got) { + t.Errorf("got %v, expected %v, input %v", got, testCase.ExpectedClean, testCase.Input) + } + }) + } +} diff --git a/aws/internal/service/lakeformation/waiter/status.go b/aws/internal/service/lakeformation/waiter/status.go new file mode 100644 index 000000000000..80beaa016a8f --- /dev/null +++ b/aws/internal/service/lakeformation/waiter/status.go @@ -0,0 +1,53 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lakeformation" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" +) + +func PermissionsStatus(conn *lakeformation.LakeFormation, input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var permissions []*lakeformation.PrincipalResourcePermissions + + err := conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { + for _, permission := range resp.PrincipalResourcePermissions { + if permission == nil { + continue + } + + if aws.StringValue(input.Principal.DataLakePrincipalIdentifier) != aws.StringValue(permission.Principal.DataLakePrincipalIdentifier) { + continue + } + + permissions = append(permissions, permission) + } + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, lakeformation.ErrCodeEntityNotFoundException) { + return nil, StatusNotFound, err + } + + if tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "Invalid principal") { + return nil, StatusIAMDelay, nil + } + + if err != nil { + return nil, StatusFailed, fmt.Errorf("error listing permissions: %w", err) + } + + // clean permissions = filter out permissions that do not pertain to this specific resource + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, permissions) + + if len(cleanPermissions) == 0 { + return nil, StatusNotFound, nil + } + + return permissions, StatusAvailable, nil + } +} diff --git a/aws/internal/service/lakeformation/waiter/waiter.go b/aws/internal/service/lakeformation/waiter/waiter.go new file mode 100644 index 000000000000..ba7343e01abc --- /dev/null +++ b/aws/internal/service/lakeformation/waiter/waiter.go @@ -0,0 +1,35 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/lakeformation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + PermissionsReadyTimeout = 1 * time.Minute + PermissionsDeleteRetryTimeout = 30 * time.Second + + StatusAvailable = "AVAILABLE" + StatusNotFound = "NOT FOUND" + StatusFailed = "FAILED" + StatusIAMDelay = "IAM DELAY" +) + +func PermissionsReady(conn *lakeformation.LakeFormation, input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool) ([]*lakeformation.PrincipalResourcePermissions, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusIAMDelay}, + Target: []string{StatusAvailable}, + Refresh: PermissionsStatus(conn, input, tableType, columnNames, excludedColumnNames, columnWildcard), + Timeout: PermissionsReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.([]*lakeformation.PrincipalResourcePermissions); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/lambda/finder/finder.go b/aws/internal/service/lambda/finder/finder.go new file mode 100644 index 000000000000..561f2dfa9111 --- /dev/null +++ b/aws/internal/service/lambda/finder/finder.go @@ -0,0 +1,45 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// EventSourceMappingConfigurationByID returns the event source mapping corresponding to the specified ID. +// Returns NotFoundError if no event source mapping is found. +func EventSourceMappingConfigurationByID(conn *lambda.Lambda, uuid string) (*lambda.EventSourceMappingConfiguration, error) { + input := &lambda.GetEventSourceMappingInput{ + UUID: aws.String(uuid), + } + + return EventSourceMappingConfiguration(conn, input) +} + +// EventSourceMappingConfiguration returns the event source mapping corresponding to the specified input. +// Returns NotFoundError if no event source mapping is found. +func EventSourceMappingConfiguration(conn *lambda.Lambda, input *lambda.GetEventSourceMappingInput) (*lambda.EventSourceMappingConfiguration, error) { + output, err := conn.GetEventSourceMapping(input) + + if tfawserr.ErrCodeEquals(err, lambda.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + // Handle any empty result. + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/lambda/waiter/status.go b/aws/internal/service/lambda/waiter/status.go index ed6de2e71b0e..1641d845b441 100644 --- a/aws/internal/service/lambda/waiter/status.go +++ b/aws/internal/service/lambda/waiter/status.go @@ -3,12 +3,14 @@ package waiter import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lambda" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lambda/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( EventSourceMappingStateCreating = "Creating" + EventSourceMappingStateDeleting = "Deleting" EventSourceMappingStateDisabled = "Disabled" EventSourceMappingStateDisabling = "Disabling" EventSourceMappingStateEnabled = "Enabled" @@ -18,13 +20,9 @@ const ( func EventSourceMappingState(conn *lambda.Lambda, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &lambda.GetEventSourceMappingInput{ - UUID: aws.String(id), - } - - output, err := conn.GetEventSourceMapping(input) + eventSourceMappingConfiguration, err := finder.EventSourceMappingConfigurationByID(conn, id) - if tfawserr.ErrCodeEquals(err, lambda.ErrCodeResourceNotFoundException) { + if tfresource.NotFound(err) { return nil, "", nil } @@ -32,10 +30,6 @@ func EventSourceMappingState(conn *lambda.Lambda, id string) resource.StateRefre return nil, "", err } - if output == nil { - return nil, "", nil - } - - return output, aws.StringValue(output.State), nil + return eventSourceMappingConfiguration, aws.StringValue(eventSourceMappingConfiguration.State), nil } } diff --git a/aws/internal/service/lambda/waiter/waiter.go b/aws/internal/service/lambda/waiter/waiter.go index dfc3ee75f3cf..7e470ad083a5 100644 --- a/aws/internal/service/lambda/waiter/waiter.go +++ b/aws/internal/service/lambda/waiter/waiter.go @@ -8,8 +8,16 @@ import ( ) const ( - EventSourceMappingCreateTimeout = 10 * time.Minute - EventSourceMappingUpdateTimeout = 10 * time.Minute + EventSourceMappingCreateTimeout = 10 * time.Minute + EventSourceMappingUpdateTimeout = 10 * time.Minute + EventSourceMappingDeleteTimeout = 5 * time.Minute + LambdaFunctionCreateTimeout = 5 * time.Minute + LambdaFunctionUpdateTimeout = 5 * time.Minute + LambdaFunctionPublishTimeout = 5 * time.Minute + LambdaFunctionPutConcurrencyTimeout = 1 * time.Minute + LambdaFunctionExtraThrottlingTimeout = 9 * time.Minute + + EventSourceMappingPropagationTimeout = 5 * time.Minute ) func EventSourceMappingCreate(conn *lambda.Lambda, id string) (*lambda.EventSourceMappingConfiguration, error) { @@ -36,6 +44,23 @@ func EventSourceMappingCreate(conn *lambda.Lambda, id string) (*lambda.EventSour return nil, err } +func EventSourceMappingDelete(conn *lambda.Lambda, id string) (*lambda.EventSourceMappingConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{EventSourceMappingStateDeleting}, + Target: []string{}, + Refresh: EventSourceMappingState(conn, id), + Timeout: EventSourceMappingDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*lambda.EventSourceMappingConfiguration); ok { + return output, err + } + + return nil, err +} + func EventSourceMappingUpdate(conn *lambda.Lambda, id string) (*lambda.EventSourceMappingConfiguration, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ diff --git a/aws/internal/service/macie2/finder/finder.go b/aws/internal/service/macie2/finder/finder.go new file mode 100644 index 000000000000..f7bf5f556b11 --- /dev/null +++ b/aws/internal/service/macie2/finder/finder.go @@ -0,0 +1,35 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/macie2" +) + +// MemberNotAssociated Return a list of members not associated and compare with account ID +func MemberNotAssociated(conn *macie2.Macie2, accountID string) (*macie2.Member, error) { + input := &macie2.ListMembersInput{ + OnlyAssociated: aws.String("false"), + } + var result *macie2.Member + + err := conn.ListMembersPages(input, func(page *macie2.ListMembersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, member := range page.Members { + if member == nil { + continue + } + + if aws.StringValue(member.AccountId) == accountID { + result = member + return false + } + } + + return !lastPage + }) + + return result, err +} diff --git a/aws/internal/service/macie2/waiter/status.go b/aws/internal/service/macie2/waiter/status.go new file mode 100644 index 000000000000..0641f0931065 --- /dev/null +++ b/aws/internal/service/macie2/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/macie2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/macie2/finder" +) + +// MemberRelationshipStatus fetches the Member and its relationship status +func MemberRelationshipStatus(conn *macie2.Macie2, adminAccountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + adminAccount, err := finder.MemberNotAssociated(conn, adminAccountID) + + if err != nil { + return nil, "Unknown", err + } + + if adminAccount == nil { + return adminAccount, "NotFound", nil + } + + return adminAccount, aws.StringValue(adminAccount.RelationshipStatus), nil + } +} diff --git a/aws/internal/service/macie2/waiter/waiter.go b/aws/internal/service/macie2/waiter/waiter.go new file mode 100644 index 000000000000..7811f6c54dfa --- /dev/null +++ b/aws/internal/service/macie2/waiter/waiter.go @@ -0,0 +1,32 @@ +package waiter + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/macie2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for the MemberRelationshipStatus to be Invited, Enabled, or Paused + MemberInvitedTimeout = 5 * time.Minute +) + +// MemberInvited waits for an AdminAccount to return Invited, Enabled and Paused +func MemberInvited(ctx context.Context, conn *macie2.Macie2, adminAccountID string) (*macie2.Member, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{macie2.RelationshipStatusCreated, macie2.RelationshipStatusEmailVerificationInProgress}, + Target: []string{macie2.RelationshipStatusInvited, macie2.RelationshipStatusEnabled, macie2.RelationshipStatusPaused}, + Refresh: MemberRelationshipStatus(conn, adminAccountID), + Timeout: MemberInvitedTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*macie2.Member); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/mq/waiter/status.go b/aws/internal/service/mq/waiter/status.go new file mode 100644 index 000000000000..a7c34267fe9d --- /dev/null +++ b/aws/internal/service/mq/waiter/status.go @@ -0,0 +1,30 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/mq" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func BrokerStatus(conn *mq.MQ, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := conn.DescribeBroker(&mq.DescribeBrokerInput{ + BrokerId: aws.String(id), + }) + + if tfawserr.ErrCodeEquals(err, mq.ErrCodeNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if output == nil { + return nil, "", nil + } + + return output, aws.StringValue(output.BrokerState), nil + } +} diff --git a/aws/internal/service/mq/waiter/waiter.go b/aws/internal/service/mq/waiter/waiter.go new file mode 100644 index 000000000000..d55ab76c36b5 --- /dev/null +++ b/aws/internal/service/mq/waiter/waiter.go @@ -0,0 +1,72 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/mq" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + BrokerCreateTimeout = 30 * time.Minute + BrokerDeleteTimeout = 30 * time.Minute + BrokerRebootTimeout = 30 * time.Minute +) + +func BrokerCreated(conn *mq.MQ, id string) (*mq.DescribeBrokerResponse, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{ + mq.BrokerStateCreationInProgress, + mq.BrokerStateRebootInProgress, + }, + Target: []string{mq.BrokerStateRunning}, + Timeout: BrokerCreateTimeout, + Refresh: BrokerStatus(conn, id), + } + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*mq.DescribeBrokerResponse); ok { + return output, err + } + + return nil, err +} + +func BrokerDeleted(conn *mq.MQ, id string) (*mq.DescribeBrokerResponse, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{ + mq.BrokerStateCreationFailed, + mq.BrokerStateDeletionInProgress, + mq.BrokerStateRebootInProgress, + mq.BrokerStateRunning, + }, + Target: []string{}, + Timeout: BrokerDeleteTimeout, + Refresh: BrokerStatus(conn, id), + } + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*mq.DescribeBrokerResponse); ok { + return output, err + } + + return nil, err +} + +func BrokerRebooted(conn *mq.MQ, id string) (*mq.DescribeBrokerResponse, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{ + mq.BrokerStateRebootInProgress, + }, + Target: []string{mq.BrokerStateRunning}, + Timeout: BrokerRebootTimeout, + Refresh: BrokerStatus(conn, id), + } + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*mq.DescribeBrokerResponse); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/msk/waiter/waiter.go b/aws/internal/service/msk/waiter/waiter.go new file mode 100644 index 000000000000..55b8c8a69973 --- /dev/null +++ b/aws/internal/service/msk/waiter/waiter.go @@ -0,0 +1,11 @@ +package waiter + +import ( + "time" +) + +const ( + ClusterCreateTimeout = 120 * time.Minute + ClusterUpdateTimeout = 120 * time.Minute + ClusterDeleteTimeout = 120 * time.Minute +) diff --git a/aws/internal/service/mwaa/finder/finder.go b/aws/internal/service/mwaa/finder/finder.go new file mode 100644 index 000000000000..da514fce335b --- /dev/null +++ b/aws/internal/service/mwaa/finder/finder.go @@ -0,0 +1,25 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/mwaa" +) + +// EnvironmentByName returns the MWAA Environment corresponding to the specified Name. +// Returns nil if no environment is found. +func EnvironmentByName(conn *mwaa.MWAA, name string) (*mwaa.Environment, error) { + input := &mwaa.GetEnvironmentInput{ + Name: aws.String(name), + } + + output, err := conn.GetEnvironment(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.Environment, nil +} diff --git a/aws/internal/service/mwaa/waiter/status.go b/aws/internal/service/mwaa/waiter/status.go new file mode 100644 index 000000000000..19d1843a071f --- /dev/null +++ b/aws/internal/service/mwaa/waiter/status.go @@ -0,0 +1,35 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/mwaa" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/mwaa/finder" +) + +const ( + environmentStatusNotFound = "NotFound" + environmentStatusUnknown = "Unknown" +) + +// EnvironmentStatus fetches the Environment and its Status +func EnvironmentStatus(conn *mwaa.MWAA, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + environment, err := finder.EnvironmentByName(conn, name) + + if tfawserr.ErrCodeEquals(err, mwaa.ErrCodeResourceNotFoundException) { + return nil, environmentStatusNotFound, nil + } + + if err != nil { + return nil, environmentStatusUnknown, err + } + + if environment == nil { + return nil, environmentStatusNotFound, nil + } + + return environment, aws.StringValue(environment.Status), nil + } +} diff --git a/aws/internal/service/mwaa/waiter/waiter.go b/aws/internal/service/mwaa/waiter/waiter.go new file mode 100644 index 000000000000..daf8d4fb8fb2 --- /dev/null +++ b/aws/internal/service/mwaa/waiter/waiter.go @@ -0,0 +1,73 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/mwaa" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for an environment creation + EnvironmentCreatedTimeout = 120 * time.Minute + + // Maximum amount of time to wait for an environment update + EnvironmentUpdatedTimeout = 90 * time.Minute + + // Maximum amount of time to wait for an environment deletion + EnvironmentDeletedTimeout = 90 * time.Minute +) + +// EnvironmentCreated waits for a Environment to return AVAILABLE +func EnvironmentCreated(conn *mwaa.MWAA, name string) (*mwaa.Environment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{mwaa.EnvironmentStatusCreating}, + Target: []string{mwaa.EnvironmentStatusAvailable}, + Refresh: EnvironmentStatus(conn, name), + Timeout: EnvironmentCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*mwaa.Environment); ok { + return v, err + } + + return nil, err +} + +// EnvironmentUpdated waits for a Environment to return AVAILABLE +func EnvironmentUpdated(conn *mwaa.MWAA, name string) (*mwaa.Environment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{mwaa.EnvironmentStatusUpdating}, + Target: []string{mwaa.EnvironmentStatusAvailable}, + Refresh: EnvironmentStatus(conn, name), + Timeout: EnvironmentUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*mwaa.Environment); ok { + return v, err + } + + return nil, err +} + +// EnvironmentDeleted waits for a Environment to be deleted +func EnvironmentDeleted(conn *mwaa.MWAA, name string) (*mwaa.Environment, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{mwaa.EnvironmentStatusDeleting}, + Target: []string{}, + Refresh: EnvironmentStatus(conn, name), + Timeout: EnvironmentDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*mwaa.Environment); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/neptune/finder/finder.go b/aws/internal/service/neptune/finder/finder.go new file mode 100644 index 000000000000..d46df9c82385 --- /dev/null +++ b/aws/internal/service/neptune/finder/finder.go @@ -0,0 +1,51 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/neptune" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfneptune "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/neptune" +) + +func EndpointById(conn *neptune.Neptune, id string) (*neptune.DBClusterEndpoint, error) { + clusterId, endpointId, err := tfneptune.ReadAwsNeptuneClusterEndpointId(id) + if err != nil { + return nil, err + } + input := &neptune.DescribeDBClusterEndpointsInput{ + DBClusterIdentifier: aws.String(clusterId), + DBClusterEndpointIdentifier: aws.String(endpointId), + } + + output, err := conn.DescribeDBClusterEndpoints(input) + + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBClusterEndpointNotFoundFault) || + tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBClusterNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + endpoints := output.DBClusterEndpoints + if len(endpoints) == 0 { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return endpoints[0], nil +} diff --git a/aws/internal/service/neptune/id.go b/aws/internal/service/neptune/id.go new file mode 100644 index 000000000000..12be11e52984 --- /dev/null +++ b/aws/internal/service/neptune/id.go @@ -0,0 +1,14 @@ +package glue + +import ( + "fmt" + "strings" +) + +func ReadAwsNeptuneClusterEndpointId(id string) (clusterIdentifier string, endpointIndetifer string, err error) { + idParts := strings.Split(id, ":") + if len(idParts) != 2 { + return "", "", fmt.Errorf("expected ID in format clusterIdentifier:endpointIndetifer, received: %s", id) + } + return idParts[0], idParts[1], nil +} diff --git a/aws/internal/service/neptune/waiter/status.go b/aws/internal/service/neptune/waiter/status.go index 3c833b0a240a..20cb83867c36 100644 --- a/aws/internal/service/neptune/waiter/status.go +++ b/aws/internal/service/neptune/waiter/status.go @@ -4,6 +4,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/neptune/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -12,6 +14,15 @@ const ( // EventSubscription Unknown EventSubscriptionStatusUnknown = "Unknown" + + // Cluster NotFound + ClusterStatusNotFound = "NotFound" + + // Cluster Unknown + ClusterStatusUnknown = "Unknown" + + // DBClusterEndpoint Unknown + DBClusterEndpointStatusUnknown = "Unknown" ) // EventSubscriptionStatus fetches the EventSubscription and its Status @@ -34,3 +45,43 @@ func EventSubscriptionStatus(conn *neptune.Neptune, subscriptionName string) res return output.EventSubscriptionsList[0], aws.StringValue(output.EventSubscriptionsList[0].Status), nil } } + +// ClusterStatus fetches the Cluster and its Status +func ClusterStatus(conn *neptune.Neptune, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &neptune.DescribeDBClustersInput{ + DBClusterIdentifier: aws.String(id), + } + + output, err := conn.DescribeDBClusters(input) + + if err != nil { + return nil, ClusterStatusUnknown, err + } + + if len(output.DBClusters) == 0 { + return nil, ClusterStatusNotFound, nil + } + + cluster := output.DBClusters[0] + + return cluster, aws.StringValue(cluster.Status), nil + } +} + +// DBClusterEndpointStatus fetches the DBClusterEndpoint and its Status +func DBClusterEndpointStatus(conn *neptune.Neptune, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.EndpointById(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, DBClusterEndpointStatusUnknown, err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/neptune/waiter/waiter.go b/aws/internal/service/neptune/waiter/waiter.go index 061e5e080a95..7fd64dac1ce3 100644 --- a/aws/internal/service/neptune/waiter/waiter.go +++ b/aws/internal/service/neptune/waiter/waiter.go @@ -10,9 +10,15 @@ import ( const ( // Maximum amount of time to wait for an EventSubscription to return Deleted EventSubscriptionDeletedTimeout = 10 * time.Minute + + // Maximum amount of time to wait for an DBClusterEndpoint to return Available + DBClusterEndpointAvailableTimeout = 10 * time.Minute + + // Maximum amount of time to wait for an DBClusterEndpoint to return Deleted + DBClusterEndpointDeletedTimeout = 10 * time.Minute ) -// DeploymentDeployed waits for a EventSubscription to return Deleted +// EventSubscriptionDeleted waits for a EventSubscription to return Deleted func EventSubscriptionDeleted(conn *neptune.Neptune, subscriptionName string) (*neptune.EventSubscription, error) { stateConf := &resource.StateChangeConf{ Pending: []string{"deleting"}, @@ -29,3 +35,91 @@ func EventSubscriptionDeleted(conn *neptune.Neptune, subscriptionName string) (* return nil, err } + +// DBClusterDeleted waits for a Cluster to return Deleted +func DBClusterDeleted(conn *neptune.Neptune, id string, timeout time.Duration) (*neptune.DBCluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "available", + "deleting", + "backing-up", + "modifying", + }, + Target: []string{ClusterStatusNotFound}, + Refresh: ClusterStatus(conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*neptune.DBCluster); ok { + return v, err + } + + return nil, err +} + +// DBClusterAvailable waits for a Cluster to return Available +func DBClusterAvailable(conn *neptune.Neptune, id string, timeout time.Duration) (*neptune.DBCluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "creating", + "backing-up", + "modifying", + "preparing-data-migration", + "migrating", + "configuring-iam-database-auth", + }, + Target: []string{"available"}, + Refresh: ClusterStatus(conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*neptune.DBCluster); ok { + return v, err + } + + return nil, err +} + +// DBClusterEndpointAvailable waits for a DBClusterEndpoint to return Available +func DBClusterEndpointAvailable(conn *neptune.Neptune, id string) (*neptune.DBClusterEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating", "modifying"}, + Target: []string{"available"}, + Refresh: DBClusterEndpointStatus(conn, id), + Timeout: DBClusterEndpointAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*neptune.DBClusterEndpoint); ok { + return v, err + } + + return nil, err +} + +// DBClusterEndpointDeleted waits for a DBClusterEndpoint to return Deleted +func DBClusterEndpointDeleted(conn *neptune.Neptune, id string) (*neptune.DBClusterEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting"}, + Target: []string{}, + Refresh: DBClusterEndpointStatus(conn, id), + Timeout: DBClusterEndpointDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*neptune.DBClusterEndpoint); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/organizations/finder/finder.go b/aws/internal/service/organizations/finder/finder.go new file mode 100644 index 000000000000..e9ee347b1898 --- /dev/null +++ b/aws/internal/service/organizations/finder/finder.go @@ -0,0 +1,25 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/service/organizations" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func Organization(conn *organizations.Organizations) (*organizations.Organization, error) { + input := &organizations.DescribeOrganizationInput{} + + output, err := conn.DescribeOrganization(input) + + if err != nil { + return nil, err + } + + if output == nil || output.Organization == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Organization, nil +} diff --git a/aws/internal/service/quicksight/finder/finder.go b/aws/internal/service/quicksight/finder/finder.go new file mode 100644 index 000000000000..0479a278effc --- /dev/null +++ b/aws/internal/service/quicksight/finder/finder.go @@ -0,0 +1,33 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" +) + +func GroupMembership(conn *quicksight.QuickSight, listInput *quicksight.ListGroupMembershipsInput, userName string) (bool, error) { + + found := false + + for { + resp, err := conn.ListGroupMemberships(listInput) + if err != nil { + return false, err + } + + for _, member := range resp.GroupMemberList { + if aws.StringValue(member.MemberName) == userName { + found = true + break + } + } + + if found || resp.NextToken == nil { + break + } + + listInput.NextToken = resp.NextToken + } + + return found, nil +} diff --git a/aws/internal/service/quicksight/waiter/status.go b/aws/internal/service/quicksight/waiter/status.go new file mode 100644 index 000000000000..a03b4575a6e2 --- /dev/null +++ b/aws/internal/service/quicksight/waiter/status.go @@ -0,0 +1,31 @@ +package waiter + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// DataSourceStatus fetches the DataSource and its Status +func DataSourceStatus(ctx context.Context, conn *quicksight.QuickSight, accountId, datasourceId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &quicksight.DescribeDataSourceInput{ + AwsAccountId: aws.String(accountId), + DataSourceId: aws.String(datasourceId), + } + + output, err := conn.DescribeDataSourceWithContext(ctx, input) + + if err != nil { + return nil, "", err + } + + if output == nil || output.DataSource == nil { + return nil, "", nil + } + + return output.DataSource, aws.StringValue(output.DataSource.Status), nil + } +} diff --git a/aws/internal/service/quicksight/waiter/waiter.go b/aws/internal/service/quicksight/waiter/waiter.go new file mode 100644 index 000000000000..a5085c7bd40c --- /dev/null +++ b/aws/internal/service/quicksight/waiter/waiter.go @@ -0,0 +1,61 @@ +package waiter + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + DataSourceCreateTimeout = 5 * time.Minute + DataSourceUpdateTimeout = 5 * time.Minute +) + +// DataSourceCreated waits for a DataSource to return CREATION_SUCCESSFUL +func DataSourceCreated(ctx context.Context, conn *quicksight.QuickSight, accountId, dataSourceId string) (*quicksight.DataSource, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{quicksight.ResourceStatusCreationInProgress}, + Target: []string{quicksight.ResourceStatusCreationSuccessful}, + Refresh: DataSourceStatus(ctx, conn, accountId, dataSourceId), + Timeout: DataSourceCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*quicksight.DataSource); ok { + if status, errorInfo := aws.StringValue(output.Status), output.ErrorInfo; status == quicksight.ResourceStatusCreationFailed && errorInfo != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(errorInfo.Type), aws.StringValue(errorInfo.Message))) + } + + return output, err + } + + return nil, err +} + +// DataSourceUpdated waits for a DataSource to return UPDATE_SUCCESSFUL +func DataSourceUpdated(ctx context.Context, conn *quicksight.QuickSight, accountId, dataSourceId string) (*quicksight.DataSource, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{quicksight.ResourceStatusUpdateInProgress}, + Target: []string{quicksight.ResourceStatusUpdateSuccessful}, + Refresh: DataSourceStatus(ctx, conn, accountId, dataSourceId), + Timeout: DataSourceUpdateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*quicksight.DataSource); ok { + if status, errorInfo := aws.StringValue(output.Status), output.ErrorInfo; status == quicksight.ResourceStatusUpdateFailed && errorInfo != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(errorInfo.Type), aws.StringValue(errorInfo.Message))) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/ram/finder/finder.go b/aws/internal/service/ram/finder/finder.go new file mode 100644 index 000000000000..fdf820606a4e --- /dev/null +++ b/aws/internal/service/ram/finder/finder.go @@ -0,0 +1,211 @@ +package finder + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + FindInvitationTimeout = 2 * time.Minute + FindResourceShareTimeout = 1 * time.Minute +) + +// ResourceShareOwnerOtherAccountsByArn returns the resource share owned by other accounts corresponding to the specified ARN. +// Returns nil if no configuration is found. +func ResourceShareOwnerOtherAccountsByArn(conn *ram.RAM, arn string) (*ram.ResourceShare, error) { + listResourceSharesInput := &ram.GetResourceSharesInput{ + ResourceOwner: aws.String(ram.ResourceOwnerOtherAccounts), + ResourceShareArns: aws.StringSlice([]string{arn}), + } + + return resourceShare(conn, listResourceSharesInput) +} + +// ResourceShareOwnerSelfByArn returns the resource share owned by own account corresponding to the specified ARN. +// Returns nil if no configuration is found. +func ResourceShareOwnerSelfByArn(conn *ram.RAM, arn string) (*ram.ResourceShare, error) { + listResourceSharesInput := &ram.GetResourceSharesInput{ + ResourceOwner: aws.String(ram.ResourceOwnerSelf), + ResourceShareArns: aws.StringSlice([]string{arn}), + } + + return resourceShare(conn, listResourceSharesInput) +} + +// ResourceShareInvitationByResourceShareArnAndStatus returns the resource share invitation corresponding to the specified resource share ARN. +// Returns nil if no configuration is found. +func ResourceShareInvitationByResourceShareArnAndStatus(conn *ram.RAM, resourceShareArn, status string) (*ram.ResourceShareInvitation, error) { + var invitation *ram.ResourceShareInvitation + + // Retry for Ram resource share invitation eventual consistency + err := resource.Retry(FindInvitationTimeout, func() *resource.RetryError { + i, err := resourceShareInvitationByResourceShareArnAndStatus(conn, resourceShareArn, status) + invitation = i + + if err != nil { + return resource.NonRetryableError(err) + } + + if invitation == nil { + return resource.RetryableError(&resource.NotFoundError{}) + } + + return nil + }) + + if tfresource.TimedOut(err) { + invitation, err = resourceShareInvitationByResourceShareArnAndStatus(conn, resourceShareArn, status) + } + + if invitation == nil { + return nil, nil + } + + if err != nil { + return nil, err + } + + return invitation, nil +} + +// ResourceShareInvitationByArn returns the resource share invitation corresponding to the specified ARN. +// Returns nil if no configuration is found. +func ResourceShareInvitationByArn(conn *ram.RAM, arn string) (*ram.ResourceShareInvitation, error) { + var invitation *ram.ResourceShareInvitation + + // Retry for Ram resource share invitation eventual consistency + err := resource.Retry(FindInvitationTimeout, func() *resource.RetryError { + i, err := resourceShareInvitationByArn(conn, arn) + invitation = i + + if err != nil { + return resource.NonRetryableError(err) + } + + if invitation == nil { + resource.RetryableError(&resource.NotFoundError{}) + } + + return nil + }) + + if tfresource.TimedOut(err) { + invitation, err = resourceShareInvitationByArn(conn, arn) + } + + if invitation == nil { + return nil, nil + } + + if err != nil { + return nil, err + } + + return invitation, nil +} + +func resourceShare(conn *ram.RAM, input *ram.GetResourceSharesInput) (*ram.ResourceShare, error) { + var shares *ram.GetResourceSharesOutput + + // Retry for Ram resource share eventual consistency + err := resource.Retry(FindResourceShareTimeout, func() *resource.RetryError { + ss, err := conn.GetResourceShares(input) + shares = ss + + if tfawserr.ErrCodeEquals(err, ram.ErrCodeUnknownResourceException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + if len(shares.ResourceShares) == 0 { + return resource.RetryableError(&resource.NotFoundError{}) + } + + return nil + }) + + if tfresource.TimedOut(err) { + shares, err = conn.GetResourceShares(input) + } + + if err != nil { + return nil, err + } + + if shares == nil || len(shares.ResourceShares) == 0 { + return nil, nil + } + + return shares.ResourceShares[0], nil +} + +func resourceShareInvitationByResourceShareArnAndStatus(conn *ram.RAM, resourceShareArn, status string) (*ram.ResourceShareInvitation, error) { + var invitation *ram.ResourceShareInvitation + + input := &ram.GetResourceShareInvitationsInput{ + ResourceShareArns: []*string{aws.String(resourceShareArn)}, + } + + err := conn.GetResourceShareInvitationsPages(input, func(page *ram.GetResourceShareInvitationsOutput, lastPage bool) bool { + for _, rsi := range page.ResourceShareInvitations { + if aws.StringValue(rsi.Status) == status { + invitation = rsi + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + return invitation, nil +} + +func resourceShareInvitationByArn(conn *ram.RAM, arn string) (*ram.ResourceShareInvitation, error) { + input := &ram.GetResourceShareInvitationsInput{ + ResourceShareInvitationArns: []*string{aws.String(arn)}, + } + + output, err := conn.GetResourceShareInvitations(input) + + if err != nil { + return nil, err + } + + if len(output.ResourceShareInvitations) == 0 { + return nil, nil + } + + return output.ResourceShareInvitations[0], nil +} + +func ResourceSharePrincipalAssociationByShareARNPrincipal(conn *ram.RAM, resourceShareARN, principal string) (*ram.ResourceShareAssociation, error) { + input := &ram.GetResourceShareAssociationsInput{ + AssociationType: aws.String(ram.ResourceShareAssociationTypePrincipal), + Principal: aws.String(principal), + ResourceShareArns: aws.StringSlice([]string{resourceShareARN}), + } + + output, err := conn.GetResourceShareAssociations(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.ResourceShareAssociations) == 0 || output.ResourceShareAssociations[0] == nil { + return nil, nil + } + + return output.ResourceShareAssociations[0], nil +} diff --git a/aws/internal/service/ram/waiter/status.go b/aws/internal/service/ram/waiter/status.go new file mode 100644 index 000000000000..ee586f500c77 --- /dev/null +++ b/aws/internal/service/ram/waiter/status.go @@ -0,0 +1,84 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ram" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ram/finder" +) + +const ( + ResourceShareInvitationStatusNotFound = "NotFound" + ResourceShareInvitationStatusUnknown = "Unknown" + + ResourceShareStatusNotFound = "NotFound" + ResourceShareStatusUnknown = "Unknown" + + PrincipalAssociationStatusNotFound = "NotFound" +) + +// ResourceShareInvitationStatus fetches the ResourceShareInvitation and its Status +func ResourceShareInvitationStatus(conn *ram.RAM, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + invitation, err := finder.ResourceShareInvitationByArn(conn, arn) + + if err != nil { + return nil, ResourceShareInvitationStatusUnknown, err + } + + if invitation == nil { + return nil, ResourceShareInvitationStatusNotFound, nil + } + + return invitation, aws.StringValue(invitation.Status), nil + } +} + +// ResourceShareOwnerSelfStatus fetches the ResourceShare and its Status +func ResourceShareOwnerSelfStatus(conn *ram.RAM, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + share, err := finder.ResourceShareOwnerSelfByArn(conn, arn) + + if tfawserr.ErrCodeEquals(err, ram.ErrCodeUnknownResourceException) { + return nil, ResourceShareStatusNotFound, nil + } + + if err != nil { + return nil, ResourceShareStatusUnknown, err + } + + if share == nil { + return nil, ResourceShareStatusNotFound, nil + } + + return share, aws.StringValue(share.Status), nil + } +} + +func ResourceSharePrincipalAssociationStatus(conn *ram.RAM, resourceShareArn, principal string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + association, err := finder.ResourceSharePrincipalAssociationByShareARNPrincipal(conn, resourceShareArn, principal) + + if tfawserr.ErrCodeEquals(err, ram.ErrCodeUnknownResourceException) { + return nil, PrincipalAssociationStatusNotFound, err + } + + if err != nil { + return nil, ram.ResourceShareAssociationStatusFailed, err + } + + if association == nil { + return nil, ram.ResourceShareAssociationStatusDisassociated, nil + } + + if aws.StringValue(association.Status) == ram.ResourceShareAssociationStatusFailed { + extendedErr := fmt.Errorf("association status message: %s", aws.StringValue(association.StatusMessage)) + return association, aws.StringValue(association.Status), extendedErr + } + + return association, aws.StringValue(association.Status), nil + } +} diff --git a/aws/internal/service/ram/waiter/waiter.go b/aws/internal/service/ram/waiter/waiter.go new file mode 100644 index 000000000000..3a49493728f3 --- /dev/null +++ b/aws/internal/service/ram/waiter/waiter.go @@ -0,0 +1,119 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/ram" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + PrincipalAssociationTimeout = 3 * time.Minute + PrincipalDisassociationTimeout = 3 * time.Minute +) + +// ResourceShareInvitationAccepted waits for a ResourceShareInvitation to return ACCEPTED +func ResourceShareInvitationAccepted(conn *ram.RAM, arn string, timeout time.Duration) (*ram.ResourceShareInvitation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareInvitationStatusPending}, + Target: []string{ram.ResourceShareInvitationStatusAccepted}, + Refresh: ResourceShareInvitationStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShareInvitation); ok { + return v, err + } + + return nil, err +} + +// ResourceShareOwnedBySelfDisassociated waits for a ResourceShare owned by own account to be disassociated +func ResourceShareOwnedBySelfDisassociated(conn *ram.RAM, arn string, timeout time.Duration) (*ram.ResourceShare, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareAssociationStatusAssociated}, + Target: []string{}, + Refresh: ResourceShareOwnerSelfStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShare); ok { + return v, err + } + + return nil, err +} + +// ResourceShareOwnedBySelfActive waits for a ResourceShare owned by own account to return ACTIVE +func ResourceShareOwnedBySelfActive(conn *ram.RAM, arn string, timeout time.Duration) (*ram.ResourceShare, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareStatusPending}, + Target: []string{ram.ResourceShareStatusActive}, + Refresh: ResourceShareOwnerSelfStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShare); ok { + return v, err + } + + return nil, err +} + +// ResourceShareOwnedBySelfDeleted waits for a ResourceShare owned by own account to return DELETED +func ResourceShareOwnedBySelfDeleted(conn *ram.RAM, arn string, timeout time.Duration) (*ram.ResourceShare, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareStatusDeleting}, + Target: []string{ram.ResourceShareStatusDeleted}, + Refresh: ResourceShareOwnerSelfStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShare); ok { + return v, err + } + + return nil, err +} + +func ResourceSharePrincipalAssociated(conn *ram.RAM, resourceShareARN, principal string) (*ram.ResourceShareAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareAssociationStatusAssociating, PrincipalAssociationStatusNotFound}, + Target: []string{ram.ResourceShareAssociationStatusAssociated}, + Refresh: ResourceSharePrincipalAssociationStatus(conn, resourceShareARN, principal), + Timeout: PrincipalAssociationTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShareAssociation); ok { + return v, err + } + + return nil, err +} + +func ResourceSharePrincipalDisassociated(conn *ram.RAM, resourceShareARN, principal string) (*ram.ResourceShareAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ram.ResourceShareAssociationStatusAssociated, ram.ResourceShareAssociationStatusDisassociating}, + Target: []string{ram.ResourceShareAssociationStatusDisassociated, PrincipalAssociationStatusNotFound}, + Refresh: ResourceSharePrincipalAssociationStatus(conn, resourceShareARN, principal), + Timeout: PrincipalDisassociationTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ram.ResourceShareAssociation); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/rds/enum.go b/aws/internal/service/rds/enum.go new file mode 100644 index 000000000000..4ce89944e26a --- /dev/null +++ b/aws/internal/service/rds/enum.go @@ -0,0 +1,14 @@ +package rds + +const ( + DBClusterRoleStatusActive = "ACTIVE" + DBClusterRoleStatusDeleted = "DELETED" + DBClusterRoleStatusPending = "PENDING" +) + +const ( + EventSubscriptionStatusActive = "active" + EventSubscriptionStatusCreating = "creating" + EventSubscriptionStatusDeleting = "deleting" + EventSubscriptionStatusModifying = "modifying" +) diff --git a/aws/internal/service/rds/finder/finder.go b/aws/internal/service/rds/finder/finder.go index a05a70c61d58..975a6e323bba 100644 --- a/aws/internal/service/rds/finder/finder.go +++ b/aws/internal/service/rds/finder/finder.go @@ -3,10 +3,14 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // DBProxyTarget returns matching DBProxyTarget. -func DBProxyTarget(conn *rds.RDS, dbProxyName string, targetGroupName string, targetType string, rdsResourceId string) (*rds.DBProxyTarget, error) { +func DBProxyTarget(conn *rds.RDS, dbProxyName, targetGroupName, targetType, rdsResourceId string) (*rds.DBProxyTarget, error) { input := &rds.DescribeDBProxyTargetsInput{ DBProxyName: aws.String(dbProxyName), TargetGroupName: aws.String(targetGroupName), @@ -30,3 +34,129 @@ func DBProxyTarget(conn *rds.RDS, dbProxyName string, targetGroupName string, ta return dbProxyTarget, err } + +// DBProxyEndpoint returns matching DBProxyEndpoint. +func DBProxyEndpoint(conn *rds.RDS, id string) (*rds.DBProxyEndpoint, error) { + dbProxyName, dbProxyEndpointName, err := tfrds.ResourceAwsDbProxyEndpointParseID(id) + if err != nil { + return nil, err + } + + input := &rds.DescribeDBProxyEndpointsInput{ + DBProxyName: aws.String(dbProxyName), + DBProxyEndpointName: aws.String(dbProxyEndpointName), + } + var dbProxyEndpoint *rds.DBProxyEndpoint + + err = conn.DescribeDBProxyEndpointsPages(input, func(page *rds.DescribeDBProxyEndpointsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, endpoint := range page.DBProxyEndpoints { + if aws.StringValue(endpoint.DBProxyEndpointName) == dbProxyEndpointName && + aws.StringValue(endpoint.DBProxyName) == dbProxyName { + dbProxyEndpoint = endpoint + return false + } + } + + return !lastPage + }) + + return dbProxyEndpoint, err +} + +func DBClusterRoleByDBClusterIDAndRoleARN(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { + dbCluster, err := DBClusterByID(conn, dbClusterID) + + if err != nil { + return nil, err + } + + for _, associatedRole := range dbCluster.AssociatedRoles { + if aws.StringValue(associatedRole.RoleArn) == roleARN { + if status := aws.StringValue(associatedRole.Status); status == tfrds.DBClusterRoleStatusDeleted { + return nil, &resource.NotFoundError{ + Message: status, + } + } + + return associatedRole, nil + } + } + + return nil, &resource.NotFoundError{} +} + +func DBClusterByID(conn *rds.RDS, id string) (*rds.DBCluster, error) { + input := &rds.DescribeDBClustersInput{ + DBClusterIdentifier: aws.String(id), + } + + output, err := conn.DescribeDBClusters(input) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || len(output.DBClusters) == 0 || output.DBClusters[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + dbCluster := output.DBClusters[0] + + // Eventual consistency check. + if aws.StringValue(dbCluster.DBClusterIdentifier) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return dbCluster, nil +} + +func DBProxyByName(conn *rds.RDS, name string) (*rds.DBProxy, error) { + input := &rds.DescribeDBProxiesInput{ + DBProxyName: aws.String(name), + } + + output, err := conn.DescribeDBProxies(input) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBProxyNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || len(output.DBProxies) == 0 || output.DBProxies[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.DBProxies[0], nil +} + +func EventSubscriptionByID(conn *rds.RDS, id string) (*rds.EventSubscription, error) { + input := &rds.DescribeEventSubscriptionsInput{ + SubscriptionName: aws.String(id), + } + + output, err := conn.DescribeEventSubscriptions(input) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeSubscriptionNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || len(output.EventSubscriptionsList) == 0 || output.EventSubscriptionsList[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.EventSubscriptionsList[0], nil +} diff --git a/aws/internal/service/rds/id.go b/aws/internal/service/rds/id.go new file mode 100644 index 000000000000..74e74c85784b --- /dev/null +++ b/aws/internal/service/rds/id.go @@ -0,0 +1,33 @@ +package rds + +import ( + "fmt" + "strings" +) + +func ResourceAwsDbProxyEndpointParseID(id string) (string, string, error) { + idParts := strings.SplitN(id, "/", 2) + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected db_proxy_name/db_proxy_endpoint_name", id) + } + return idParts[0], idParts[1], nil +} + +const clusterRoleAssociationResourceIDSeparator = "," + +func ClusterRoleAssociationCreateResourceID(dbClusterID, roleARN string) string { + parts := []string{dbClusterID, roleARN} + id := strings.Join(parts, clusterRoleAssociationResourceIDSeparator) + + return id +} + +func ClusterRoleAssociationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, clusterRoleAssociationResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected DBCLUSTERID%[2]sROLEARN", id, clusterRoleAssociationResourceIDSeparator) +} diff --git a/aws/internal/service/rds/waiter/status.go b/aws/internal/service/rds/waiter/status.go index 59039e5a825c..fd85185e0fa1 100644 --- a/aws/internal/service/rds/waiter/status.go +++ b/aws/internal/service/rds/waiter/status.go @@ -4,33 +4,63 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - // EventSubscription NotFound - EventSubscriptionStatusNotFound = "NotFound" + // ProxyEndpoint NotFound + ProxyEndpointStatusNotFound = "NotFound" - // EventSubscription Unknown - EventSubscriptionStatusUnknown = "Unknown" + // ProxyEndpoint Unknown + ProxyEndpointStatusUnknown = "Unknown" ) -// EventSubscriptionStatus fetches the EventSubscription and its Status -func EventSubscriptionStatus(conn *rds.RDS, subscriptionName string) resource.StateRefreshFunc { +func EventSubscriptionStatus(conn *rds.RDS, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &rds.DescribeEventSubscriptionsInput{ - SubscriptionName: aws.String(subscriptionName), + output, err := finder.EventSubscriptionByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil } - output, err := conn.DescribeEventSubscriptions(input) + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +// DBProxyEndpointStatus fetches the ProxyEndpoint and its Status +func DBProxyEndpointStatus(conn *rds.RDS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DBProxyEndpoint(conn, id) if err != nil { - return nil, EventSubscriptionStatusUnknown, err + return nil, ProxyEndpointStatusUnknown, err + } + + if output == nil { + return nil, ProxyEndpointStatusNotFound, nil } - if len(output.EventSubscriptionsList) == 0 { - return nil, EventSubscriptionStatusNotFound, nil + return output, aws.StringValue(output.Status), nil + } +} + +func DBClusterRoleStatus(conn *rds.RDS, dbClusterID, roleARN string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err } - return output.EventSubscriptionsList[0], aws.StringValue(output.EventSubscriptionsList[0].Status), nil + return output, aws.StringValue(output.Status), nil } } diff --git a/aws/internal/service/rds/waiter/waiter.go b/aws/internal/service/rds/waiter/waiter.go index aa18ccb20908..aeed1a093688 100644 --- a/aws/internal/service/rds/waiter/waiter.go +++ b/aws/internal/service/rds/waiter/waiter.go @@ -5,26 +5,141 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" ) const ( - // Maximum amount of time to wait for an EventSubscription to return Deleted - EventSubscriptionDeletedTimeout = 10 * time.Minute + RdsClusterInitiateUpgradeTimeout = 5 * time.Minute + + DBClusterRoleAssociationCreatedTimeout = 5 * time.Minute + DBClusterRoleAssociationDeletedTimeout = 5 * time.Minute ) -// DeploymentDeployed waits for a EventSubscription to return Deleted -func EventSubscriptionDeleted(conn *rds.RDS, subscriptionName string) (*rds.EventSubscription, error) { +func EventSubscriptionCreated(conn *rds.RDS, id string, timeout time.Duration) (*rds.EventSubscription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.EventSubscriptionStatusCreating}, + Target: []string{tfrds.EventSubscriptionStatusActive}, + Refresh: EventSubscriptionStatus(conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.EventSubscription); ok { + return output, err + } + + return nil, err +} + +func EventSubscriptionDeleted(conn *rds.RDS, id string, timeout time.Duration) (*rds.EventSubscription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.EventSubscriptionStatusDeleting}, + Target: []string{}, + Refresh: EventSubscriptionStatus(conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.EventSubscription); ok { + return output, err + } + + return nil, err +} + +func EventSubscriptionUpdated(conn *rds.RDS, id string, timeout time.Duration) (*rds.EventSubscription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.EventSubscriptionStatusModifying}, + Target: []string{tfrds.EventSubscriptionStatusActive}, + Refresh: EventSubscriptionStatus(conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.EventSubscription); ok { + return output, err + } + + return nil, err +} + +// DBProxyEndpointAvailable waits for a DBProxyEndpoint to return Available +func DBProxyEndpointAvailable(conn *rds.RDS, id string, timeout time.Duration) (*rds.DBProxyEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + rds.DBProxyEndpointStatusCreating, + rds.DBProxyEndpointStatusModifying, + }, + Target: []string{rds.DBProxyEndpointStatusAvailable}, + Refresh: DBProxyEndpointStatus(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBProxyEndpoint); ok { + return output, err + } + + return nil, err +} + +// DBProxyEndpointDeleted waits for a DBProxyEndpoint to return Deleted +func DBProxyEndpointDeleted(conn *rds.RDS, id string, timeout time.Duration) (*rds.DBProxyEndpoint, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{rds.DBProxyEndpointStatusDeleting}, + Target: []string{}, + Refresh: DBProxyEndpointStatus(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBProxyEndpoint); ok { + return output, err + } + + return nil, err +} + +func DBClusterRoleAssociationCreated(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.DBClusterRoleStatusPending}, + Target: []string{tfrds.DBClusterRoleStatusActive}, + Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN), + Timeout: DBClusterRoleAssociationCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBClusterRole); ok { + return output, err + } + + return nil, err +} + +func DBClusterRoleAssociationDeleted(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{"deleting"}, - Target: []string{EventSubscriptionStatusNotFound}, - Refresh: EventSubscriptionStatus(conn, subscriptionName), - Timeout: EventSubscriptionDeletedTimeout, + Pending: []string{tfrds.DBClusterRoleStatusActive, tfrds.DBClusterRoleStatusPending}, + Target: []string{}, + Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN), + Timeout: DBClusterRoleAssociationDeletedTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*rds.EventSubscription); ok { - return v, err + if output, ok := outputRaw.(*rds.DBClusterRole); ok { + return output, err } return nil, err diff --git a/aws/internal/service/redshift/enum.go b/aws/internal/service/redshift/enum.go new file mode 100644 index 000000000000..8e518dcf27a9 --- /dev/null +++ b/aws/internal/service/redshift/enum.go @@ -0,0 +1,37 @@ +package redshift + +// https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#rs-mgmt-cluster-status. +const ( + ClusterStatusAvailable = "available" + ClusterStatusAvailablePrepForResize = "available, prep-for-resize" + ClusterStatusAvailableResizeCleanup = "available, resize-cleanup" + ClusterStatusCancellingResize = "cancelling-resize" + ClusterStatusCreating = "creating" + ClusterStatusDeleting = "deleting" + ClusterStatusFinalSnapshot = "final-snapshot" + ClusterStatusHardwareFailure = "hardware-failure" + ClusterStatusIncompatibleHsm = "incompatible-hsm" + ClusterStatusIncompatibleNetwork = "incompatible-network" + ClusterStatusIncompatibleParameters = "incompatible-parameters" + ClusterStatusIncompatibleRestore = "incompatible-restore" + ClusterStatusModifying = "modifying" + ClusterStatusPaused = "paused" + ClusterStatusRebooting = "rebooting" + ClusterStatusRenaming = "renaming" + ClusterStatusResizing = "resizing" + ClusterStatusRotatingKeys = "rotating-keys" + ClusterStatusStorageFull = "storage-full" + ClusterStatusUpdatingHsm = "updating-hsm" +) + +const ( + ClusterTypeMultiNode = "multi-node" + ClusterTypeSingleNode = "single-node" +) + +func ClusterType_Values() []string { + return []string{ + ClusterTypeMultiNode, + ClusterTypeSingleNode, + } +} diff --git a/aws/internal/service/redshift/errors.go b/aws/internal/service/redshift/errors.go new file mode 100644 index 000000000000..53a43bd1d801 --- /dev/null +++ b/aws/internal/service/redshift/errors.go @@ -0,0 +1,5 @@ +package redshift + +const ( + ErrCodeInvalidParameterValue = "InvalidParameterValue" +) diff --git a/aws/internal/service/redshift/finder/finder.go b/aws/internal/service/redshift/finder/finder.go new file mode 100644 index 000000000000..2688b090b88b --- /dev/null +++ b/aws/internal/service/redshift/finder/finder.go @@ -0,0 +1,67 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ClusterByID(conn *redshift.Redshift, id string) (*redshift.Cluster, error) { + input := &redshift.DescribeClustersInput{ + ClusterIdentifier: aws.String(id), + } + + output, err := conn.DescribeClusters(input) + + if tfawserr.ErrCodeEquals(err, redshift.ErrCodeClusterNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Clusters) == 0 || output.Clusters[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Clusters); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.Clusters[0], nil +} + +func ScheduledActionByName(conn *redshift.Redshift, name string) (*redshift.ScheduledAction, error) { + input := &redshift.DescribeScheduledActionsInput{ + ScheduledActionName: aws.String(name), + } + + output, err := conn.DescribeScheduledActions(input) + + if tfawserr.ErrCodeEquals(err, redshift.ErrCodeScheduledActionNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.ScheduledActions) == 0 || output.ScheduledActions[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.ScheduledActions); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.ScheduledActions[0], nil +} diff --git a/aws/internal/service/redshift/waiter/status.go b/aws/internal/service/redshift/waiter/status.go new file mode 100644 index 000000000000..5c2dc0bd96d7 --- /dev/null +++ b/aws/internal/service/redshift/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ClusterStatus(conn *redshift.Redshift, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ClusterStatus), nil + } +} diff --git a/aws/internal/service/redshift/waiter/waiter.go b/aws/internal/service/redshift/waiter/waiter.go new file mode 100644 index 000000000000..6cd56dbc9bf7 --- /dev/null +++ b/aws/internal/service/redshift/waiter/waiter.go @@ -0,0 +1,38 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfredshift "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift" +) + +const ( + ClusterInvalidClusterStateFaultTimeout = 15 * time.Minute +) + +func ClusterDeleted(conn *redshift.Redshift, id string, timeout time.Duration) (*redshift.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + tfredshift.ClusterStatusAvailable, + tfredshift.ClusterStatusCreating, + tfredshift.ClusterStatusDeleting, + tfredshift.ClusterStatusFinalSnapshot, + tfredshift.ClusterStatusRebooting, + tfredshift.ClusterStatusRenaming, + tfredshift.ClusterStatusResizing, + }, + Target: []string{}, + Refresh: ClusterStatus(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*redshift.Cluster); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/route53/enum.go b/aws/internal/service/route53/enum.go index d10f69e91891..bb8204082e5f 100644 --- a/aws/internal/service/route53/enum.go +++ b/aws/internal/service/route53/enum.go @@ -6,4 +6,10 @@ const ( KeySigningKeyStatusDeleting = "DELETING" KeySigningKeyStatusInactive = "INACTIVE" KeySigningKeyStatusInternalFailure = "INTERNAL_FAILURE" + + ServeSignatureActionNeeded = "ACTION_NEEDED" + ServeSignatureDeleting = "DELETING" + ServeSignatureInternalFailure = "INTERNAL_FAILURE" + ServeSignatureNotSigning = "NOT_SIGNING" + ServeSignatureSigning = "SIGNING" ) diff --git a/aws/internal/service/route53/finder/finder.go b/aws/internal/service/route53/finder/finder.go index de40116d4e8f..bbd8b9e43db0 100644 --- a/aws/internal/service/route53/finder/finder.go +++ b/aws/internal/service/route53/finder/finder.go @@ -5,9 +5,47 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfroute53 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) +func HealthCheckByID(conn *route53.Route53, id string) (*route53.HealthCheck, error) { + input := &route53.GetHealthCheckInput{ + HealthCheckId: aws.String(id), + } + + output, err := conn.GetHealthCheck(input) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchHealthCheck) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || output.HealthCheck == nil || output.HealthCheck.HealthCheckConfig == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.HealthCheck, nil +} + +func HostedZoneDnssec(conn *route53.Route53, hostedZoneID string) (*route53.GetDNSSECOutput, error) { + input := &route53.GetDNSSECInput{ + HostedZoneId: aws.String(hostedZoneID), + } + + output, err := conn.GetDNSSEC(input) + + if err != nil { + return nil, err + } + + return output, nil +} + func KeySigningKey(conn *route53.Route53, hostedZoneID string, name string) (*route53.KeySigningKey, error) { input := &route53.GetDNSSECInput{ HostedZoneId: aws.String(hostedZoneID), diff --git a/aws/internal/service/route53/waiter/status.go b/aws/internal/service/route53/waiter/status.go index eb520ef2d1e2..ac56d38bdaa3 100644 --- a/aws/internal/service/route53/waiter/status.go +++ b/aws/internal/service/route53/waiter/status.go @@ -27,6 +27,22 @@ func ChangeInfoStatus(conn *route53.Route53, changeID string) resource.StateRefr } } +func HostedZoneDnssecStatus(conn *route53.Route53, hostedZoneID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + hostedZoneDnssec, err := finder.HostedZoneDnssec(conn, hostedZoneID) + + if err != nil { + return nil, "", err + } + + if hostedZoneDnssec == nil || hostedZoneDnssec.Status == nil { + return nil, "", nil + } + + return hostedZoneDnssec.Status, aws.StringValue(hostedZoneDnssec.Status.ServeSignature), nil + } +} + func KeySigningKeyStatus(conn *route53.Route53, hostedZoneID string, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { keySigningKey, err := finder.KeySigningKey(conn, hostedZoneID, name) diff --git a/aws/internal/service/route53/waiter/waiter.go b/aws/internal/service/route53/waiter/waiter.go index c6c8dfd4a8db..130d3202d1a5 100644 --- a/aws/internal/service/route53/waiter/waiter.go +++ b/aws/internal/service/route53/waiter/waiter.go @@ -3,6 +3,7 @@ package waiter import ( "errors" "fmt" + "math/rand" "time" "github.com/aws/aws-sdk-go/aws" @@ -11,19 +12,28 @@ import ( ) const ( - ChangeTimeout = 30 * time.Minute + ChangeTimeout = 30 * time.Minute + ChangeMinTimeout = 5 * time.Second + ChangePollInterval = 15 * time.Second + + HostedZoneDnssecStatusTimeout = 5 * time.Minute KeySigningKeyStatusTimeout = 5 * time.Minute ) func ChangeInfoStatusInsync(conn *route53.Route53, changeID string) (*route53.ChangeInfo, error) { + rand.Seed(time.Now().UTC().UnixNano()) + + // Route53 is vulnerable to throttling so longer delays, poll intervals helps significantly to avoid + stateConf := &resource.StateChangeConf{ - Pending: []string{route53.ChangeStatusPending}, - Target: []string{route53.ChangeStatusInsync}, - Refresh: ChangeInfoStatus(conn, changeID), - Delay: 30 * time.Second, - MinTimeout: 5 * time.Second, - Timeout: ChangeTimeout, + Pending: []string{route53.ChangeStatusPending}, + Target: []string{route53.ChangeStatusInsync}, + Delay: time.Duration(rand.Int63n(20)+10) * time.Second, //nolint:gomnd + MinTimeout: ChangeMinTimeout, + PollInterval: ChangePollInterval, + Refresh: ChangeInfoStatus(conn, changeID), + Timeout: ChangeTimeout, } outputRaw, err := stateConf.WaitForState() @@ -35,6 +45,38 @@ func ChangeInfoStatusInsync(conn *route53.Route53, changeID string) (*route53.Ch return nil, err } +func HostedZoneDnssecStatusUpdated(conn *route53.Route53, hostedZoneID string, status string) (*route53.DNSSECStatus, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{status}, + Refresh: HostedZoneDnssecStatus(conn, hostedZoneID), + MinTimeout: 5 * time.Second, + Timeout: HostedZoneDnssecStatusTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*route53.DNSSECStatus); ok { + if err != nil && output != nil && output.ServeSignature != nil && output.StatusMessage != nil { + newErr := fmt.Errorf("%s: %s", aws.StringValue(output.ServeSignature), aws.StringValue(output.StatusMessage)) + + switch e := err.(type) { + case *resource.TimeoutError: + if e.LastError == nil { + e.LastError = newErr + } + case *resource.UnexpectedStateError: + if e.LastError == nil { + e.LastError = newErr + } + } + } + + return output, err + } + + return nil, err +} + func KeySigningKeyStatusUpdated(conn *route53.Route53, hostedZoneID string, name string, status string) (*route53.KeySigningKey, error) { stateConf := &resource.StateChangeConf{ Target: []string{status}, diff --git a/aws/internal/service/route53recoverycontrolconfig/waiter/status.go b/aws/internal/service/route53recoverycontrolconfig/waiter/status.go new file mode 100644 index 000000000000..a5f2393b8ffe --- /dev/null +++ b/aws/internal/service/route53recoverycontrolconfig/waiter/status.go @@ -0,0 +1,79 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + r53rcc "github.com/aws/aws-sdk-go/service/route53recoverycontrolconfig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func Route53RecoveryControlConfigClusterStatus(conn *r53rcc.Route53RecoveryControlConfig, clusterArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &r53rcc.DescribeClusterInput{ + ClusterArn: aws.String(clusterArn), + } + + output, err := conn.DescribeCluster(input) + + if err != nil { + return output, "", err + } + + return output, aws.StringValue(output.Cluster.Status), nil + } +} + +func Route53RecoveryControlConfigRoutingControlStatus(conn *r53rcc.Route53RecoveryControlConfig, routingControlArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &r53rcc.DescribeRoutingControlInput{ + RoutingControlArn: aws.String(routingControlArn), + } + + output, err := conn.DescribeRoutingControl(input) + + if err != nil { + return output, "", err + } + + return output, aws.StringValue(output.RoutingControl.Status), nil + } +} + +func Route53RecoveryControlConfigControlPanelStatus(conn *r53rcc.Route53RecoveryControlConfig, controlPanelArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &r53rcc.DescribeControlPanelInput{ + ControlPanelArn: aws.String(controlPanelArn), + } + + output, err := conn.DescribeControlPanel(input) + + if err != nil { + return output, "", err + } + + return output, aws.StringValue(output.ControlPanel.Status), nil + } +} + +func Route53RecoveryControlConfigSafetyRuleStatus(conn *r53rcc.Route53RecoveryControlConfig, safetyRuleArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &r53rcc.DescribeSafetyRuleInput{ + SafetyRuleArn: aws.String(safetyRuleArn), + } + + output, err := conn.DescribeSafetyRule(input) + + if err != nil { + return output, "", err + } + + if output.AssertionRule != nil { + return output, aws.StringValue(output.AssertionRule.Status), nil + } + + if output.GatingRule != nil { + return output, aws.StringValue(output.GatingRule.Status), nil + } + + return output, "", nil + } +} diff --git a/aws/internal/service/route53recoverycontrolconfig/waiter/waiter.go b/aws/internal/service/route53recoverycontrolconfig/waiter/waiter.go new file mode 100644 index 000000000000..bcc2876b602a --- /dev/null +++ b/aws/internal/service/route53recoverycontrolconfig/waiter/waiter.go @@ -0,0 +1,161 @@ +package waiter + +import ( + "time" + + r53rcc "github.com/aws/aws-sdk-go/service/route53recoverycontrolconfig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + Route53RecoveryControlConfigTimeout = 60 * time.Second + Route53RecoveryControlConfigMinTimeout = 5 * time.Second +) + +func Route53RecoveryControlConfigClusterCreated(conn *r53rcc.Route53RecoveryControlConfig, clusterArn string) (*r53rcc.DescribeClusterOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPending}, + Target: []string{r53rcc.StatusDeployed}, + Refresh: Route53RecoveryControlConfigClusterStatus(conn, clusterArn), + Timeout: Route53RecoveryControlConfigTimeout, + MinTimeout: Route53RecoveryControlConfigMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeClusterOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigClusterDeleted(conn *r53rcc.Route53RecoveryControlConfig, clusterArn string) (*r53rcc.DescribeClusterOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPendingDeletion}, + Target: []string{}, + Refresh: Route53RecoveryControlConfigClusterStatus(conn, clusterArn), + Timeout: Route53RecoveryControlConfigTimeout, + Delay: Route53RecoveryControlConfigMinTimeout, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeClusterOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigRoutingControlCreated(conn *r53rcc.Route53RecoveryControlConfig, routingControlArn string) (*r53rcc.DescribeRoutingControlOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPending}, + Target: []string{r53rcc.StatusDeployed}, + Refresh: Route53RecoveryControlConfigRoutingControlStatus(conn, routingControlArn), + Timeout: Route53RecoveryControlConfigTimeout, + MinTimeout: Route53RecoveryControlConfigMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeRoutingControlOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigRoutingControlDeleted(conn *r53rcc.Route53RecoveryControlConfig, routingControlArn string) (*r53rcc.DescribeRoutingControlOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPendingDeletion}, + Target: []string{}, + Refresh: Route53RecoveryControlConfigRoutingControlStatus(conn, routingControlArn), + Timeout: Route53RecoveryControlConfigTimeout, + Delay: Route53RecoveryControlConfigMinTimeout, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeRoutingControlOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigControlPanelCreated(conn *r53rcc.Route53RecoveryControlConfig, controlPanelArn string) (*r53rcc.DescribeControlPanelOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPending}, + Target: []string{r53rcc.StatusDeployed}, + Refresh: Route53RecoveryControlConfigControlPanelStatus(conn, controlPanelArn), + Timeout: Route53RecoveryControlConfigTimeout, + MinTimeout: Route53RecoveryControlConfigMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeControlPanelOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigControlPanelDeleted(conn *r53rcc.Route53RecoveryControlConfig, controlPanelArn string) (*r53rcc.DescribeControlPanelOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPendingDeletion}, + Target: []string{}, + Refresh: Route53RecoveryControlConfigControlPanelStatus(conn, controlPanelArn), + Timeout: Route53RecoveryControlConfigTimeout, + Delay: Route53RecoveryControlConfigMinTimeout, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeControlPanelOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigSafetyRuleCreated(conn *r53rcc.Route53RecoveryControlConfig, safetyRuleArn string) (*r53rcc.DescribeSafetyRuleOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPending}, + Target: []string{r53rcc.StatusDeployed}, + Refresh: Route53RecoveryControlConfigSafetyRuleStatus(conn, safetyRuleArn), + Timeout: Route53RecoveryControlConfigTimeout, + MinTimeout: Route53RecoveryControlConfigMinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeSafetyRuleOutput); ok { + return output, err + } + + return nil, err +} + +func Route53RecoveryControlConfigSafetyRuleDeleted(conn *r53rcc.Route53RecoveryControlConfig, safetyRuleArn string) (*r53rcc.DescribeSafetyRuleOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{r53rcc.StatusPendingDeletion}, + Target: []string{}, + Refresh: Route53RecoveryControlConfigSafetyRuleStatus(conn, safetyRuleArn), + Timeout: Route53RecoveryControlConfigTimeout, + Delay: Route53RecoveryControlConfigMinTimeout, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*r53rcc.DescribeSafetyRuleOutput); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/route53resolver/finder/finder.go b/aws/internal/service/route53resolver/finder/finder.go index 2c056d947f69..63e030eaecad 100644 --- a/aws/internal/service/route53resolver/finder/finder.go +++ b/aws/internal/service/route53resolver/finder/finder.go @@ -3,6 +3,8 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53resolver" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfroute53resolver "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/route53resolver" ) // ResolverQueryLogConfigAssociationByID returns the query logging configuration association corresponding to the specified ID. @@ -75,3 +77,135 @@ func ResolverDnssecConfigByID(conn *route53resolver.Route53Resolver, dnssecConfi return config, nil } + +// FirewallRuleGroupByID returns the DNS Firewall rule group corresponding to the specified ID. +// Returns nil if no DNS Firewall rule group is found. +func FirewallRuleGroupByID(conn *route53resolver.Route53Resolver, firewallGroupId string) (*route53resolver.FirewallRuleGroup, error) { + input := &route53resolver.GetFirewallRuleGroupInput{ + FirewallRuleGroupId: aws.String(firewallGroupId), + } + + output, err := conn.GetFirewallRuleGroup(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.FirewallRuleGroup, nil +} + +// FirewallDomainListByID returns the DNS Firewall rule group corresponding to the specified ID. +// Returns nil if no DNS Firewall rule group is found. +func FirewallDomainListByID(conn *route53resolver.Route53Resolver, firewallDomainListId string) (*route53resolver.FirewallDomainList, error) { + input := &route53resolver.GetFirewallDomainListInput{ + FirewallDomainListId: aws.String(firewallDomainListId), + } + + output, err := conn.GetFirewallDomainList(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.FirewallDomainList, nil +} + +// FirewallConfigByID returns the dnssec configuration corresponding to the specified ID. +// Returns NotFoundError if no configuration is found. +func FirewallConfigByID(conn *route53resolver.Route53Resolver, firewallConfigID string) (*route53resolver.FirewallConfig, error) { + input := &route53resolver.ListFirewallConfigsInput{} + + var config *route53resolver.FirewallConfig + // GetFirewallConfigs does not support query with id + err := conn.ListFirewallConfigsPages(input, func(page *route53resolver.ListFirewallConfigsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.FirewallConfigs { + if aws.StringValue(c.Id) == firewallConfigID { + config = c + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if config == nil { + return nil, &resource.NotFoundError{} + } + + return config, nil +} + +// FirewallRuleByID returns the DNS Firewall rule corresponding to the specified rule group and domain list IDs. +// Returns nil if no DNS Firewall rule is found. +func FirewallRuleByID(conn *route53resolver.Route53Resolver, firewallRuleId string) (*route53resolver.FirewallRule, error) { + firewallRuleGroupId, firewallDomainListId, err := tfroute53resolver.FirewallRuleParseID(firewallRuleId) + + if err != nil { + return nil, err + } + + var rule *route53resolver.FirewallRule + + input := &route53resolver.ListFirewallRulesInput{ + FirewallRuleGroupId: aws.String(firewallRuleGroupId), + } + + err = conn.ListFirewallRulesPages(input, func(page *route53resolver.ListFirewallRulesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, r := range page.FirewallRules { + if aws.StringValue(r.FirewallDomainListId) == firewallDomainListId { + rule = r + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if rule == nil { + return nil, nil + } + + return rule, nil +} + +// FirewallRuleGroupAssociationByID returns the DNS Firewall rule group association corresponding to the specified ID. +// Returns nil if no DNS Firewall rule group association is found. +func FirewallRuleGroupAssociationByID(conn *route53resolver.Route53Resolver, firewallRuleGroupAssociationId string) (*route53resolver.FirewallRuleGroupAssociation, error) { + input := &route53resolver.GetFirewallRuleGroupAssociationInput{ + FirewallRuleGroupAssociationId: aws.String(firewallRuleGroupAssociationId), + } + + output, err := conn.GetFirewallRuleGroupAssociation(input) + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.FirewallRuleGroupAssociation, nil +} diff --git a/aws/internal/service/route53resolver/id.go b/aws/internal/service/route53resolver/id.go new file mode 100644 index 000000000000..41da738ab648 --- /dev/null +++ b/aws/internal/service/route53resolver/id.go @@ -0,0 +1,25 @@ +package route53resolver + +import ( + "fmt" + "strings" +) + +const ruleIdSeparator = ":" + +func FirewallRuleCreateID(firewallRuleGroupId, firewallDomainListId string) string { + parts := []string{firewallRuleGroupId, firewallDomainListId} + id := strings.Join(parts, ruleIdSeparator) + + return id +} + +func FirewallRuleParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ruleIdSeparator, 2) + + if len(parts) < 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected firewall_rule_group_id%sfirewall_domain_list_id", id, ruleIdSeparator) + } + + return parts[0], parts[1], nil +} diff --git a/aws/internal/service/route53resolver/waiter/status.go b/aws/internal/service/route53resolver/waiter/status.go index b0086ebc332e..72d24206b4ce 100644 --- a/aws/internal/service/route53resolver/waiter/status.go +++ b/aws/internal/service/route53resolver/waiter/status.go @@ -17,6 +17,12 @@ const ( resolverDnssecConfigStatusNotFound = "NotFound" resolverDnssecConfigStatusUnknown = "Unknown" + + firewallDomainListStatusNotFound = "NotFound" + firewallDomainListStatusUnknown = "Unknown" + + resolverFirewallRuleGroupAssociationStatusNotFound = "NotFound" + resolverFirewallRuleGroupAssociationStatusUnknown = "Unknown" ) // QueryLogConfigAssociationStatus fetches the QueryLogConfigAssociation and its Status @@ -81,3 +87,45 @@ func DnssecConfigStatus(conn *route53resolver.Route53Resolver, dnssecConfigID st return dnssecConfig, aws.StringValue(dnssecConfig.ValidationStatus), nil } } + +// FirewallDomainListStatus fetches the FirewallDomainList and its Status +func FirewallDomainListStatus(conn *route53resolver.Route53Resolver, firewallDomainListId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + firewallDomainList, err := finder.FirewallDomainListByID(conn, firewallDomainListId) + + if tfawserr.ErrCodeEquals(err, route53resolver.ErrCodeResourceNotFoundException) { + return nil, firewallDomainListStatusNotFound, nil + } + + if err != nil { + return nil, firewallDomainListStatusUnknown, err + } + + if firewallDomainList == nil { + return nil, firewallDomainListStatusNotFound, nil + } + + return firewallDomainList, aws.StringValue(firewallDomainList.Status), nil + } +} + +// FirewallRuleGroupAssociationStatus fetches the FirewallRuleGroupAssociation and its Status +func FirewallRuleGroupAssociationStatus(conn *route53resolver.Route53Resolver, firewallRuleGroupAssociationId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + firewallRuleGroupAssociation, err := finder.FirewallRuleGroupAssociationByID(conn, firewallRuleGroupAssociationId) + + if tfawserr.ErrCodeEquals(err, route53resolver.ErrCodeResourceNotFoundException) { + return nil, resolverFirewallRuleGroupAssociationStatusNotFound, nil + } + + if err != nil { + return nil, resolverFirewallRuleGroupAssociationStatusUnknown, err + } + + if firewallRuleGroupAssociation == nil { + return nil, resolverFirewallRuleGroupAssociationStatusNotFound, nil + } + + return firewallRuleGroupAssociation, aws.StringValue(firewallRuleGroupAssociation.Status), nil + } +} diff --git a/aws/internal/service/route53resolver/waiter/waiter.go b/aws/internal/service/route53resolver/waiter/waiter.go index a414e334b034..ea18d0b32e61 100644 --- a/aws/internal/service/route53resolver/waiter/waiter.go +++ b/aws/internal/service/route53resolver/waiter/waiter.go @@ -1,8 +1,10 @@ package waiter import ( + "fmt" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53resolver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -25,6 +27,21 @@ const ( // Maximum amount of time to wait for a DnssecConfig to return DISABLED DnssecConfigDeletedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a FirewallDomainList to be updated + FirewallDomainListUpdatedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a FirewallDomainList to be deleted + FirewallDomainListDeletedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a FirewallRuleGroupAssociation to be created + FirewallRuleGroupAssociationCreatedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a FirewallRuleGroupAssociation to be updated + FirewallRuleGroupAssociationUpdatedTimeout = 5 * time.Minute + + // Maximum amount of time to wait for a FirewallRuleGroupAssociation to be deleted + FirewallRuleGroupAssociationDeletedTimeout = 5 * time.Minute ) // QueryLogConfigAssociationCreated waits for a QueryLogConfig to return ACTIVE @@ -134,3 +151,102 @@ func DnssecConfigDisabled(conn *route53resolver.Route53Resolver, dnssecConfigID return nil, err } + +// FirewallDomainListUpdated waits for a FirewallDomainList to be updated +func FirewallDomainListUpdated(conn *route53resolver.Route53Resolver, firewallDomainListId string) (*route53resolver.FirewallDomainList, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + route53resolver.FirewallDomainListStatusUpdating, + route53resolver.FirewallDomainListStatusImporting, + }, + Target: []string{ + route53resolver.FirewallDomainListStatusComplete, + route53resolver.FirewallDomainListStatusCompleteImportFailed, + }, + Refresh: FirewallDomainListStatus(conn, firewallDomainListId), + Timeout: FirewallDomainListUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.FirewallDomainList); ok { + if aws.StringValue(v.Status) != route53resolver.FirewallDomainListStatusComplete { + err = fmt.Errorf("error updating Route 53 Resolver DNS Firewall domain list (%s): %s", firewallDomainListId, aws.StringValue(v.StatusMessage)) + } + return v, err + } + + return nil, err +} + +// FirewallDomainListDeleted waits for a FirewallDomainList to be deleted +func FirewallDomainListDeleted(conn *route53resolver.Route53Resolver, firewallDomainListId string) (*route53resolver.FirewallDomainList, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.FirewallDomainListStatusDeleting}, + Target: []string{}, + Refresh: FirewallDomainListStatus(conn, firewallDomainListId), + Timeout: FirewallDomainListDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.FirewallDomainList); ok { + return v, err + } + + return nil, err +} + +// FirewallRuleGroupAssociationCreated waits for a FirewallRuleGroupAssociation to return COMPLETE +func FirewallRuleGroupAssociationCreated(conn *route53resolver.Route53Resolver, firewallRuleGroupAssociationId string) (*route53resolver.FirewallRuleGroupAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.FirewallRuleGroupAssociationStatusUpdating}, + Target: []string{route53resolver.FirewallRuleGroupAssociationStatusComplete}, + Refresh: FirewallRuleGroupAssociationStatus(conn, firewallRuleGroupAssociationId), + Timeout: FirewallRuleGroupAssociationCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.FirewallRuleGroupAssociation); ok { + return v, err + } + + return nil, err +} + +// FirewallRuleGroupAssociationUpdated waits for a FirewallRuleGroupAssociation to return COMPLETE +func FirewallRuleGroupAssociationUpdated(conn *route53resolver.Route53Resolver, firewallRuleGroupAssociationId string) (*route53resolver.FirewallRuleGroupAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.FirewallRuleGroupAssociationStatusUpdating}, + Target: []string{route53resolver.FirewallRuleGroupAssociationStatusComplete}, + Refresh: FirewallRuleGroupAssociationStatus(conn, firewallRuleGroupAssociationId), + Timeout: FirewallRuleGroupAssociationUpdatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.FirewallRuleGroupAssociation); ok { + return v, err + } + + return nil, err +} + +// FirewallRuleGroupAssociationDeleted waits for a FirewallRuleGroupAssociation to be deleted +func FirewallRuleGroupAssociationDeleted(conn *route53resolver.Route53Resolver, firewallRuleGroupAssociationId string) (*route53resolver.FirewallRuleGroupAssociation, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{route53resolver.FirewallRuleGroupAssociationStatusDeleting}, + Target: []string{}, + Refresh: FirewallRuleGroupAssociationStatus(conn, firewallRuleGroupAssociationId), + Timeout: FirewallRuleGroupAssociationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*route53resolver.FirewallRuleGroupAssociation); ok { + return v, err + } + + return nil, err +} diff --git a/aws/internal/service/s3/enum.go b/aws/internal/service/s3/enum.go new file mode 100644 index 000000000000..9686fdf8cd82 --- /dev/null +++ b/aws/internal/service/s3/enum.go @@ -0,0 +1,27 @@ +package s3 + +import ( + "github.com/aws/aws-sdk-go/service/s3" +) + +// These should be defined in the AWS SDK for Go. There is an open issue https://github.com/aws/aws-sdk-go/issues/2683 +const ( + BucketCannedACLAwsExecRead = "aws-exec-read" + BucketCannedACLLogDeliveryWrite = "log-delivery-write" +) + +func BucketCannedACL_Values() []string { + result := s3.BucketCannedACL_Values() + result = appendUniqueString(result, BucketCannedACLAwsExecRead) + result = appendUniqueString(result, BucketCannedACLLogDeliveryWrite) + return result +} + +func appendUniqueString(slice []string, elem string) []string { + for _, e := range slice { + if e == elem { + return slice + } + } + return append(slice, elem) +} diff --git a/aws/internal/service/s3/errors.go b/aws/internal/service/s3/errors.go index cde53679a638..ceafeca51dfd 100644 --- a/aws/internal/service/s3/errors.go +++ b/aws/internal/service/s3/errors.go @@ -4,6 +4,8 @@ package s3 // https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#pkg-constants const ( + ErrCodeNoSuchConfiguration = "NoSuchConfiguration" ErrCodeNoSuchPublicAccessBlockConfiguration = "NoSuchPublicAccessBlockConfiguration" ErrCodeNoSuchTagSet = "NoSuchTagSet" + ErrCodeOperationAborted = "OperationAborted" ) diff --git a/aws/internal/service/s3/waiter/waiter.go b/aws/internal/service/s3/waiter/waiter.go new file mode 100644 index 000000000000..15e8c699f9aa --- /dev/null +++ b/aws/internal/service/s3/waiter/waiter.go @@ -0,0 +1,19 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/s3" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + BucketCreatedTimeout = 2 * time.Minute + PropagationTimeout = 1 * time.Minute +) + +// RetryWhenBucketNotFound retries the specified function if the returned error indicates that a bucket is not found. +// If the retries time out the specified function is called one last time. +func RetryWhenBucketNotFound(f func() (interface{}, error)) (interface{}, error) { + return tfresource.RetryWhenAwsErrCodeEquals(PropagationTimeout, f, s3.ErrCodeNoSuchBucket) +} diff --git a/aws/internal/service/s3control/errors.go b/aws/internal/service/s3control/errors.go new file mode 100644 index 000000000000..357924765c5c --- /dev/null +++ b/aws/internal/service/s3control/errors.go @@ -0,0 +1,9 @@ +package s3control + +// Error code constants missing from AWS Go SDK: +// https://docs.aws.amazon.com/sdk-for-go/api/service/s3control/#pkg-constants + +const ( + ErrCodeNoSuchAccessPoint = "NoSuchAccessPoint" + ErrCodeNoSuchAccessPointPolicy = "NoSuchAccessPointPolicy" +) diff --git a/aws/internal/service/s3control/finder/finder.go b/aws/internal/service/s3control/finder/finder.go new file mode 100644 index 000000000000..1dad3975b3cd --- /dev/null +++ b/aws/internal/service/s3control/finder/finder.go @@ -0,0 +1,24 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3control" +) + +func PublicAccessBlockConfiguration(conn *s3control.S3Control, accountID string) (*s3control.PublicAccessBlockConfiguration, error) { + input := &s3control.GetPublicAccessBlockInput{ + AccountId: aws.String(accountID), + } + + output, err := conn.GetPublicAccessBlock(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output.PublicAccessBlockConfiguration, nil +} diff --git a/aws/internal/service/s3control/waiter/status.go b/aws/internal/service/s3control/waiter/status.go new file mode 100644 index 000000000000..a5d4e3b2baae --- /dev/null +++ b/aws/internal/service/s3control/waiter/status.go @@ -0,0 +1,78 @@ +package waiter + +import ( + "strconv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/s3control/finder" +) + +// PublicAccessBlockConfigurationBlockPublicAcls fetches the PublicAccessBlockConfiguration and its BlockPublicAcls +func PublicAccessBlockConfigurationBlockPublicAcls(conn *s3control.S3Control, accountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + publicAccessBlockConfiguration, err := finder.PublicAccessBlockConfiguration(conn, accountID) + + if err != nil { + return nil, "false", err + } + + if publicAccessBlockConfiguration == nil { + return nil, "false", nil + } + + return publicAccessBlockConfiguration, strconv.FormatBool(aws.BoolValue(publicAccessBlockConfiguration.BlockPublicAcls)), nil + } +} + +// PublicAccessBlockConfigurationBlockPublicPolicy fetches the PublicAccessBlockConfiguration and its BlockPublicPolicy +func PublicAccessBlockConfigurationBlockPublicPolicy(conn *s3control.S3Control, accountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + publicAccessBlockConfiguration, err := finder.PublicAccessBlockConfiguration(conn, accountID) + + if err != nil { + return nil, "false", err + } + + if publicAccessBlockConfiguration == nil { + return nil, "false", nil + } + + return publicAccessBlockConfiguration, strconv.FormatBool(aws.BoolValue(publicAccessBlockConfiguration.BlockPublicPolicy)), nil + } +} + +// PublicAccessBlockConfigurationIgnorePublicAcls fetches the PublicAccessBlockConfiguration and its IgnorePublicAcls +func PublicAccessBlockConfigurationIgnorePublicAcls(conn *s3control.S3Control, accountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + publicAccessBlockConfiguration, err := finder.PublicAccessBlockConfiguration(conn, accountID) + + if err != nil { + return nil, "false", err + } + + if publicAccessBlockConfiguration == nil { + return nil, "false", nil + } + + return publicAccessBlockConfiguration, strconv.FormatBool(aws.BoolValue(publicAccessBlockConfiguration.IgnorePublicAcls)), nil + } +} + +// PublicAccessBlockConfigurationRestrictPublicBuckets fetches the PublicAccessBlockConfiguration and its RestrictPublicBuckets +func PublicAccessBlockConfigurationRestrictPublicBuckets(conn *s3control.S3Control, accountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + publicAccessBlockConfiguration, err := finder.PublicAccessBlockConfiguration(conn, accountID) + + if err != nil { + return nil, "false", err + } + + if publicAccessBlockConfiguration == nil { + return nil, "false", nil + } + + return publicAccessBlockConfiguration, strconv.FormatBool(aws.BoolValue(publicAccessBlockConfiguration.RestrictPublicBuckets)), nil + } +} diff --git a/aws/internal/service/s3control/waiter/waiter.go b/aws/internal/service/s3control/waiter/waiter.go new file mode 100644 index 000000000000..81ebccd46417 --- /dev/null +++ b/aws/internal/service/s3control/waiter/waiter.go @@ -0,0 +1,92 @@ +package waiter + +import ( + "strconv" + "time" + + "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Minimum amount of times to verify change propagation + PropagationContinuousTargetOccurence = 2 + + // Minimum amount of time to wait between S3control change polls + PropagationMinTimeout = 5 * time.Second + + // Maximum amount of time to wait for S3control changes to propagate + PropagationTimeout = 1 * time.Minute +) + +func PublicAccessBlockConfigurationBlockPublicAclsUpdated(conn *s3control.S3Control, accountID string, expectedValue bool) (*s3control.PublicAccessBlockConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(expectedValue)}, + Refresh: PublicAccessBlockConfigurationBlockPublicAcls(conn, accountID), + Timeout: PropagationTimeout, + MinTimeout: PropagationMinTimeout, + ContinuousTargetOccurence: PropagationContinuousTargetOccurence, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*s3control.PublicAccessBlockConfiguration); ok { + return output, err + } + + return nil, err +} + +func PublicAccessBlockConfigurationBlockPublicPolicyUpdated(conn *s3control.S3Control, accountID string, expectedValue bool) (*s3control.PublicAccessBlockConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(expectedValue)}, + Refresh: PublicAccessBlockConfigurationBlockPublicPolicy(conn, accountID), + Timeout: PropagationTimeout, + MinTimeout: PropagationMinTimeout, + ContinuousTargetOccurence: PropagationContinuousTargetOccurence, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*s3control.PublicAccessBlockConfiguration); ok { + return output, err + } + + return nil, err +} + +func PublicAccessBlockConfigurationIgnorePublicAclsUpdated(conn *s3control.S3Control, accountID string, expectedValue bool) (*s3control.PublicAccessBlockConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(expectedValue)}, + Refresh: PublicAccessBlockConfigurationIgnorePublicAcls(conn, accountID), + Timeout: PropagationTimeout, + MinTimeout: PropagationMinTimeout, + ContinuousTargetOccurence: PropagationContinuousTargetOccurence, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*s3control.PublicAccessBlockConfiguration); ok { + return output, err + } + + return nil, err +} + +func PublicAccessBlockConfigurationRestrictPublicBucketsUpdated(conn *s3control.S3Control, accountID string, expectedValue bool) (*s3control.PublicAccessBlockConfiguration, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(expectedValue)}, + Refresh: PublicAccessBlockConfigurationRestrictPublicBuckets(conn, accountID), + Timeout: PropagationTimeout, + MinTimeout: PropagationMinTimeout, + ContinuousTargetOccurence: PropagationContinuousTargetOccurence, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*s3control.PublicAccessBlockConfiguration); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/s3outposts/waiter/waiter.go b/aws/internal/service/s3outposts/waiter/waiter.go index 8b3f94330c91..c2c769356afb 100644 --- a/aws/internal/service/s3outposts/waiter/waiter.go +++ b/aws/internal/service/s3outposts/waiter/waiter.go @@ -15,7 +15,7 @@ const ( EndpointStatusPending = "Pending" // Maximum amount of time to wait for Endpoint to return Available on creation - EndpointStatusCreatedTimeout = 5 * time.Minute + EndpointStatusCreatedTimeout = 20 * time.Minute ) // EndpointStatusCreated waits for Endpoint to return Available diff --git a/aws/internal/service/sagemaker/errors.go b/aws/internal/service/sagemaker/errors.go new file mode 100644 index 000000000000..2eb2da79efe9 --- /dev/null +++ b/aws/internal/service/sagemaker/errors.go @@ -0,0 +1,5 @@ +package sagemaker + +const ( + ErrCodeValidationException = "ValidationException" +) diff --git a/aws/internal/service/sagemaker/finder/finder.go b/aws/internal/service/sagemaker/finder/finder.go index 280f46778b42..cccf1e6be2fd 100644 --- a/aws/internal/service/sagemaker/finder/finder.go +++ b/aws/internal/service/sagemaker/finder/finder.go @@ -3,6 +3,10 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sagemaker" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfsagemaker "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sagemaker" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // CodeRepositoryByName returns the code repository corresponding to the specified name. @@ -13,6 +17,7 @@ func CodeRepositoryByName(conn *sagemaker.SageMaker, name string) (*sagemaker.De } output, err := conn.DescribeCodeRepository(input) + if err != nil { return nil, err } @@ -32,6 +37,7 @@ func ModelPackageGroupByName(conn *sagemaker.SageMaker, name string) (*sagemaker } output, err := conn.DescribeModelPackageGroup(input) + if err != nil { return nil, err } @@ -51,6 +57,7 @@ func ImageByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeIma } output, err := conn.DescribeImage(input) + if err != nil { return nil, err } @@ -70,6 +77,7 @@ func ImageVersionByName(conn *sagemaker.SageMaker, name string) (*sagemaker.Desc } output, err := conn.DescribeImageVersion(input) + if err != nil { return nil, err } @@ -81,6 +89,33 @@ func ImageVersionByName(conn *sagemaker.SageMaker, name string) (*sagemaker.Desc return output, nil } +// DeviceFleetByName returns the Device Fleet corresponding to the specified Device Fleet name. +// Returns nil if no Device Fleet is found. +func DeviceFleetByName(conn *sagemaker.SageMaker, id string) (*sagemaker.DescribeDeviceFleetOutput, error) { + input := &sagemaker.DescribeDeviceFleetInput{ + DeviceFleetName: aws.String(id), + } + + output, err := conn.DescribeDeviceFleet(input) + + if tfawserr.ErrMessageContains(err, tfsagemaker.ErrCodeValidationException, "No devicefleet with name") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + // DomainByName returns the domain corresponding to the specified domain id. // Returns nil if no domain is found. func DomainByName(conn *sagemaker.SageMaker, domainID string) (*sagemaker.DescribeDomainOutput, error) { @@ -89,6 +124,7 @@ func DomainByName(conn *sagemaker.SageMaker, domainID string) (*sagemaker.Descri } output, err := conn.DescribeDomain(input) + if err != nil { return nil, err } @@ -100,20 +136,26 @@ func DomainByName(conn *sagemaker.SageMaker, domainID string) (*sagemaker.Descri return output, nil } -// FeatureGroupByName returns the feature group corresponding to the specified name. -// Returns nil if no feature group is found. func FeatureGroupByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeFeatureGroupOutput, error) { input := &sagemaker.DescribeFeatureGroupInput{ FeatureGroupName: aws.String(name), } output, err := conn.DescribeFeatureGroup(input) + + if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } if output == nil { - return nil, nil + return nil, tfresource.NewEmptyResultError(input) } return output, nil @@ -128,6 +170,7 @@ func UserProfileByName(conn *sagemaker.SageMaker, domainID, userProfileName stri } output, err := conn.DescribeUserProfile(input) + if err != nil { return nil, err } @@ -147,6 +190,7 @@ func AppImageConfigByName(conn *sagemaker.SageMaker, appImageConfigID string) (* } output, err := conn.DescribeAppImageConfig(input) + if err != nil { return nil, err } @@ -157,3 +201,176 @@ func AppImageConfigByName(conn *sagemaker.SageMaker, appImageConfigID string) (* return output, nil } + +// AppByName returns the domain corresponding to the specified domain id. +// Returns nil if no domain is found. +func AppByName(conn *sagemaker.SageMaker, domainID, userProfileName, appType, appName string) (*sagemaker.DescribeAppOutput, error) { + input := &sagemaker.DescribeAppInput{ + DomainId: aws.String(domainID), + UserProfileName: aws.String(userProfileName), + AppType: aws.String(appType), + AppName: aws.String(appName), + } + + output, err := conn.DescribeApp(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + return output, nil +} + +func WorkforceByName(conn *sagemaker.SageMaker, name string) (*sagemaker.Workforce, error) { + input := &sagemaker.DescribeWorkforceInput{ + WorkforceName: aws.String(name), + } + + output, err := conn.DescribeWorkforce(input) + + if tfawserr.ErrMessageContains(err, tfsagemaker.ErrCodeValidationException, "No workforce") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Workforce == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Workforce, nil +} + +func WorkteamByName(conn *sagemaker.SageMaker, name string) (*sagemaker.Workteam, error) { + input := &sagemaker.DescribeWorkteamInput{ + WorkteamName: aws.String(name), + } + + output, err := conn.DescribeWorkteam(input) + + if tfawserr.ErrMessageContains(err, tfsagemaker.ErrCodeValidationException, "The work team") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Workteam == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Workteam, nil +} + +func HumanTaskUiByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeHumanTaskUiOutput, error) { + input := &sagemaker.DescribeHumanTaskUiInput{ + HumanTaskUiName: aws.String(name), + } + + output, err := conn.DescribeHumanTaskUi(input) + + if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func EndpointConfigByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeEndpointConfigOutput, error) { + input := &sagemaker.DescribeEndpointConfigInput{ + EndpointConfigName: aws.String(name), + } + + output, err := conn.DescribeEndpointConfig(input) + + if tfawserr.ErrMessageContains(err, tfsagemaker.ErrCodeValidationException, "Could not find endpoint configuration") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func FlowDefinitionByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeFlowDefinitionOutput, error) { + input := &sagemaker.DescribeFlowDefinitionInput{ + FlowDefinitionName: aws.String(name), + } + + output, err := conn.DescribeFlowDefinition(input) + + if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func StudioLifecycleConfigByName(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeStudioLifecycleConfigOutput, error) { + input := &sagemaker.DescribeStudioLifecycleConfigInput{ + StudioLifecycleConfigName: aws.String(name), + } + + output, err := conn.DescribeStudioLifecycleConfig(input) + + if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} diff --git a/aws/internal/service/sagemaker/waiter/status.go b/aws/internal/service/sagemaker/waiter/status.go index 57b7eb68f3fb..9e92faf87aaf 100644 --- a/aws/internal/service/sagemaker/waiter/status.go +++ b/aws/internal/service/sagemaker/waiter/status.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sagemaker/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -17,10 +18,9 @@ const ( SagemakerImageVersionStatusNotFound = "NotFound" SagemakerImageVersionStatusFailed = "Failed" SagemakerDomainStatusNotFound = "NotFound" - SagemakerFeatureGroupStatusNotFound = "NotFound" - SagemakerFeatureGroupStatusUnknown = "Unknown" SagemakerUserProfileStatusNotFound = "NotFound" SagemakerModelPackageGroupStatusNotFound = "NotFound" + SagemakerAppStatusNotFound = "NotFound" ) // NotebookInstanceStatus fetches the NotebookInstance and its Status @@ -156,23 +156,35 @@ func DomainStatus(conn *sagemaker.SageMaker, domainID string) resource.StateRefr } } -// FeatureGroupStatus fetches the Feature Group and its Status func FeatureGroupStatus(conn *sagemaker.SageMaker, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { output, err := finder.FeatureGroupByName(conn, name) - if tfawserr.ErrCodeEquals(err, sagemaker.ErrCodeResourceNotFound) { - return nil, SagemakerFeatureGroupStatusNotFound, nil + + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return nil, SagemakerFeatureGroupStatusUnknown, err + return nil, "", err } - if output == nil { - return nil, SagemakerFeatureGroupStatusNotFound, nil + return output, aws.StringValue(output.FeatureGroupStatus), nil + } +} + +func FlowDefinitionStatus(conn *sagemaker.SageMaker, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.FlowDefinitionByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil } - return output, aws.StringValue(output.FeatureGroupStatus), nil + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.FlowDefinitionStatus), nil } } @@ -201,3 +213,31 @@ func UserProfileStatus(conn *sagemaker.SageMaker, domainID, userProfileName stri return output, aws.StringValue(output.Status), nil } } + +// AppStatus fetches the App and its Status +func AppStatus(conn *sagemaker.SageMaker, domainID, userProfileName, appType, appName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &sagemaker.DescribeAppInput{ + DomainId: aws.String(domainID), + UserProfileName: aws.String(userProfileName), + AppType: aws.String(appType), + AppName: aws.String(appName), + } + + output, err := conn.DescribeApp(input) + + if tfawserr.ErrMessageContains(err, "ValidationException", "RecordNotFound") { + return nil, SagemakerAppStatusNotFound, nil + } + + if err != nil { + return nil, sagemaker.AppStatusFailed, err + } + + if output == nil { + return nil, SagemakerAppStatusNotFound, nil + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/sagemaker/waiter/waiter.go b/aws/internal/service/sagemaker/waiter/waiter.go index c3618d16d088..1323356bd603 100644 --- a/aws/internal/service/sagemaker/waiter/waiter.go +++ b/aws/internal/service/sagemaker/waiter/waiter.go @@ -1,14 +1,17 @@ package waiter import ( + "errors" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sagemaker" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - NotebookInstanceInServiceTimeout = 10 * time.Minute + NotebookInstanceInServiceTimeout = 60 * time.Minute NotebookInstanceStoppedTimeout = 10 * time.Minute NotebookInstanceDeletedTimeout = 10 * time.Minute ModelPackageGroupCompletedTimeout = 10 * time.Minute @@ -23,6 +26,10 @@ const ( FeatureGroupDeletedTimeout = 10 * time.Minute UserProfileInServiceTimeout = 10 * time.Minute UserProfileDeletedTimeout = 10 * time.Minute + AppInServiceTimeout = 10 * time.Minute + AppDeletedTimeout = 10 * time.Minute + FlowDefinitionActiveTimeout = 2 * time.Minute + FlowDefinitionDeletedTimeout = 2 * time.Minute ) // NotebookInstanceInService waits for a NotebookInstance to return InService @@ -213,6 +220,7 @@ func DomainInService(conn *sagemaker.SageMaker, domainID string) (*sagemaker.Des Pending: []string{ SagemakerDomainStatusNotFound, sagemaker.DomainStatusPending, + sagemaker.DomainStatusUpdating, }, Target: []string{sagemaker.DomainStatusInService}, Refresh: DomainStatus(conn, domainID), @@ -260,6 +268,10 @@ func FeatureGroupCreated(conn *sagemaker.SageMaker, name string) (*sagemaker.Des outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*sagemaker.DescribeFeatureGroupOutput); ok { + if status, reason := aws.StringValue(output.FeatureGroupStatus), aws.StringValue(output.FailureReason); status == sagemaker.FeatureGroupStatusCreateFailed && reason != "" { + tfresource.SetLastError(err, errors.New(reason)) + } + return output, err } @@ -278,6 +290,10 @@ func FeatureGroupDeleted(conn *sagemaker.SageMaker, name string) (*sagemaker.Des outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*sagemaker.DescribeFeatureGroupOutput); ok { + if status, reason := aws.StringValue(output.FeatureGroupStatus), aws.StringValue(output.FailureReason); status == sagemaker.FeatureGroupStatusDeleteFailed && reason != "" { + tfresource.SetLastError(err, errors.New(reason)) + } + return output, err } @@ -325,3 +341,90 @@ func UserProfileDeleted(conn *sagemaker.SageMaker, domainID, userProfileName str return nil, err } + +// AppInService waits for a App to return InService +func AppInService(conn *sagemaker.SageMaker, domainID, userProfileName, appType, appName string) (*sagemaker.DescribeAppOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + SagemakerAppStatusNotFound, + sagemaker.AppStatusPending, + }, + Target: []string{sagemaker.AppStatusInService}, + Refresh: AppStatus(conn, domainID, userProfileName, appType, appName), + Timeout: AppInServiceTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeAppOutput); ok { + return output, err + } + + return nil, err +} + +// AppDeleted waits for a App to return Deleted +func AppDeleted(conn *sagemaker.SageMaker, domainID, userProfileName, appType, appName string) (*sagemaker.DescribeAppOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + sagemaker.AppStatusDeleting, + }, + Target: []string{ + sagemaker.AppStatusDeleted, + }, + Refresh: AppStatus(conn, domainID, userProfileName, appType, appName), + Timeout: AppDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeAppOutput); ok { + return output, err + } + + return nil, err +} + +// FlowDefinitionActive waits for a FlowDefinition to return Active +func FlowDefinitionActive(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeFlowDefinitionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{sagemaker.FlowDefinitionStatusInitializing}, + Target: []string{sagemaker.FlowDefinitionStatusActive}, + Refresh: FlowDefinitionStatus(conn, name), + Timeout: FlowDefinitionActiveTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeFlowDefinitionOutput); ok { + if status, reason := aws.StringValue(output.FlowDefinitionStatus), aws.StringValue(output.FailureReason); status == sagemaker.FlowDefinitionStatusFailed && reason != "" { + tfresource.SetLastError(err, errors.New(reason)) + } + + return output, err + } + + return nil, err +} + +// FlowDefinitionDeleted waits for a FlowDefinition to return Deleted +func FlowDefinitionDeleted(conn *sagemaker.SageMaker, name string) (*sagemaker.DescribeFlowDefinitionOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{sagemaker.FlowDefinitionStatusDeleting}, + Target: []string{}, + Refresh: FlowDefinitionStatus(conn, name), + Timeout: FlowDefinitionDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sagemaker.DescribeFlowDefinitionOutput); ok { + if status, reason := aws.StringValue(output.FlowDefinitionStatus), aws.StringValue(output.FailureReason); status == sagemaker.FlowDefinitionStatusFailed && reason != "" { + tfresource.SetLastError(err, errors.New(reason)) + } + + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/schemas/finder/finder.go b/aws/internal/service/schemas/finder/finder.go new file mode 100644 index 000000000000..c2511a294e78 --- /dev/null +++ b/aws/internal/service/schemas/finder/finder.go @@ -0,0 +1,93 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/schemas" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func DiscovererByID(conn *schemas.Schemas, id string) (*schemas.DescribeDiscovererOutput, error) { + input := &schemas.DescribeDiscovererInput{ + DiscovererId: aws.String(id), + } + + output, err := conn.DescribeDiscoverer(input) + + if tfawserr.ErrCodeEquals(err, schemas.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func RegistryByName(conn *schemas.Schemas, name string) (*schemas.DescribeRegistryOutput, error) { + input := &schemas.DescribeRegistryInput{ + RegistryName: aws.String(name), + } + + output, err := conn.DescribeRegistry(input) + + if tfawserr.ErrCodeEquals(err, schemas.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func SchemaByNameAndRegistryName(conn *schemas.Schemas, name, registryName string) (*schemas.DescribeSchemaOutput, error) { + input := &schemas.DescribeSchemaInput{ + RegistryName: aws.String(registryName), + SchemaName: aws.String(name), + } + + output, err := conn.DescribeSchema(input) + + if tfawserr.ErrCodeEquals(err, schemas.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/schemas/id.go b/aws/internal/service/schemas/id.go new file mode 100644 index 000000000000..71d0435c119b --- /dev/null +++ b/aws/internal/service/schemas/id.go @@ -0,0 +1,25 @@ +package transfer + +import ( + "fmt" + "strings" +) + +const schemaResourceIDSeparator = "/" + +func SchemaCreateResourceID(schemaName, registryName string) string { + parts := []string{schemaName, registryName} + id := strings.Join(parts, schemaResourceIDSeparator) + + return id +} + +func SchemaParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, schemaResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected SCHEMA_NAME%[2]sREGISTRY_NAME", id, schemaResourceIDSeparator) +} diff --git a/aws/internal/service/secretsmanager/waiter/waiter.go b/aws/internal/service/secretsmanager/waiter/waiter.go index 087f9e82ee8d..31333284cb6c 100644 --- a/aws/internal/service/secretsmanager/waiter/waiter.go +++ b/aws/internal/service/secretsmanager/waiter/waiter.go @@ -5,6 +5,6 @@ import ( ) const ( - // Maximum amount of time to wait for Secrets Manager deletions to propagate - DeletionPropagationTimeout = 2 * time.Minute + // Maximum amount of time to wait for Secrets Manager changes to propagate + PropagationTimeout = 2 * time.Minute ) diff --git a/aws/internal/service/securityhub/arn.go b/aws/internal/service/securityhub/arn.go new file mode 100644 index 000000000000..105541e3021d --- /dev/null +++ b/aws/internal/service/securityhub/arn.go @@ -0,0 +1,44 @@ +package securityhub + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" +) + +const ( + ARNSeparator = "/" + ARNService = "securityhub" +) + +// StandardsControlARNToStandardsSubscriptionARN converts a security standard control ARN to a subscription ARN. +func StandardsControlARNToStandardsSubscriptionARN(inputARN string) (string, error) { + parsedARN, err := arn.Parse(inputARN) + + if err != nil { + return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err) + } + + if actual, expected := parsedARN.Service, ARNService; actual != expected { + return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual) + } + + inputResourceParts := strings.Split(parsedARN.Resource, ARNSeparator) + + if actual, expected := len(inputResourceParts), 3; actual < expected { + return "", fmt.Errorf("expected at least %d resource parts in ARN (%s), got: %d", expected, inputARN, actual) + } + + outputResourceParts := append([]string{"subscription"}, inputResourceParts[1:len(inputResourceParts)-1]...) + + outputARN := arn.ARN{ + Partition: parsedARN.Partition, + Service: parsedARN.Service, + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + Resource: strings.Join(outputResourceParts, ARNSeparator), + }.String() + + return outputARN, nil +} diff --git a/aws/internal/service/securityhub/arn_test.go b/aws/internal/service/securityhub/arn_test.go new file mode 100644 index 000000000000..76a9e61994bc --- /dev/null +++ b/aws/internal/service/securityhub/arn_test.go @@ -0,0 +1,65 @@ +package securityhub_test + +import ( + "regexp" + "testing" + + tfsecurityhub "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub" +) + +func TestStandardsControlARNToStandardsSubscriptionARN(t *testing.T) { + testCases := []struct { + TestName string + InputARN string + ExpectedError *regexp.Regexp + ExpectedARN string + }{ + { + TestName: "empty ARN", + InputARN: "", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "unparsable ARN", + InputARN: "test", + ExpectedError: regexp.MustCompile(`error parsing ARN`), + }, + { + TestName: "invalid ARN service", + InputARN: "arn:aws:ec2:us-west-2:1234567890:control/cis-aws-foundations-benchmark/v/1.2.0/1.1", + ExpectedError: regexp.MustCompile(`expected service securityhub`), + }, + { + TestName: "invalid ARN resource parts", + InputARN: "arn:aws:securityhub:us-west-2:1234567890:control/cis-aws-foundations-benchmark", + ExpectedError: regexp.MustCompile(`expected at least 3 resource parts`), + }, + { + TestName: "valid ARN", + InputARN: "arn:aws:securityhub:us-west-2:1234567890:control/cis-aws-foundations-benchmark/v/1.2.0/1.1", + ExpectedARN: "arn:aws:securityhub:us-west-2:1234567890:subscription/cis-aws-foundations-benchmark/v/1.2.0", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + got, err := tfsecurityhub.StandardsControlARNToStandardsSubscriptionARN(testCase.InputARN) + + if err == nil && testCase.ExpectedError != nil { + t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String()) + } + + if err != nil && testCase.ExpectedError == nil { + t.Fatalf("got unexpected error: %s", err) + } + + if err != nil && !testCase.ExpectedError.MatchString(err.Error()) { + t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err) + } + + if got != testCase.ExpectedARN { + t.Errorf("got %s, expected %s", got, testCase.ExpectedARN) + } + }) + } +} diff --git a/aws/internal/service/securityhub/finder/finder.go b/aws/internal/service/securityhub/finder/finder.go index e13cd6a0708c..4b99b707f1ca 100644 --- a/aws/internal/service/securityhub/finder/finder.go +++ b/aws/internal/service/securityhub/finder/finder.go @@ -1,8 +1,12 @@ package finder import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func AdminAccount(conn *securityhub.SecurityHub, adminAccountID string) (*securityhub.AdminAccount, error) { @@ -30,3 +34,101 @@ func AdminAccount(conn *securityhub.SecurityHub, adminAccountID string) (*securi return result, err } + +func Insight(ctx context.Context, conn *securityhub.SecurityHub, arn string) (*securityhub.Insight, error) { + input := &securityhub.GetInsightsInput{ + InsightArns: aws.StringSlice([]string{arn}), + MaxResults: aws.Int64(1), + } + + output, err := conn.GetInsightsWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.Insights) == 0 { + return nil, nil + } + + return output.Insights[0], nil +} + +func StandardsControlByStandardsSubscriptionARNAndStandardsControlARN(ctx context.Context, conn *securityhub.SecurityHub, standardsSubscriptionARN, standardsControlARN string) (*securityhub.StandardsControl, error) { + input := &securityhub.DescribeStandardsControlsInput{ + StandardsSubscriptionArn: aws.String(standardsSubscriptionARN), + } + var output *securityhub.StandardsControl + + err := conn.DescribeStandardsControlsPagesWithContext(ctx, input, func(page *securityhub.DescribeStandardsControlsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, control := range page.Controls { + if aws.StringValue(control.StandardsControlArn) == standardsControlARN { + output = control + + return false + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + +func StandardsSubscriptionByARN(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) { + input := &securityhub.GetEnabledStandardsInput{ + StandardsSubscriptionArns: aws.StringSlice([]string{arn}), + } + + output, err := conn.GetEnabledStandards(input) + + if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || len(output.StandardsSubscriptions) == 0 || output.StandardsSubscriptions[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO Check for multiple results. + // TODO https://github.com/hashicorp/terraform-provider-aws/pull/17613. + + subscription := output.StandardsSubscriptions[0] + + if status := aws.StringValue(subscription.StandardsStatus); status == securityhub.StandardsStatusFailed { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return subscription, nil +} diff --git a/aws/internal/service/securityhub/waiter/status.go b/aws/internal/service/securityhub/waiter/status.go index 6ab9c3360041..d377ec257b37 100644 --- a/aws/internal/service/securityhub/waiter/status.go +++ b/aws/internal/service/securityhub/waiter/status.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/service/securityhub" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -13,6 +14,8 @@ const ( // AdminStatus Unknown AdminStatusUnknown = "Unknown" + + StandardsStatusNotFound = "NotFound" ) // AdminAccountAdminStatus fetches the AdminAccount and its AdminStatus @@ -31,3 +34,21 @@ func AdminAccountAdminStatus(conn *securityhub.SecurityHub, adminAccountID strin return adminAccount, aws.StringValue(adminAccount.Status), nil } } + +func StandardsSubscriptionStatus(conn *securityhub.SecurityHub, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.StandardsSubscriptionByARN(conn, arn) + + if tfresource.NotFound(err) { + // Return a fake result and status to deal with the INCOMPLETE subscription status + // being a target for both Create and Delete. + return "", StandardsStatusNotFound, nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.StandardsStatus), nil + } +} diff --git a/aws/internal/service/securityhub/waiter/waiter.go b/aws/internal/service/securityhub/waiter/waiter.go index deac42a90912..d91080fcfb03 100644 --- a/aws/internal/service/securityhub/waiter/waiter.go +++ b/aws/internal/service/securityhub/waiter/waiter.go @@ -13,6 +13,9 @@ const ( // Maximum amount of time to wait for an AdminAccount to return NotFound AdminAccountNotFoundTimeout = 5 * time.Minute + + StandardsSubscriptionCreateTimeout = 3 * time.Minute + StandardsSubscriptionDeleteTimeout = 3 * time.Minute ) // AdminAccountEnabled waits for an AdminAccount to return Enabled @@ -50,3 +53,37 @@ func AdminAccountNotFound(conn *securityhub.SecurityHub, adminAccountID string) return nil, err } + +func StandardsSubscriptionCreated(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{securityhub.StandardsStatusPending}, + Target: []string{securityhub.StandardsStatusReady, securityhub.StandardsStatusIncomplete}, + Refresh: StandardsSubscriptionStatus(conn, arn), + Timeout: StandardsSubscriptionCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*securityhub.StandardsSubscription); ok { + return output, err + } + + return nil, err +} + +func StandardsSubscriptionDeleted(conn *securityhub.SecurityHub, arn string) (*securityhub.StandardsSubscription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{securityhub.StandardsStatusDeleting}, + Target: []string{StandardsStatusNotFound, securityhub.StandardsStatusIncomplete}, + Refresh: StandardsSubscriptionStatus(conn, arn), + Timeout: StandardsSubscriptionDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*securityhub.StandardsSubscription); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/servicecatalog/enum.go b/aws/internal/service/servicecatalog/enum.go new file mode 100644 index 000000000000..90f3ea3ccfed --- /dev/null +++ b/aws/internal/service/servicecatalog/enum.go @@ -0,0 +1,33 @@ +package servicecatalog + +const ( + // If AWS adds these to the API, we should use those and remove these. + + AcceptLanguageEnglish = "en" + AcceptLanguageJapanese = "jp" + AcceptLanguageChinese = "zh" + + ConstraintTypeLaunch = "LAUNCH" + ConstraintTypeNotification = "NOTIFICATION" + ConstraintTypeResourceUpdate = "RESOURCE_UPDATE" + ConstraintTypeStackset = "STACKSET" + ConstraintTypeTemplate = "TEMPLATE" +) + +func AcceptLanguage_Values() []string { + return []string{ + AcceptLanguageEnglish, + AcceptLanguageJapanese, + AcceptLanguageChinese, + } +} + +func ConstraintType_Values() []string { + return []string{ + ConstraintTypeLaunch, + ConstraintTypeNotification, + ConstraintTypeResourceUpdate, + ConstraintTypeStackset, + ConstraintTypeTemplate, + } +} diff --git a/aws/internal/service/servicecatalog/finder/finder.go b/aws/internal/service/servicecatalog/finder/finder.go new file mode 100644 index 000000000000..0cf7c7496b82 --- /dev/null +++ b/aws/internal/service/servicecatalog/finder/finder.go @@ -0,0 +1,162 @@ +package finder + +import ( + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" +) + +func PortfolioShare(conn *servicecatalog.ServiceCatalog, portfolioID, shareType, principalID string) (*servicecatalog.PortfolioShareDetail, error) { + input := &servicecatalog.DescribePortfolioSharesInput{ + PortfolioId: aws.String(portfolioID), + Type: aws.String(shareType), + } + var result *servicecatalog.PortfolioShareDetail + + err := conn.DescribePortfolioSharesPages(input, func(page *servicecatalog.DescribePortfolioSharesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.PortfolioShareDetails { + if deet == nil { + continue + } + + if strings.Contains(principalID, aws.StringValue(deet.PrincipalId)) { + result = deet + return false + } + } + + return !lastPage + }) + + return result, err +} + +func ProductPortfolioAssociation(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) (*servicecatalog.PortfolioDetail, error) { + // seems odd that the sourcePortfolioID is not returned or searchable... + input := &servicecatalog.ListPortfoliosForProductInput{ + ProductId: aws.String(productID), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + var result *servicecatalog.PortfolioDetail + + err := conn.ListPortfoliosForProductPages(input, func(page *servicecatalog.ListPortfoliosForProductOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.PortfolioDetails { + if deet == nil { + continue + } + + if aws.StringValue(deet.Id) == portfolioID { + result = deet + return false + } + } + + return !lastPage + }) + + return result, err +} + +func BudgetResourceAssociation(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) (*servicecatalog.BudgetDetail, error) { + input := &servicecatalog.ListBudgetsForResourceInput{ + ResourceId: aws.String(resourceID), + } + + var result *servicecatalog.BudgetDetail + + err := conn.ListBudgetsForResourcePages(input, func(page *servicecatalog.ListBudgetsForResourceOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, budget := range page.Budgets { + if budget == nil { + continue + } + + if aws.StringValue(budget.BudgetName) == budgetName { + result = budget + return false + } + } + + return !lastPage + }) + + return result, err +} + +func TagOptionResourceAssociation(conn *servicecatalog.ServiceCatalog, tagOptionID, resourceID string) (*servicecatalog.ResourceDetail, error) { + input := &servicecatalog.ListResourcesForTagOptionInput{ + TagOptionId: aws.String(tagOptionID), + } + + var result *servicecatalog.ResourceDetail + + err := conn.ListResourcesForTagOptionPages(input, func(page *servicecatalog.ListResourcesForTagOptionOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.ResourceDetails { + if deet == nil { + continue + } + + if aws.StringValue(deet.Id) == resourceID { + result = deet + return false + } + } + + return !lastPage + }) + + return result, err +} + +func PrincipalPortfolioAssociation(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) (*servicecatalog.Principal, error) { + input := &servicecatalog.ListPrincipalsForPortfolioInput{ + PortfolioId: aws.String(portfolioID), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + var result *servicecatalog.Principal + + err := conn.ListPrincipalsForPortfolioPages(input, func(page *servicecatalog.ListPrincipalsForPortfolioOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.Principals { + if deet == nil { + continue + } + + if aws.StringValue(deet.PrincipalARN) == principalARN { + result = deet + return false + } + } + + return !lastPage + }) + + return result, err +} diff --git a/aws/internal/service/servicecatalog/id.go b/aws/internal/service/servicecatalog/id.go new file mode 100644 index 000000000000..a15f1a759a24 --- /dev/null +++ b/aws/internal/service/servicecatalog/id.go @@ -0,0 +1,93 @@ +package servicecatalog + +import ( + "fmt" + "strings" +) + +func PortfolioShareParseResourceID(id string) (string, string, string, error) { + parts := strings.SplitN(id, ":", 3) + + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected portfolioID:type:principalID", id) + } + + return parts[0], parts[1], parts[2], nil +} + +func PortfolioShareCreateResourceID(portfolioID, shareType, principalID string) string { + return strings.Join([]string{portfolioID, shareType, principalID}, ":") +} + +func ProductPortfolioAssociationParseID(id string) (string, string, string, error) { + parts := strings.SplitN(id, ":", 3) + + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected acceptLanguage:portfolioID:productID", id) + } + + return parts[0], parts[1], parts[2], nil +} + +func ProductPortfolioAssociationCreateID(acceptLanguage, portfolioID, productID string) string { + return strings.Join([]string{acceptLanguage, portfolioID, productID}, ":") +} + +func BudgetResourceAssociationParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), budgetName:resourceID", id) + } + + return parts[0], parts[1], nil +} + +func BudgetResourceAssociationID(budgetName, resourceID string) string { + return strings.Join([]string{budgetName, resourceID}, ":") +} + +func TagOptionResourceAssociationParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), tagOptionID:resourceID", id) + } + + return parts[0], parts[1], nil +} + +func TagOptionResourceAssociationID(tagOptionID, resourceID string) string { + return strings.Join([]string{tagOptionID, resourceID}, ":") +} + +func ProvisioningArtifactID(artifactID, productID string) string { + return strings.Join([]string{artifactID, productID}, ":") +} + +func ProvisioningArtifactParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected artifactID:productID", id) + } + return parts[0], parts[1], nil +} + +func PrincipalPortfolioAssociationParseID(id string) (string, string, string, error) { + parts := strings.SplitN(id, ",", 3) + + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected acceptLanguage,principalARN,portfolioID", id) + } + + return parts[0], parts[1], parts[2], nil +} + +func PrincipalPortfolioAssociationID(acceptLanguage, principalARN, portfolioID string) string { + return strings.Join([]string{acceptLanguage, principalARN, portfolioID}, ",") +} + +func PortfolioConstraintsID(acceptLanguage, portfolioID, productID string) string { + return strings.Join([]string{acceptLanguage, portfolioID, productID}, ":") +} diff --git a/aws/internal/service/servicecatalog/waiter/status.go b/aws/internal/service/servicecatalog/waiter/status.go new file mode 100644 index 000000000000..7f4dfe872419 --- /dev/null +++ b/aws/internal/service/servicecatalog/waiter/status.go @@ -0,0 +1,463 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfservicecatalog "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicecatalog/finder" +) + +func ProductStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeProductAsAdminInput{ + Id: aws.String(productID), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + output, err := conn.DescribeProductAsAdmin(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceInUseException) { + return nil, StatusUnavailable, err + } + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeLimitExceededException) { + return nil, StatusUnavailable, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing product status: %w", err) + } + + if output == nil || output.ProductViewDetail == nil { + return nil, StatusUnavailable, fmt.Errorf("error describing product status: empty product view detail") + } + + return output, aws.StringValue(output.ProductViewDetail.Status), err + } +} + +func TagOptionStatus(conn *servicecatalog.ServiceCatalog, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeTagOptionInput{ + Id: aws.String(id), + } + + output, err := conn.DescribeTagOption(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing tag option: %w", err) + } + + if output == nil || output.TagOptionDetail == nil { + return nil, StatusUnavailable, fmt.Errorf("error describing tag option: empty tag option detail") + } + + return output.TagOptionDetail, servicecatalog.StatusAvailable, err + } +} + +func PortfolioShareStatusWithToken(conn *servicecatalog.ServiceCatalog, token string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribePortfolioShareStatusInput{ + PortfolioShareToken: aws.String(token), + } + output, err := conn.DescribePortfolioShareStatus(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.ShareStatusError, fmt.Errorf("error describing portfolio share status: %w", err) + } + + if output == nil { + return nil, StatusUnavailable, fmt.Errorf("error describing portfolio share status: empty response") + } + + return output, aws.StringValue(output.Status), err + } +} + +func PortfolioShareStatus(conn *servicecatalog.ServiceCatalog, portfolioID, shareType, principalID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.PortfolioShare(conn, portfolioID, shareType, principalID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.ShareStatusError, fmt.Errorf("error finding portfolio share: %w", err) + } + + if output == nil { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("error finding portfolio share (%s:%s:%s): empty response", portfolioID, shareType, principalID), + } + } + + if !aws.BoolValue(output.Accepted) { + return output, servicecatalog.ShareStatusInProgress, err + } + + return output, servicecatalog.ShareStatusCompleted, err + } +} + +func OrganizationsAccessStatus(conn *servicecatalog.ServiceCatalog) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.GetAWSOrganizationsAccessStatusInput{} + + output, err := conn.GetAWSOrganizationsAccessStatus(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + + return nil, OrganizationAccessStatusError, fmt.Errorf("error getting Organizations Access: %w", err) + } + + if output == nil { + return nil, StatusUnavailable, fmt.Errorf("error getting Organizations Access: empty response") + } + + return output, aws.StringValue(output.AccessStatus), err + } +} + +func ConstraintStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeConstraintInput{ + Id: aws.String(id), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + output, err := conn.DescribeConstraint(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("constraint not found (accept language %s, ID: %s): %s", acceptLanguage, id, err), + } + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing constraint: %w", err) + } + + if output == nil || output.ConstraintDetail == nil { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("describing constraint (accept language %s, ID: %s): empty response", acceptLanguage, id), + } + } + + return output, aws.StringValue(output.Status), err + } +} + +func ProductPortfolioAssociationStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ProductPortfolioAssociation(conn, acceptLanguage, portfolioID, productID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("product portfolio association not found (%s): %s", tfservicecatalog.ProductPortfolioAssociationCreateID(acceptLanguage, portfolioID, productID), err), + } + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing product portfolio association: %w", err) + } + + if output == nil { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("finding product portfolio association (%s): empty response", tfservicecatalog.ProductPortfolioAssociationCreateID(acceptLanguage, portfolioID, productID)), + } + } + + return output, servicecatalog.StatusAvailable, err + } +} + +func ServiceActionStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeServiceActionInput{ + Id: aws.String(id), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + output, err := conn.DescribeServiceAction(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing Service Action: %w", err) + } + + if output == nil || output.ServiceActionDetail == nil { + return nil, StatusUnavailable, fmt.Errorf("error describing Service Action: empty Service Action Detail") + } + + return output.ServiceActionDetail, servicecatalog.StatusAvailable, nil + } +} + +func BudgetResourceAssociationStatus(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.BudgetResourceAssociation(conn, budgetName, resourceID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("tag option resource association not found (%s): %s", tfservicecatalog.BudgetResourceAssociationID(budgetName, resourceID), err), + } + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing tag option resource association: %w", err) + } + + if output == nil { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("finding tag option resource association (%s): empty response", tfservicecatalog.BudgetResourceAssociationID(budgetName, resourceID)), + } + } + + return output, servicecatalog.StatusAvailable, err + } +} + +func TagOptionResourceAssociationStatus(conn *servicecatalog.ServiceCatalog, tagOptionID, resourceID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.TagOptionResourceAssociation(conn, tagOptionID, resourceID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("tag option resource association not found (%s): %s", tfservicecatalog.TagOptionResourceAssociationID(tagOptionID, resourceID), err), + } + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing tag option resource association: %w", err) + } + + if output == nil { + return nil, StatusNotFound, &resource.NotFoundError{ + Message: fmt.Sprintf("finding tag option resource association (%s): empty response", tfservicecatalog.TagOptionResourceAssociationID(tagOptionID, resourceID)), + } + } + + return output, servicecatalog.StatusAvailable, err + } +} + +func ProvisioningArtifactStatus(conn *servicecatalog.ServiceCatalog, id, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeProvisioningArtifactInput{ + ProvisioningArtifactId: aws.String(id), + ProductId: aws.String(productID), + } + + output, err := conn.DescribeProvisioningArtifact(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + if output == nil || output.ProvisioningArtifactDetail == nil { + return nil, StatusUnavailable, err + } + + return output, aws.StringValue(output.Status), err + } +} + +func PrincipalPortfolioAssociationStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.PrincipalPortfolioAssociation(conn, acceptLanguage, principalARN, portfolioID) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, fmt.Errorf("error describing principal portfolio association: %w", err) + } + + if output == nil { + return nil, StatusNotFound, err + } + + return output, servicecatalog.StatusAvailable, err + } +} + +func LaunchPathsStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.ListLaunchPathsInput{ + AcceptLanguage: aws.String(acceptLanguage), + ProductId: aws.String(productID), + } + + var summaries []*servicecatalog.LaunchPathSummary + + err := conn.ListLaunchPathsPages(input, func(page *servicecatalog.ListLaunchPathsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, summary := range page.LaunchPathSummaries { + if summary == nil { + continue + } + + summaries = append(summaries, summary) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, nil + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + return summaries, servicecatalog.StatusAvailable, err + } +} + +func ProvisionedProductStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeProvisionedProductInput{} + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + // one or the other but not both + if id != "" { + input.Id = aws.String(id) + } else if name != "" { + input.Name = aws.String(name) + } + + output, err := conn.DescribeProvisionedProduct(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + if output == nil || output.ProvisionedProductDetail == nil { + return nil, StatusNotFound, err + } + + return output, aws.StringValue(output.ProvisionedProductDetail.Status), err + } +} + +func RecordStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.DescribeRecordInput{ + Id: aws.String(id), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + output, err := conn.DescribeRecord(input) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, err + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + if output == nil || output.RecordDetail == nil { + return nil, StatusNotFound, err + } + + return output, aws.StringValue(output.RecordDetail.Status), err + } +} + +func PortfolioConstraintsStatus(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &servicecatalog.ListConstraintsForPortfolioInput{ + PortfolioId: aws.String(portfolioID), + } + + if acceptLanguage != "" { + input.AcceptLanguage = aws.String(acceptLanguage) + } + + if productID != "" { + input.ProductId = aws.String(productID) + } + + var output []*servicecatalog.ConstraintDetail + + err := conn.ListConstraintsForPortfolioPages(input, func(page *servicecatalog.ListConstraintsForPortfolioOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, deet := range page.ConstraintDetails { + if deet == nil { + continue + } + + output = append(output, deet) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, StatusNotFound, nil + } + + if err != nil { + return nil, servicecatalog.StatusFailed, err + } + + return output, servicecatalog.StatusAvailable, err + } +} diff --git a/aws/internal/service/servicecatalog/waiter/waiter.go b/aws/internal/service/servicecatalog/waiter/waiter.go new file mode 100644 index 000000000000..e91282a15322 --- /dev/null +++ b/aws/internal/service/servicecatalog/waiter/waiter.go @@ -0,0 +1,562 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicecatalog" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + ProductReadyTimeout = 3 * time.Minute + ProductDeleteTimeout = 3 * time.Minute + + TagOptionReadyTimeout = 3 * time.Minute + TagOptionDeleteTimeout = 3 * time.Minute + + PortfolioShareCreateTimeout = 3 * time.Minute + + OrganizationsAccessStableTimeout = 3 * time.Minute + ConstraintReadyTimeout = 3 * time.Minute + ConstraintDeleteTimeout = 3 * time.Minute + + ProductPortfolioAssociationReadyTimeout = 3 * time.Minute + ProductPortfolioAssociationDeleteTimeout = 3 * time.Minute + + ServiceActionReadyTimeout = 3 * time.Minute + ServiceActionDeleteTimeout = 3 * time.Minute + + BudgetResourceAssociationReadyTimeout = 3 * time.Minute + BudgetResourceAssociationDeleteTimeout = 3 * time.Minute + + TagOptionResourceAssociationReadyTimeout = 3 * time.Minute + TagOptionResourceAssociationDeleteTimeout = 3 * time.Minute + + ProvisioningArtifactReadyTimeout = 3 * time.Minute + ProvisioningArtifactDeletedTimeout = 3 * time.Minute + + PrincipalPortfolioAssociationReadyTimeout = 3 * time.Minute + PrincipalPortfolioAssociationDeleteTimeout = 3 * time.Minute + + LaunchPathsReadyTimeout = 3 * time.Minute + + ProvisionedProductReadyTimeout = 30 * time.Minute + ProvisionedProductUpdateTimeout = 30 * time.Minute + ProvisionedProductDeleteTimeout = 30 * time.Minute + + RecordReadyTimeout = 30 * time.Minute + + PortfolioConstraintsReadyTimeout = 3 * time.Minute + + MinTimeout = 2 * time.Second + NotFoundChecks = 5 + ContinuousTargetOccurrence = 2 + + StatusNotFound = "NOT_FOUND" + StatusUnavailable = "UNAVAILABLE" + + // AWS documentation is wrong, says that status will be "AVAILABLE" but it is actually "CREATED" + StatusCreated = "CREATED" + + OrganizationAccessStatusError = "ERROR" +) + +func ProductReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) (*servicecatalog.DescribeProductAsAdminOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable, StatusCreated}, + Refresh: ProductStatus(conn, acceptLanguage, productID), + Timeout: ProductReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeProductAsAdminOutput); ok { + return output, err + } + + return nil, err +} + +func ProductDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) (*servicecatalog.DescribeProductAsAdminOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, servicecatalog.StatusAvailable, StatusCreated, StatusUnavailable}, + Target: []string{StatusNotFound}, + Refresh: ProductStatus(conn, acceptLanguage, productID), + Timeout: ProductDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil, nil + } + + return nil, err +} + +func TagOptionReady(conn *servicecatalog.ServiceCatalog, id string) (*servicecatalog.TagOptionDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: TagOptionStatus(conn, id), + Timeout: TagOptionReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.TagOptionDetail); ok { + return output, err + } + + return nil, err +} + +func TagOptionDeleted(conn *servicecatalog.ServiceCatalog, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: TagOptionStatus(conn, id), + Timeout: TagOptionDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + return err +} + +func PortfolioShareReady(conn *servicecatalog.ServiceCatalog, portfolioID, shareType, principalID string, acceptRequired bool) (*servicecatalog.PortfolioShareDetail, error) { + targets := []string{servicecatalog.ShareStatusCompleted} + + if !acceptRequired { + targets = append(targets, servicecatalog.ShareStatusInProgress) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.ShareStatusNotStarted, servicecatalog.ShareStatusInProgress, StatusNotFound, StatusUnavailable}, + Target: targets, + Refresh: PortfolioShareStatus(conn, portfolioID, shareType, principalID), + Timeout: PortfolioShareCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.PortfolioShareDetail); ok { + return output, err + } + + return nil, err +} + +func PortfolioShareCreatedWithToken(conn *servicecatalog.ServiceCatalog, token string, acceptRequired bool) (*servicecatalog.DescribePortfolioShareStatusOutput, error) { + targets := []string{servicecatalog.ShareStatusCompleted} + + if !acceptRequired { + targets = append(targets, servicecatalog.ShareStatusInProgress) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.ShareStatusNotStarted, servicecatalog.ShareStatusInProgress, StatusNotFound, StatusUnavailable}, + Target: targets, + Refresh: PortfolioShareStatusWithToken(conn, token), + Timeout: PortfolioShareCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribePortfolioShareStatusOutput); ok { + return output, err + } + + return nil, err +} + +func PortfolioShareDeleted(conn *servicecatalog.ServiceCatalog, portfolioID, shareType, principalID string) (*servicecatalog.PortfolioShareDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.ShareStatusNotStarted, servicecatalog.ShareStatusInProgress, servicecatalog.ShareStatusCompleted, StatusUnavailable}, + Target: []string{StatusNotFound}, + Refresh: PortfolioShareStatus(conn, portfolioID, shareType, principalID), + Timeout: PortfolioShareCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if tfresource.NotFound(err) { + return nil, nil + } + + if output, ok := outputRaw.(*servicecatalog.PortfolioShareDetail); ok { + return output, err + } + + return nil, err +} + +func PortfolioShareDeletedWithToken(conn *servicecatalog.ServiceCatalog, token string) (*servicecatalog.DescribePortfolioShareStatusOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.ShareStatusNotStarted, servicecatalog.ShareStatusInProgress, StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.ShareStatusCompleted}, + Refresh: PortfolioShareStatusWithToken(conn, token), + Timeout: PortfolioShareCreateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribePortfolioShareStatusOutput); ok { + return output, err + } + + return nil, err +} + +func OrganizationsAccessStable(conn *servicecatalog.ServiceCatalog) (string, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.AccessStatusUnderChange, StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.AccessStatusEnabled, servicecatalog.AccessStatusDisabled}, + Refresh: OrganizationsAccessStatus(conn), + Timeout: OrganizationsAccessStableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.GetAWSOrganizationsAccessStatusOutput); ok { + return aws.StringValue(output.AccessStatus), err + } + + return "", err +} + +func ConstraintReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) (*servicecatalog.DescribeConstraintOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, servicecatalog.StatusCreating, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: ConstraintStatus(conn, acceptLanguage, id), + Timeout: ConstraintReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeConstraintOutput); ok { + return output, err + } + + return nil, err +} + +func ConstraintDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable, servicecatalog.StatusCreating}, + Target: []string{StatusNotFound}, + Refresh: ConstraintStatus(conn, acceptLanguage, id), + Timeout: ConstraintDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ProductPortfolioAssociationReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) (*servicecatalog.PortfolioDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: ProductPortfolioAssociationStatus(conn, acceptLanguage, portfolioID, productID), + Timeout: ProductPortfolioAssociationReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.PortfolioDetail); ok { + return output, err + } + + return nil, err +} + +func ProductPortfolioAssociationDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: ProductPortfolioAssociationStatus(conn, acceptLanguage, portfolioID, productID), + Timeout: ProductPortfolioAssociationDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ServiceActionReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) (*servicecatalog.ServiceActionDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: ServiceActionStatus(conn, acceptLanguage, id), + Timeout: ServiceActionReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.ServiceActionDetail); ok { + return output, err + } + + return nil, err +} + +func ServiceActionDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: ServiceActionStatus(conn, acceptLanguage, id), + Timeout: ServiceActionDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + return err +} + +func BudgetResourceAssociationReady(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) (*servicecatalog.BudgetDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: BudgetResourceAssociationStatus(conn, budgetName, resourceID), + Timeout: BudgetResourceAssociationReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.BudgetDetail); ok { + return output, err + } + + return nil, err +} + +func BudgetResourceAssociationDeleted(conn *servicecatalog.ServiceCatalog, budgetName, resourceID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: BudgetResourceAssociationStatus(conn, budgetName, resourceID), + Timeout: BudgetResourceAssociationDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func TagOptionResourceAssociationReady(conn *servicecatalog.ServiceCatalog, tagOptionID, resourceID string) (*servicecatalog.ResourceDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: TagOptionResourceAssociationStatus(conn, tagOptionID, resourceID), + Timeout: TagOptionResourceAssociationReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.ResourceDetail); ok { + return output, err + } + + return nil, err +} + +func TagOptionResourceAssociationDeleted(conn *servicecatalog.ServiceCatalog, tagOptionID, resourceID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: TagOptionResourceAssociationStatus(conn, tagOptionID, resourceID), + Timeout: TagOptionResourceAssociationDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func ProvisioningArtifactReady(conn *servicecatalog.ServiceCatalog, id, productID string) (*servicecatalog.DescribeProvisioningArtifactOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable, StatusCreated}, + Refresh: ProvisioningArtifactStatus(conn, id, productID), + Timeout: ProvisioningArtifactReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeProvisioningArtifactOutput); ok { + return output, err + } + + return nil, err +} + +func ProvisioningArtifactDeleted(conn *servicecatalog.ServiceCatalog, id, productID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusCreating, servicecatalog.StatusAvailable, StatusCreated, StatusUnavailable}, + Target: []string{StatusNotFound}, + Refresh: ProvisioningArtifactStatus(conn, id, productID), + Timeout: ProvisioningArtifactDeletedTimeout, + } + + _, err := stateConf.WaitForState() + + if tfawserr.ErrCodeEquals(err, servicecatalog.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return err + } + + return nil +} + +func PrincipalPortfolioAssociationReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) (*servicecatalog.Principal, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: PrincipalPortfolioAssociationStatus(conn, acceptLanguage, principalARN, portfolioID), + Timeout: PrincipalPortfolioAssociationReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.Principal); ok { + return output, err + } + + return nil, err +} + +func PrincipalPortfolioAssociationDeleted(conn *servicecatalog.ServiceCatalog, acceptLanguage, principalARN, portfolioID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: PrincipalPortfolioAssociationStatus(conn, acceptLanguage, principalARN, portfolioID), + Timeout: PrincipalPortfolioAssociationDeleteTimeout, + NotFoundChecks: 1, + } + + _, err := stateConf.WaitForState() + + return err +} + +func LaunchPathsReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, productID string) ([]*servicecatalog.LaunchPathSummary, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: LaunchPathsStatus(conn, acceptLanguage, productID), + Timeout: LaunchPathsReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.([]*servicecatalog.LaunchPathSummary); ok { + return output, err + } + + return nil, err +} + +func ProvisionedProductReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id, name string) (*servicecatalog.DescribeProvisionedProductOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable, servicecatalog.ProvisionedProductStatusUnderChange, servicecatalog.ProvisionedProductStatusPlanInProgress}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: ProvisionedProductStatus(conn, acceptLanguage, id, name), + Timeout: ProvisionedProductReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeProvisionedProductOutput); ok { + return output, err + } + + return nil, err +} + +func ProvisionedProductTerminated(conn *servicecatalog.ServiceCatalog, acceptLanguage, id, name string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{servicecatalog.StatusAvailable, servicecatalog.ProvisionedProductStatusUnderChange}, + Target: []string{StatusNotFound, StatusUnavailable}, + Refresh: ProvisionedProductStatus(conn, acceptLanguage, id, name), + Timeout: ProvisionedProductDeleteTimeout, + } + + _, err := stateConf.WaitForState() + + return err +} + +func RecordReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, id string) (*servicecatalog.DescribeRecordOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusUnavailable, servicecatalog.ProvisionedProductStatusUnderChange, servicecatalog.ProvisionedProductStatusPlanInProgress}, + Target: []string{servicecatalog.RecordStatusSucceeded, servicecatalog.StatusAvailable}, + Refresh: RecordStatus(conn, acceptLanguage, id), + Timeout: RecordReadyTimeout, + ContinuousTargetOccurence: ContinuousTargetOccurrence, + NotFoundChecks: NotFoundChecks, + MinTimeout: MinTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*servicecatalog.DescribeRecordOutput); ok { + return output, err + } + + return nil, err +} + +func PortfolioConstraintsReady(conn *servicecatalog.ServiceCatalog, acceptLanguage, portfolioID, productID string) ([]*servicecatalog.ConstraintDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound}, + Target: []string{servicecatalog.StatusAvailable}, + Refresh: PortfolioConstraintsStatus(conn, acceptLanguage, portfolioID, productID), + Timeout: PortfolioConstraintsReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.([]*servicecatalog.ConstraintDetail); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/servicediscovery/finder/finder.go b/aws/internal/service/servicediscovery/finder/finder.go new file mode 100644 index 000000000000..20d772600cdc --- /dev/null +++ b/aws/internal/service/servicediscovery/finder/finder.go @@ -0,0 +1,85 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/servicediscovery" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func InstanceByServiceIDAndInstanceID(conn *servicediscovery.ServiceDiscovery, serviceID, instanceID string) (*servicediscovery.Instance, error) { + input := &servicediscovery.GetInstanceInput{ + InstanceId: aws.String(instanceID), + ServiceId: aws.String(serviceID), + } + + output, err := conn.GetInstance(input) + + if tfawserr.ErrCodeEquals(err, servicediscovery.ErrCodeInstanceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Instance == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Instance, nil +} + +func OperationByID(conn *servicediscovery.ServiceDiscovery, id string) (*servicediscovery.Operation, error) { + input := &servicediscovery.GetOperationInput{ + OperationId: aws.String(id), + } + + output, err := conn.GetOperation(input) + + if tfawserr.ErrCodeEquals(err, servicediscovery.ErrCodeOperationNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Operation == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Operation, nil +} + +func ServiceByID(conn *servicediscovery.ServiceDiscovery, id string) (*servicediscovery.Service, error) { + input := &servicediscovery.GetServiceInput{ + Id: aws.String(id), + } + + output, err := conn.GetService(input) + + if tfawserr.ErrCodeEquals(err, servicediscovery.ErrCodeServiceNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Service == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Service, nil +} diff --git a/aws/internal/service/servicediscovery/waiter/status.go b/aws/internal/service/servicediscovery/waiter/status.go index 6768a31cb13b..200a1a76c302 100644 --- a/aws/internal/service/servicediscovery/waiter/status.go +++ b/aws/internal/service/servicediscovery/waiter/status.go @@ -1,35 +1,26 @@ package waiter import ( - "fmt" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/servicediscovery/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) // OperationStatus fetches the Operation and its Status -func OperationStatus(conn *servicediscovery.ServiceDiscovery, operationID string) resource.StateRefreshFunc { +func OperationStatus(conn *servicediscovery.ServiceDiscovery, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &servicediscovery.GetOperationInput{ - OperationId: aws.String(operationID), - } - - output, err := conn.GetOperation(input) + output, err := finder.OperationByID(conn, id) - if err != nil { - return nil, servicediscovery.OperationStatusFail, err + if tfresource.NotFound(err) { + return nil, "", nil } - // Error messages can also be contained in the response with FAIL status - // "ErrorCode":"CANNOT_CREATE_HOSTED_ZONE", - // "ErrorMessage":"The VPC that you chose, vpc-xxx in region xxx, is already associated with another private hosted zone that has an overlapping name space, xxx.. (Service: AmazonRoute53; Status Code: 400; Error Code: ConflictingDomainExists; Request ID: xxx)" - // "Status":"FAIL", - - if aws.StringValue(output.Operation.Status) == servicediscovery.OperationStatusFail { - return output, servicediscovery.OperationStatusFail, fmt.Errorf("%s: %s", aws.StringValue(output.Operation.ErrorCode), aws.StringValue(output.Operation.ErrorMessage)) + if err != nil { + return nil, "", err } - return output, aws.StringValue(output.Operation.Status), nil + return output, aws.StringValue(output.Status), nil } } diff --git a/aws/internal/service/servicediscovery/waiter/waiter.go b/aws/internal/service/servicediscovery/waiter/waiter.go index 5a6ed7fb2a33..99f4cdd5aa38 100644 --- a/aws/internal/service/servicediscovery/waiter/waiter.go +++ b/aws/internal/service/servicediscovery/waiter/waiter.go @@ -1,10 +1,13 @@ package waiter import ( + "fmt" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -13,7 +16,7 @@ const ( ) // OperationSuccess waits for an Operation to return Success -func OperationSuccess(conn *servicediscovery.ServiceDiscovery, operationID string) (*servicediscovery.GetOperationOutput, error) { +func OperationSuccess(conn *servicediscovery.ServiceDiscovery, operationID string) (*servicediscovery.Operation, error) { stateConf := &resource.StateChangeConf{ Pending: []string{servicediscovery.OperationStatusSubmitted, servicediscovery.OperationStatusPending}, Target: []string{servicediscovery.OperationStatusSuccess}, @@ -23,7 +26,15 @@ func OperationSuccess(conn *servicediscovery.ServiceDiscovery, operationID strin outputRaw, err := stateConf.WaitForState() - if output, ok := outputRaw.(*servicediscovery.GetOperationOutput); ok { + if output, ok := outputRaw.(*servicediscovery.Operation); ok { + // Error messages can also be contained in the response with FAIL status + // "ErrorCode":"CANNOT_CREATE_HOSTED_ZONE", + // "ErrorMessage":"The VPC that you chose, vpc-xxx in region xxx, is already associated with another private hosted zone that has an overlapping name space, xxx.. (Service: AmazonRoute53; Status Code: 400; Error Code: ConflictingDomainExists; Request ID: xxx)" + // "Status":"FAIL", + if status := aws.StringValue(output.Status); status == servicediscovery.OperationStatusFail { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(output.ErrorCode), aws.StringValue(output.ErrorMessage))) + } + return output, err } diff --git a/aws/internal/service/sfn/finder/finder.go b/aws/internal/service/sfn/finder/finder.go new file mode 100644 index 000000000000..b0ffe59db82f --- /dev/null +++ b/aws/internal/service/sfn/finder/finder.go @@ -0,0 +1,36 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sfn" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func StateMachineByARN(conn *sfn.SFN, arn string) (*sfn.DescribeStateMachineOutput, error) { + input := &sfn.DescribeStateMachineInput{ + StateMachineArn: aws.String(arn), + } + + output, err := conn.DescribeStateMachine(input) + + if tfawserr.ErrCodeEquals(err, sfn.ErrCodeStateMachineDoesNotExist) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/sfn/waiter/status.go b/aws/internal/service/sfn/waiter/status.go index 9df5ac17e286..6d208d9a018e 100644 --- a/aws/internal/service/sfn/waiter/status.go +++ b/aws/internal/service/sfn/waiter/status.go @@ -4,25 +4,22 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sfn" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sfn/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -// StateMachineStatus fetches the Operation and its Status func StateMachineStatus(conn *sfn.SFN, stateMachineArn string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &sfn.DescribeStateMachineInput{ - StateMachineArn: aws.String(stateMachineArn), - } + output, err := finder.StateMachineByARN(conn, stateMachineArn) - output, err := conn.DescribeStateMachine(input) + if tfresource.NotFound(err) { + return nil, "", nil + } if err != nil { return nil, "", err } - if output == nil { - return nil, "", nil - } - return output, aws.StringValue(output.Status), nil } } diff --git a/aws/internal/service/sfn/waiter/waiter.go b/aws/internal/service/sfn/waiter/waiter.go index 7306c4783149..422a5dcaf8a9 100644 --- a/aws/internal/service/sfn/waiter/waiter.go +++ b/aws/internal/service/sfn/waiter/waiter.go @@ -8,17 +8,17 @@ import ( ) const ( - // Maximum amount of time to wait for an Operation to return Success - StateMachineDeleteTimeout = 5 * time.Minute + StateMachineCreatedTimeout = 5 * time.Minute + StateMachineDeletedTimeout = 5 * time.Minute + StateMachineUpdatedTimeout = 1 * time.Minute ) -// StateMachineDeleted waits for an Operation to return Success func StateMachineDeleted(conn *sfn.SFN, stateMachineArn string) (*sfn.DescribeStateMachineOutput, error) { stateConf := &resource.StateChangeConf{ Pending: []string{sfn.StateMachineStatusActive, sfn.StateMachineStatusDeleting}, Target: []string{}, Refresh: StateMachineStatus(conn, stateMachineArn), - Timeout: StateMachineDeleteTimeout, + Timeout: StateMachineDeletedTimeout, } outputRaw, err := stateConf.WaitForState() diff --git a/aws/internal/service/sns/consts.go b/aws/internal/service/sns/consts.go new file mode 100644 index 000000000000..478110c1243b --- /dev/null +++ b/aws/internal/service/sns/consts.go @@ -0,0 +1,5 @@ +package sns + +const ( + FifoTopicNameSuffix = ".fifo" +) diff --git a/aws/internal/service/sns/finder/finder.go b/aws/internal/service/sns/finder/finder.go new file mode 100644 index 000000000000..79628328ebfa --- /dev/null +++ b/aws/internal/service/sns/finder/finder.go @@ -0,0 +1,26 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" +) + +func SubscriptionByARN(conn *sns.SNS, id string) (*sns.GetSubscriptionAttributesOutput, error) { + output, err := conn.GetSubscriptionAttributes(&sns.GetSubscriptionAttributesInput{ + SubscriptionArn: aws.String(id), + }) + if tfawserr.ErrCodeEquals(err, sns.ErrCodeNotFoundException) { + return nil, nil + } + + if err != nil { + return nil, err + } + + if output == nil || output.Attributes == nil || len(output.Attributes) == 0 { + return nil, nil + } + + return output, nil +} diff --git a/aws/internal/service/sns/waiter/status.go b/aws/internal/service/sns/waiter/status.go new file mode 100644 index 000000000000..45e2f163327f --- /dev/null +++ b/aws/internal/service/sns/waiter/status.go @@ -0,0 +1,23 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sns/finder" +) + +func SubscriptionPendingConfirmation(conn *sns.SNS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.SubscriptionByARN(conn, id) + if err != nil { + return nil, "", err + } + + if output == nil { + return nil, "", nil + } + + return output, aws.StringValue(output.Attributes["PendingConfirmation"]), nil + } +} diff --git a/aws/internal/service/sns/waiter/waiter.go b/aws/internal/service/sns/waiter/waiter.go new file mode 100644 index 000000000000..97bc53023e76 --- /dev/null +++ b/aws/internal/service/sns/waiter/waiter.go @@ -0,0 +1,47 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + SubscriptionCreateTimeout = 2 * time.Minute + SubscriptionPendingConfirmationTimeout = 2 * time.Minute + SubscriptionDeleteTimeout = 2 * time.Minute +) + +func SubscriptionConfirmed(conn *sns.SNS, id, expectedValue string, timeout time.Duration) (*sns.GetSubscriptionAttributesOutput, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{expectedValue}, + Refresh: SubscriptionPendingConfirmation(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sns.GetSubscriptionAttributesOutput); ok { + return output, err + } + + return nil, err +} + +func SubscriptionDeleted(conn *sns.SNS, id string) (*sns.GetSubscriptionAttributesOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{"false", "true"}, + Target: []string{}, + Refresh: SubscriptionPendingConfirmation(conn, id), + Timeout: SubscriptionDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sns.GetSubscriptionAttributesOutput); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/sqs/consts.go b/aws/internal/service/sqs/consts.go new file mode 100644 index 000000000000..d0c8a08e9c99 --- /dev/null +++ b/aws/internal/service/sqs/consts.go @@ -0,0 +1,42 @@ +package sqs + +const ( + ErrCodeInvalidAction = "InvalidAction" +) + +const ( + FifoQueueNameSuffix = ".fifo" +) + +const ( + DefaultQueueDelaySeconds = 0 + DefaultQueueKmsDataKeyReusePeriodSeconds = 300 + DefaultQueueMaximumMessageSize = 262_144 // 256 KiB. + DefaultQueueMessageRetentionPeriod = 345_600 // 4 days. + DefaultQueueReceiveMessageWaitTimeSeconds = 0 + DefaultQueueVisibilityTimeout = 30 +) + +const ( + DeduplicationScopeMessageGroup = "messageGroup" + DeduplicationScopeQueue = "queue" +) + +func DeduplicationScope_Values() []string { + return []string{ + DeduplicationScopeMessageGroup, + DeduplicationScopeQueue, + } +} + +const ( + FifoThroughputLimitPerMessageGroupId = "perMessageGroupId" + FifoThroughputLimitPerQueue = "perQueue" +) + +func FifoThroughputLimit_Values() []string { + return []string{ + FifoThroughputLimitPerMessageGroupId, + FifoThroughputLimitPerQueue, + } +} diff --git a/aws/internal/service/sqs/finder/finder.go b/aws/internal/service/sqs/finder/finder.go new file mode 100644 index 000000000000..4c8c5892c5c1 --- /dev/null +++ b/aws/internal/service/sqs/finder/finder.go @@ -0,0 +1,75 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func QueueAttributesByURL(conn *sqs.SQS, url string) (map[string]string, error) { + input := &sqs.GetQueueAttributesInput{ + AttributeNames: aws.StringSlice([]string{sqs.QueueAttributeNameAll}), + QueueUrl: aws.String(url), + } + + output, err := conn.GetQueueAttributes(input) + + if tfawserr.ErrCodeEquals(err, sqs.ErrCodeQueueDoesNotExist) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Attributes == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return aws.StringValueMap(output.Attributes), nil +} + +func QueuePolicyByURL(conn *sqs.SQS, url string) (string, error) { + input := &sqs.GetQueueAttributesInput{ + AttributeNames: aws.StringSlice([]string{sqs.QueueAttributeNamePolicy}), + QueueUrl: aws.String(url), + } + + output, err := conn.GetQueueAttributes(input) + + if tfawserr.ErrCodeEquals(err, sqs.ErrCodeQueueDoesNotExist) { + return "", &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return "", err + } + + if output == nil || output.Attributes == nil { + return "", &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + v, ok := output.Attributes[sqs.QueueAttributeNamePolicy] + + if !ok || aws.StringValue(v) == "" { + return "", &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return aws.StringValue(v), nil +} diff --git a/aws/internal/service/sqs/name.go b/aws/internal/service/sqs/name.go new file mode 100644 index 000000000000..9653911ecba6 --- /dev/null +++ b/aws/internal/service/sqs/name.go @@ -0,0 +1,25 @@ +package sqs + +import ( + "fmt" + "net/url" + "strings" +) + +// QueueNameFromURL returns the SQS queue name from the specified URL. +func QueueNameFromURL(u string) (string, error) { + v, err := url.Parse(u) + + if err != nil { + return "", err + } + + // http://sqs.us-west-2.amazonaws.com/123456789012/queueName + parts := strings.Split(v.Path, "/") + + if len(parts) != 3 { + return "", fmt.Errorf("SQS Queue URL (%s) is in the incorrect format", u) + } + + return parts[2], nil +} diff --git a/aws/internal/service/sqs/name_test.go b/aws/internal/service/sqs/name_test.go new file mode 100644 index 000000000000..8d70aac27424 --- /dev/null +++ b/aws/internal/service/sqs/name_test.go @@ -0,0 +1,57 @@ +package sqs + +import ( + "testing" +) + +func TestQueueNameFromURL(t *testing.T) { + testCases := []struct { + Name string + URL string + ExpectedQueueName string + ExpectError bool + }{ + { + Name: "empty URL", + ExpectError: true, + }, + { + Name: "invalid URL", + URL: "---", + ExpectError: true, + }, + { + Name: "too few path parts", + URL: "http://sqs.us-west-2.amazonaws.com", + ExpectError: true, + }, + { + Name: "too many path parts", + URL: "http://sqs.us-west-2.amazonaws.com/123456789012/queueName/extra", + ExpectError: true, + }, + { + Name: "valid URL", + URL: "http://sqs.us-west-2.amazonaws.com/123456789012/queueName", + ExpectedQueueName: "queueName", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got, err := QueueNameFromURL(testCase.URL) + + if err != nil && !testCase.ExpectError { + t.Errorf("got unexpected error: %s", err) + } + + if err == nil && testCase.ExpectError { + t.Errorf("expected error, but received none") + } + + if got != testCase.ExpectedQueueName { + t.Errorf("got %s, expected %s", got, testCase.ExpectedQueueName) + } + }) + } +} diff --git a/aws/internal/service/sqs/waiter/status.go b/aws/internal/service/sqs/waiter/status.go new file mode 100644 index 000000000000..a7f9210c466a --- /dev/null +++ b/aws/internal/service/sqs/waiter/status.go @@ -0,0 +1,24 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sqs/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func QueueState(conn *sqs.SQS, url string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.QueueAttributesByURL(conn, url) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, queueStateExists, nil + } +} diff --git a/aws/internal/service/sqs/waiter/waiter.go b/aws/internal/service/sqs/waiter/waiter.go new file mode 100644 index 000000000000..0df8bf619ae2 --- /dev/null +++ b/aws/internal/service/sqs/waiter/waiter.go @@ -0,0 +1,127 @@ +package waiter + +import ( + "fmt" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + awspolicy "github.com/jen20/awspolicyequivalence" + tfjson "github.com/terraform-providers/terraform-provider-aws/aws/internal/json" + tfsqs "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sqs" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sqs/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + // Maximum amount of time to wait for SQS queue attribute changes to propagate + // This timeout should not be increased without strong consideration + // as this will negatively impact user experience when configurations + // have incorrect references or permissions. + // Reference: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SetQueueAttributes.html + QueueAttributePropagationTimeout = 1 * time.Minute + + // If you delete a queue, you must wait at least 60 seconds before creating a queue with the same name. + // ReferenceL https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html + QueueCreatedTimeout = 70 * time.Second + + QueueDeletedTimeout = 15 * time.Second + + queueStateExists = "exists" +) + +func QueueAttributesPropagated(conn *sqs.SQS, url string, expected map[string]string) error { + attributesMatch := func(got map[string]string) error { + for k, e := range expected { + g, ok := got[k] + + if !ok { + // Missing attribute equivalent to empty expected value. + if e == "" { + continue + } + + // Backwards compatibility: https://github.com/hashicorp/terraform-provider-aws/issues/19786. + if k == sqs.QueueAttributeNameKmsDataKeyReusePeriodSeconds && e == strconv.Itoa(tfsqs.DefaultQueueKmsDataKeyReusePeriodSeconds) { + continue + } + + return fmt.Errorf("SQS Queue attribute (%s) not available", k) + } + + switch k { + case sqs.QueueAttributeNamePolicy: + equivalent, err := awspolicy.PoliciesAreEquivalent(g, e) + + if err != nil { + return err + } + + if !equivalent { + return fmt.Errorf("SQS Queue policies are not equivalent") + } + case sqs.QueueAttributeNameRedrivePolicy: + if !tfjson.StringsEquivalent(g, e) { + return fmt.Errorf("SQS Queue redrive policies are not equivalent") + } + default: + if g != e { + return fmt.Errorf("SQS Queue attribute (%s) got: %s, expected: %s", k, g, e) + } + } + } + + return nil + } + + var got map[string]string + err := resource.Retry(QueueAttributePropagationTimeout, func() *resource.RetryError { + var err error + + got, err = finder.QueueAttributesByURL(conn, url) + + if err != nil { + return resource.NonRetryableError(err) + } + + err = attributesMatch(got) + + if err != nil { + return resource.RetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + got, err = finder.QueueAttributesByURL(conn, url) + + if err != nil { + return err + } + + err = attributesMatch(got) + } + + if err != nil { + return err + } + + return nil +} + +func QueueDeleted(conn *sqs.SQS, url string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{queueStateExists}, + Target: []string{}, + Refresh: QueueState(conn, url), + Timeout: QueueDeletedTimeout, + + ContinuousTargetOccurence: 3, + } + + _, err := stateConf.WaitForState() + + return err +} diff --git a/aws/internal/service/ssm/finder/finder.go b/aws/internal/service/ssm/finder/finder.go new file mode 100644 index 000000000000..66f938b8a2e4 --- /dev/null +++ b/aws/internal/service/ssm/finder/finder.go @@ -0,0 +1,61 @@ +package finder + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" +) + +// DocumentByName returns the Document corresponding to the specified name. +func DocumentByName(conn *ssm.SSM, name string) (*ssm.DocumentDescription, error) { + input := &ssm.DescribeDocumentInput{ + Name: aws.String(name), + } + + output, err := conn.DescribeDocument(input) + if err != nil { + return nil, err + } + + if output == nil || output.Document == nil { + return nil, fmt.Errorf("error describing SSM Document (%s): empty result", name) + } + + doc := output.Document + + if aws.StringValue(doc.Status) == ssm.DocumentStatusFailed { + return nil, fmt.Errorf("Document is in a failed state: %s", aws.StringValue(doc.StatusInformation)) + } + + return output.Document, nil +} + +// PatchGroup returns matching SSM Patch Group by Patch Group and BaselineId. +func PatchGroup(conn *ssm.SSM, patchGroup, baselineId string) (*ssm.PatchGroupPatchBaselineMapping, error) { + input := &ssm.DescribePatchGroupsInput{} + var result *ssm.PatchGroupPatchBaselineMapping + + err := conn.DescribePatchGroupsPages(input, func(page *ssm.DescribePatchGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, mapping := range page.Mappings { + if mapping == nil { + continue + } + + if aws.StringValue(mapping.PatchGroup) == patchGroup { + if mapping.BaselineIdentity != nil && aws.StringValue(mapping.BaselineIdentity.BaselineId) == baselineId { + result = mapping + return false + } + } + } + + return !lastPage + }) + + return result, err +} diff --git a/aws/internal/service/ssm/waiter/status.go b/aws/internal/service/ssm/waiter/status.go new file mode 100644 index 000000000000..63b19dd26981 --- /dev/null +++ b/aws/internal/service/ssm/waiter/status.go @@ -0,0 +1,29 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ssm/finder" +) + +const ( + DocumentStatusUnknown = "Unknown" +) + +// DocumentStatus fetches the Document and its Status +func DocumentStatus(conn *ssm.SSM, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DocumentByName(conn, name) + + if err != nil { + return nil, ssm.DocumentStatusFailed, err + } + + if output == nil { + return output, DocumentStatusUnknown, nil + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/ssm/waiter/waiter.go b/aws/internal/service/ssm/waiter/waiter.go new file mode 100644 index 000000000000..bc4f820aa51d --- /dev/null +++ b/aws/internal/service/ssm/waiter/waiter.go @@ -0,0 +1,49 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + DocumentDeleteTimeout = 2 * time.Minute + DocumentActiveTimeout = 2 * time.Minute +) + +// DocumentDeleted waits for an Document to return Deleted +func DocumentDeleted(conn *ssm.SSM, name string) (*ssm.DocumentDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ssm.DocumentStatusDeleting}, + Target: []string{}, + Refresh: DocumentStatus(conn, name), + Timeout: DocumentDeleteTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ssm.DocumentDescription); ok { + return output, err + } + + return nil, err +} + +// DocumentActive waits for an Document to return Active +func DocumentActive(conn *ssm.SSM, name string) (*ssm.DocumentDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ssm.DocumentStatusCreating, ssm.DocumentStatusUpdating}, + Target: []string{ssm.DocumentStatusActive}, + Refresh: DocumentStatus(conn, name), + Timeout: DocumentActiveTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ssm.DocumentDescription); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/storagegateway/enum.go b/aws/internal/service/storagegateway/enum.go new file mode 100644 index 000000000000..a60d1901ab78 --- /dev/null +++ b/aws/internal/service/storagegateway/enum.go @@ -0,0 +1,70 @@ +package storagegateway + +import "time" + +const ( + AuthenticationActiveDirectory = "ActiveDirectory" + AuthenticationGuestAccess = "GuestAccess" +) + +func Authentication_Values() []string { + return []string{ + AuthenticationActiveDirectory, + AuthenticationGuestAccess, + } +} + +const ( + DefaultStorageClassS3IntelligentTiering = "S3_INTELLIGENT_TIERING" + DefaultStorageClassS3OneZoneIA = "S3_ONEZONE_IA" + DefaultStorageClassS3Standard = "S3_STANDARD" + DefaultStorageClassS3StandardIA = "S3_STANDARD_IA" +) + +func DefaultStorageClass_Values() []string { + return []string{ + DefaultStorageClassS3IntelligentTiering, + DefaultStorageClassS3OneZoneIA, + DefaultStorageClassS3Standard, + DefaultStorageClassS3StandardIA, + } +} + +const ( + FileShareStatusAvailable = "AVAILABLE" + FileShareStatusCreating = "CREATING" + FileShareStatusDeleting = "DELETING" + FileShareStatusForceDeleting = "FORCE_DELETING" + FileShareStatusUpdating = "UPDATING" +) + +const ( + FileSystemAssociationCreateTimeout = 3 * time.Minute + FileSystemAssociationUpdateTimeout = 3 * time.Minute + FileSystemAssociationDeleteTimeout = 3 * time.Minute +) + +const ( + FileSystemAssociationStatusAvailable = "AVAILABLE" + FileSystemAssociationStatusCreating = "CREATING" + FileSystemAssociationStatusDeleting = "DELETING" + FileSystemAssociationStatusForceDeleting = "FORCE_DELETING" + FileSystemAssociationStatusUpdating = "UPDATING" + FileSystemAssociationStatusError = "ERROR" +) + +func FileSystemAssociationStatusAvailableStatusPending() []string { + return []string{FileSystemAssociationStatusCreating, FileSystemAssociationStatusUpdating} +} + +func FileSystemAssociationStatusAvailableStatusTarget() []string { + return []string{FileSystemAssociationStatusAvailable} +} + +func FileSystemAssociationStatusDeletedStatusPending() []string { + return []string{FileSystemAssociationStatusAvailable, FileSystemAssociationStatusDeleting, FileSystemAssociationStatusForceDeleting} +} + +func FileSystemAssociationStatusDeletedStatusTarget() []string { + return []string{} +} diff --git a/aws/internal/service/storagegateway/errors.go b/aws/internal/service/storagegateway/errors.go new file mode 100644 index 000000000000..4fcedce8859e --- /dev/null +++ b/aws/internal/service/storagegateway/errors.go @@ -0,0 +1,42 @@ +package storagegateway + +import ( + "errors" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/storagegateway" +) + +const ( + OperationErrCodeFileShareNotFound = "FileShareNotFound" + FileSystemAssociationNotFound = "FileSystemAssociationNotFound" +) + +// OperationErrorCode returns the operation error code from the specified error: +// * err is of type awserr.Error and represents a storagegateway.InternalServerError or storagegateway.InvalidGatewayRequestException +// * Error_ is not nil +// See https://docs.aws.amazon.com/storagegateway/latest/userguide/AWSStorageGatewayAPI.html#APIErrorResponses for details. +func OperationErrorCode(err error) string { + if inner := (*storagegateway.InternalServerError)(nil); errors.As(err, &inner) && inner.Error_ != nil { + return aws.StringValue(inner.Error_.ErrorCode) + } + + if inner := (*storagegateway.InvalidGatewayRequestException)(nil); errors.As(err, &inner) && inner.Error_ != nil { + return aws.StringValue(inner.Error_.ErrorCode) + } + + return "" +} + +// Error code constants missing from AWS Go SDK: +// https://docs.aws.amazon.com/sdk-for-go/api/service/storagegateway/#pkg-constants + +func InvalidGatewayRequestErrCodeEquals(err error, errCode string) bool { + var igrex *storagegateway.InvalidGatewayRequestException + if errors.As(err, &igrex) { + if err := igrex.Error_; err != nil { + return aws.StringValue(err.ErrorCode) == errCode + } + } + return false +} diff --git a/aws/internal/service/storagegateway/finder/finder.go b/aws/internal/service/storagegateway/finder/finder.go new file mode 100644 index 000000000000..c0d01922f0ce --- /dev/null +++ b/aws/internal/service/storagegateway/finder/finder.go @@ -0,0 +1,143 @@ +package finder + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/storagegateway" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfstoragegateway "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/storagegateway" +) + +func LocalDiskByDiskId(conn *storagegateway.StorageGateway, gatewayARN string, diskID string) (*storagegateway.Disk, error) { + input := &storagegateway.ListLocalDisksInput{ + GatewayARN: aws.String(gatewayARN), + } + + output, err := conn.ListLocalDisks(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, disk := range output.Disks { + if aws.StringValue(disk.DiskId) == diskID { + return disk, nil + } + } + + return nil, nil +} + +func LocalDiskByDiskPath(conn *storagegateway.StorageGateway, gatewayARN string, diskPath string) (*storagegateway.Disk, error) { + input := &storagegateway.ListLocalDisksInput{ + GatewayARN: aws.String(gatewayARN), + } + + output, err := conn.ListLocalDisks(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, disk := range output.Disks { + if aws.StringValue(disk.DiskPath) == diskPath { + return disk, nil + } + } + + return nil, nil +} + +func UploadBufferDisk(conn *storagegateway.StorageGateway, gatewayARN string, diskID string) (*string, error) { + input := &storagegateway.DescribeUploadBufferInput{ + GatewayARN: aws.String(gatewayARN), + } + + var result *string + + output, err := conn.DescribeUploadBuffer(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + for _, diskId := range output.DiskIds { + if aws.StringValue(diskId) == diskID { + result = diskId + break + } + } + + return result, err +} + +func SMBFileShareByARN(conn *storagegateway.StorageGateway, arn string) (*storagegateway.SMBFileShareInfo, error) { + input := &storagegateway.DescribeSMBFileSharesInput{ + FileShareARNList: aws.StringSlice([]string{arn}), + } + + output, err := conn.DescribeSMBFileShares(input) + + if tfstoragegateway.OperationErrorCode(err) == tfstoragegateway.OperationErrCodeFileShareNotFound { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.SMBFileShareInfoList) == 0 || output.SMBFileShareInfoList[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO Check for multiple results. + // TODO https://github.com/hashicorp/terraform-provider-aws/pull/17613. + return output.SMBFileShareInfoList[0], nil +} + +func FileSystemAssociationByARN(conn *storagegateway.StorageGateway, fileSystemAssociationARN string) (*storagegateway.FileSystemAssociationInfo, error) { + + input := &storagegateway.DescribeFileSystemAssociationsInput{ + FileSystemAssociationARNList: []*string{aws.String(fileSystemAssociationARN)}, + } + log.Printf("[DEBUG] Reading Storage Gateway File System Associations: %s", input) + + output, err := conn.DescribeFileSystemAssociations(input) + if err != nil { + if tfstoragegateway.InvalidGatewayRequestErrCodeEquals(err, tfstoragegateway.FileSystemAssociationNotFound) { + log.Printf("[WARN] Storage Gateway File System Association (%s) not found", fileSystemAssociationARN) + return nil, nil + } + + return nil, fmt.Errorf("error reading Storage Gateway File System Association (%s): %w", fileSystemAssociationARN, err) + } + + if output == nil || len(output.FileSystemAssociationInfoList) == 0 || output.FileSystemAssociationInfoList[0] == nil { + log.Printf("[WARN] Storage Gateway File System Association (%s) not found", fileSystemAssociationARN) + return nil, nil + } + + filesystem := output.FileSystemAssociationInfoList[0] + + return filesystem, nil +} diff --git a/aws/internal/service/storagegateway/waiter/status.go b/aws/internal/service/storagegateway/waiter/status.go index 2d344cfccc85..0bc640ac7ccf 100644 --- a/aws/internal/service/storagegateway/waiter/status.go +++ b/aws/internal/service/storagegateway/waiter/status.go @@ -8,6 +8,8 @@ import ( "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/storagegateway/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -18,6 +20,8 @@ const ( NfsFileShareStatusUnknown = "Unknown" SmbFileShareStatusNotFound = "NotFound" SmbFileShareStatusUnknown = "Unknown" + FileSystemAssociationStatusNotFound = "NotFound" + FileSystemAssociationStatusUnknown = "Unknown" ) func StorageGatewayGatewayStatus(conn *storagegateway.StorageGateway, gatewayARN string) resource.StateRefreshFunc { @@ -111,27 +115,37 @@ func NfsFileShareStatus(conn *storagegateway.StorageGateway, fileShareArn string } } -func SmbFileShareStatus(conn *storagegateway.StorageGateway, fileShareArn string) resource.StateRefreshFunc { +func SMBFileShareStatus(conn *storagegateway.StorageGateway, arn string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - input := &storagegateway.DescribeSMBFileSharesInput{ - FileShareARNList: []*string{aws.String(fileShareArn)}, + output, err := finder.SMBFileShareByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil } - log.Printf("[DEBUG] Reading Storage Gateway SMB File Share: %s", input) - output, err := conn.DescribeSMBFileShares(input) if err != nil { - if tfawserr.ErrMessageContains(err, storagegateway.ErrCodeInvalidGatewayRequestException, "The specified file share was not found.") { - return nil, SmbFileShareStatusNotFound, nil - } - return nil, SmbFileShareStatusUnknown, fmt.Errorf("error reading Storage Gateway SMB File Share: %w", err) + return nil, "", err } - if output == nil || len(output.SMBFileShareInfoList) == 0 || output.SMBFileShareInfoList[0] == nil { - return nil, SmbFileShareStatusNotFound, nil + return output, aws.StringValue(output.FileShareStatus), nil + } +} + +func FileSystemAssociationStatus(conn *storagegateway.StorageGateway, fileSystemArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + output, err := finder.FileSystemAssociationByARN(conn, fileSystemArn) + + // there was an unhandled error in the Finder + if err != nil { + return nil, "", err } - fileshare := output.SMBFileShareInfoList[0] + // no error, and no File System Association found + if output == nil { + return nil, FileSystemAssociationStatusNotFound, nil + } - return fileshare, aws.StringValue(fileshare.FileShareStatus), nil + return output, aws.StringValue(output.FileSystemAssociationStatus), nil } } diff --git a/aws/internal/service/storagegateway/waiter/waiter.go b/aws/internal/service/storagegateway/waiter/waiter.go index 881ae65c914b..380a8bf4fe01 100644 --- a/aws/internal/service/storagegateway/waiter/waiter.go +++ b/aws/internal/service/storagegateway/waiter/waiter.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/service/storagegateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfstoragegateway "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/storagegateway" ) const ( @@ -16,6 +17,8 @@ const ( NfsFileShareDeletedDelay = 5 * time.Second SmbFileShareAvailableDelay = 5 * time.Second SmbFileShareDeletedDelay = 5 * time.Second + FileSystemAssociationAvailableDelay = 5 * time.Second + FileSystemAssociationDeletedDelay = 5 * time.Second ) func StorageGatewayGatewayConnected(conn *storagegateway.StorageGateway, gatewayARN string, timeout time.Duration) (*storagegateway.DescribeGatewayInformationOutput, error) { @@ -111,12 +114,11 @@ func NfsFileShareDeleted(conn *storagegateway.StorageGateway, fileShareArn strin return nil, err } -// SmbFileShareAvailable waits for a SMB File Share to return Available -func SmbFileShareAvailable(conn *storagegateway.StorageGateway, fileShareArn string, timeout time.Duration) (*storagegateway.SMBFileShareInfo, error) { +func SMBFileShareCreated(conn *storagegateway.StorageGateway, arn string, timeout time.Duration) (*storagegateway.SMBFileShareInfo, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{"CREATING", "UPDATING"}, - Target: []string{"AVAILABLE"}, - Refresh: SmbFileShareStatus(conn, fileShareArn), + Pending: []string{tfstoragegateway.FileShareStatusCreating}, + Target: []string{tfstoragegateway.FileShareStatusAvailable}, + Refresh: SMBFileShareStatus(conn, arn), Timeout: timeout, Delay: SmbFileShareAvailableDelay, } @@ -130,11 +132,11 @@ func SmbFileShareAvailable(conn *storagegateway.StorageGateway, fileShareArn str return nil, err } -func SmbFileShareDeleted(conn *storagegateway.StorageGateway, fileShareArn string, timeout time.Duration) (*storagegateway.SMBFileShareInfo, error) { +func SMBFileShareDeleted(conn *storagegateway.StorageGateway, arn string, timeout time.Duration) (*storagegateway.SMBFileShareInfo, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{"AVAILABLE", "DELETING", "FORCE_DELETING"}, + Pending: []string{tfstoragegateway.FileShareStatusAvailable, tfstoragegateway.FileShareStatusDeleting, tfstoragegateway.FileShareStatusForceDeleting}, Target: []string{}, - Refresh: SmbFileShareStatus(conn, fileShareArn), + Refresh: SMBFileShareStatus(conn, arn), Timeout: timeout, Delay: SmbFileShareDeletedDelay, NotFoundChecks: 1, @@ -148,3 +150,59 @@ func SmbFileShareDeleted(conn *storagegateway.StorageGateway, fileShareArn strin return nil, err } + +func SMBFileShareUpdated(conn *storagegateway.StorageGateway, arn string, timeout time.Duration) (*storagegateway.SMBFileShareInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfstoragegateway.FileShareStatusUpdating}, + Target: []string{tfstoragegateway.FileShareStatusAvailable}, + Refresh: SMBFileShareStatus(conn, arn), + Timeout: timeout, + Delay: SmbFileShareAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*storagegateway.SMBFileShareInfo); ok { + return output, err + } + + return nil, err +} + +// FileSystemAssociationAvailable waits for a File System Association to return Available +func FileSystemAssociationAvailable(conn *storagegateway.StorageGateway, fileSystemArn string, timeout time.Duration) (*storagegateway.FileSystemAssociationInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: tfstoragegateway.FileSystemAssociationStatusAvailableStatusPending(), + Target: tfstoragegateway.FileSystemAssociationStatusAvailableStatusTarget(), + Refresh: FileSystemAssociationStatus(conn, fileSystemArn), + Timeout: timeout, + Delay: FileSystemAssociationAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*storagegateway.FileSystemAssociationInfo); ok { + return output, err + } + + return nil, err +} + +func FileSystemAssociationDeleted(conn *storagegateway.StorageGateway, fileSystemArn string, timeout time.Duration) (*storagegateway.FileSystemAssociationInfo, error) { + stateConf := &resource.StateChangeConf{ + Pending: tfstoragegateway.FileSystemAssociationStatusDeletedStatusPending(), + Target: tfstoragegateway.FileSystemAssociationStatusDeletedStatusTarget(), + Refresh: FileSystemAssociationStatus(conn, fileSystemArn), + Timeout: timeout, + Delay: FileSystemAssociationDeletedDelay, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*storagegateway.FileSystemAssociationInfo); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/sts/finder/finder.go b/aws/internal/service/sts/finder/finder.go new file mode 100644 index 000000000000..3ec4da3ef740 --- /dev/null +++ b/aws/internal/service/sts/finder/finder.go @@ -0,0 +1,25 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/service/sts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func CallerIdentity(conn *sts.STS) (*sts.GetCallerIdentityOutput, error) { + input := &sts.GetCallerIdentityInput{} + + output, err := conn.GetCallerIdentity(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/synthetics/finder/finder.go b/aws/internal/service/synthetics/finder/finder.go index c7ed5179088a..2fac465e0882 100644 --- a/aws/internal/service/synthetics/finder/finder.go +++ b/aws/internal/service/synthetics/finder/finder.go @@ -3,18 +3,34 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -// CanaryByName returns the Canary corresponding to the specified Name. -func CanaryByName(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { +func CanaryByName(conn *synthetics.Synthetics, name string) (*synthetics.Canary, error) { input := &synthetics.GetCanaryInput{ Name: aws.String(name), } output, err := conn.GetCanary(input) + + if tfawserr.ErrCodeEquals(err, synthetics.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } - return output, nil + if output == nil || output.Canary == nil || output.Canary.Status == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Canary, nil } diff --git a/aws/internal/service/synthetics/waiter/status.go b/aws/internal/service/synthetics/waiter/status.go index b25b869f6ba6..8b97980642fb 100644 --- a/aws/internal/service/synthetics/waiter/status.go +++ b/aws/internal/service/synthetics/waiter/status.go @@ -1,26 +1,25 @@ package waiter import ( - "fmt" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/synthetics" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/synthetics/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -func CanaryStatus(conn *synthetics.Synthetics, name string) resource.StateRefreshFunc { +func CanaryState(conn *synthetics.Synthetics, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { output, err := finder.CanaryByName(conn, name) - if err != nil { - return nil, synthetics.CanaryStateError, err + if tfresource.NotFound(err) { + return nil, "", nil } - if aws.StringValue(output.Canary.Status.State) == synthetics.CanaryStateError { - return output, synthetics.CanaryStateError, fmt.Errorf("%s: %s", aws.StringValue(output.Canary.Status.StateReasonCode), aws.StringValue(output.Canary.Status.StateReason)) + if err != nil { + return nil, "", err } - return output, aws.StringValue(output.Canary.Status.State), nil + return output, aws.StringValue(output.Status.State), nil } } diff --git a/aws/internal/service/synthetics/waiter/waiter.go b/aws/internal/service/synthetics/waiter/waiter.go index 39501896d6f0..b8fd35b2c69e 100644 --- a/aws/internal/service/synthetics/waiter/waiter.go +++ b/aws/internal/service/synthetics/waiter/waiter.go @@ -1,89 +1,112 @@ package waiter import ( + "fmt" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/synthetics" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - // Maximum amount of time to wait for a Canary to return Ready CanaryCreatedTimeout = 5 * time.Minute CanaryRunningTimeout = 5 * time.Minute CanaryStoppedTimeout = 5 * time.Minute CanaryDeletedTimeout = 5 * time.Minute ) -// CanaryReady waits for a Canary to return Ready -func CanaryReady(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { +func CanaryReady(conn *synthetics.Synthetics, name string) (*synthetics.Canary, error) { stateConf := &resource.StateChangeConf{ Pending: []string{synthetics.CanaryStateCreating, synthetics.CanaryStateUpdating}, Target: []string{synthetics.CanaryStateReady}, - Refresh: CanaryStatus(conn, name), + Refresh: CanaryState(conn, name), Timeout: CanaryCreatedTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { - return v, err + if output, ok := outputRaw.(*synthetics.Canary); ok { + if status := output.Status; aws.StringValue(status.State) == synthetics.CanaryStateError { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(status.StateReasonCode), aws.StringValue(status.StateReason))) + } + + return output, err } return nil, err } -// CanaryReady waits for a Canary to return Stopped -func CanaryStopped(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { +func CanaryStopped(conn *synthetics.Synthetics, name string) (*synthetics.Canary, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{synthetics.CanaryStateStopping, synthetics.CanaryStateUpdating, - synthetics.CanaryStateRunning, synthetics.CanaryStateReady, synthetics.CanaryStateStarting}, + Pending: []string{ + synthetics.CanaryStateStopping, + synthetics.CanaryStateUpdating, + synthetics.CanaryStateRunning, + synthetics.CanaryStateReady, + synthetics.CanaryStateStarting, + }, Target: []string{synthetics.CanaryStateStopped}, - Refresh: CanaryStatus(conn, name), + Refresh: CanaryState(conn, name), Timeout: CanaryStoppedTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { - return v, err + if output, ok := outputRaw.(*synthetics.Canary); ok { + if status := output.Status; aws.StringValue(status.State) == synthetics.CanaryStateError { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(status.StateReasonCode), aws.StringValue(status.StateReason))) + } + + return output, err } return nil, err } -// CanaryReady waits for a Canary to return Running -func CanaryRunning(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { +func CanaryRunning(conn *synthetics.Synthetics, name string) (*synthetics.Canary, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{synthetics.CanaryStateStarting, synthetics.CanaryStateUpdating, - synthetics.CanaryStateStarting, synthetics.CanaryStateReady}, + Pending: []string{ + synthetics.CanaryStateStarting, + synthetics.CanaryStateUpdating, + synthetics.CanaryStateStarting, + synthetics.CanaryStateReady, + }, Target: []string{synthetics.CanaryStateRunning}, - Refresh: CanaryStatus(conn, name), + Refresh: CanaryState(conn, name), Timeout: CanaryRunningTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { - return v, err + if output, ok := outputRaw.(*synthetics.Canary); ok { + if status := output.Status; aws.StringValue(status.State) == synthetics.CanaryStateError { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(status.StateReasonCode), aws.StringValue(status.StateReason))) + } + + return output, err } return nil, err } -// CanaryReady waits for a Canary to return Ready -func CanaryDeleted(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { +func CanaryDeleted(conn *synthetics.Synthetics, name string) (*synthetics.Canary, error) { stateConf := &resource.StateChangeConf{ Pending: []string{synthetics.CanaryStateDeleting, synthetics.CanaryStateStopped}, Target: []string{}, - Refresh: CanaryStatus(conn, name), + Refresh: CanaryState(conn, name), Timeout: CanaryDeletedTimeout, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { - return v, err + if output, ok := outputRaw.(*synthetics.Canary); ok { + if status := output.Status; aws.StringValue(status.State) == synthetics.CanaryStateError { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(status.StateReasonCode), aws.StringValue(status.StateReason))) + } + + return output, err } return nil, err diff --git a/aws/internal/service/transfer/enum.go b/aws/internal/service/transfer/enum.go new file mode 100644 index 000000000000..880b435fa838 --- /dev/null +++ b/aws/internal/service/transfer/enum.go @@ -0,0 +1,15 @@ +package transfer + +const ( + SecurityPolicyName2018_11 = "TransferSecurityPolicy-2018-11" + SecurityPolicyName2020_06 = "TransferSecurityPolicy-2020-06" + SecurityPolicyNameFIPS_2020_06 = "TransferSecurityPolicy-FIPS-2020-06" +) + +func SecurityPolicyName_Values() []string { + return []string{ + SecurityPolicyName2018_11, + SecurityPolicyName2020_06, + SecurityPolicyNameFIPS_2020_06, + } +} diff --git a/aws/internal/service/transfer/finder/finder.go b/aws/internal/service/transfer/finder/finder.go new file mode 100644 index 000000000000..c08d31beeee3 --- /dev/null +++ b/aws/internal/service/transfer/finder/finder.go @@ -0,0 +1,86 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func AccessByServerIDAndExternalID(conn *transfer.Transfer, serverID, externalID string) (*transfer.DescribedAccess, error) { + input := &transfer.DescribeAccessInput{ + ExternalId: aws.String(externalID), + ServerId: aws.String(serverID), + } + + output, err := conn.DescribeAccess(input) + + if tfawserr.ErrCodeEquals(err, transfer.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Access == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Access, nil +} + +func ServerByID(conn *transfer.Transfer, id string) (*transfer.DescribedServer, error) { + input := &transfer.DescribeServerInput{ + ServerId: aws.String(id), + } + + output, err := conn.DescribeServer(input) + + if tfawserr.ErrCodeEquals(err, transfer.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Server == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Server, nil +} + +func UserByServerIDAndUserName(conn *transfer.Transfer, serverID, userName string) (*transfer.DescribedUser, error) { + input := &transfer.DescribeUserInput{ + ServerId: aws.String(serverID), + UserName: aws.String(userName), + } + + output, err := conn.DescribeUser(input) + + if tfawserr.ErrCodeEquals(err, transfer.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.User == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.User, nil +} diff --git a/aws/internal/service/transfer/id.go b/aws/internal/service/transfer/id.go new file mode 100644 index 000000000000..53d2e5b498b1 --- /dev/null +++ b/aws/internal/service/transfer/id.go @@ -0,0 +1,44 @@ +package transfer + +import ( + "fmt" + "strings" +) + +const userResourceIDSeparator = "/" + +func UserCreateResourceID(serverID, userName string) string { + parts := []string{serverID, userName} + id := strings.Join(parts, userResourceIDSeparator) + + return id +} + +func UserParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, userResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected SERVERID%[2]sUSERNAME", id, userResourceIDSeparator) +} + +const accessResourceIDSeparator = "/" + +func AccessCreateResourceID(serverID, externalID string) string { + parts := []string{serverID, externalID} + id := strings.Join(parts, accessResourceIDSeparator) + + return id +} + +func AccessParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, accessResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected SERVERID%[2]sEXTERNALID", id, accessResourceIDSeparator) +} diff --git a/aws/internal/service/transfer/waiter/status.go b/aws/internal/service/transfer/waiter/status.go new file mode 100644 index 000000000000..d85474d3ebe6 --- /dev/null +++ b/aws/internal/service/transfer/waiter/status.go @@ -0,0 +1,45 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + userStateExists = "exists" +) + +func ServerState(conn *transfer.Transfer, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ServerByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} + +func UserState(conn *transfer.Transfer, serverID, userName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.UserByServerIDAndUserName(conn, serverID, userName) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, userStateExists, nil + } +} diff --git a/aws/internal/service/transfer/waiter/waiter.go b/aws/internal/service/transfer/waiter/waiter.go new file mode 100644 index 000000000000..1b92f7cecb22 --- /dev/null +++ b/aws/internal/service/transfer/waiter/waiter.go @@ -0,0 +1,98 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ServerDeletedTimeout = 10 * time.Minute + UserDeletedTimeout = 10 * time.Minute +) + +func ServerCreated(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting}, + Target: []string{transfer.StateOnline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerDeleted(conn *transfer.Transfer, id string) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: transfer.State_Values(), + Target: []string{}, + Refresh: ServerState(conn, id), + Timeout: ServerDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerStarted(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOffline, transfer.StateStopping}, + Target: []string{transfer.StateOnline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerStopped(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOnline, transfer.StateStopping}, + Target: []string{transfer.StateOffline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func UserDeleted(conn *transfer.Transfer, serverID, userName string) (*transfer.DescribedUser, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{userStateExists}, + Target: []string{}, + Refresh: UserState(conn, serverID, userName), + Timeout: UserDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedUser); ok { + return output, err + } + + return nil, err +} diff --git a/aws/internal/service/workspaces/finder/finder.go b/aws/internal/service/workspaces/finder/finder.go new file mode 100644 index 000000000000..e3807d675d8c --- /dev/null +++ b/aws/internal/service/workspaces/finder/finder.go @@ -0,0 +1,40 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/workspaces" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func DirectoryByID(conn *workspaces.WorkSpaces, id string) (*workspaces.WorkspaceDirectory, error) { + input := &workspaces.DescribeWorkspaceDirectoriesInput{ + DirectoryIds: aws.StringSlice([]string{id}), + } + + output, err := conn.DescribeWorkspaceDirectories(input) + + if err != nil { + return nil, err + } + + if output == nil || len(output.Directories) == 0 || output.Directories[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + // TODO Check for multiple results. + // TODO https://github.com/hashicorp/terraform-provider-aws/pull/17613. + + directory := output.Directories[0] + + if state := aws.StringValue(directory.State); state == workspaces.WorkspaceDirectoryStateDeregistered { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + return directory, nil +} diff --git a/aws/internal/service/workspaces/lister/list.go b/aws/internal/service/workspaces/lister/list.go new file mode 100644 index 000000000000..322f204aa653 --- /dev/null +++ b/aws/internal/service/workspaces/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeIpGroups -paginator=NextToken github.com/aws/aws-sdk-go/service/workspaces + +package lister diff --git a/aws/internal/service/workspaces/lister/list_pages_gen.go b/aws/internal/service/workspaces/lister/list_pages_gen.go new file mode 100644 index 000000000000..cd966fd5c92a --- /dev/null +++ b/aws/internal/service/workspaces/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeIpGroups -paginator=NextToken github.com/aws/aws-sdk-go/service/workspaces"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/workspaces" +) + +func DescribeIpGroupsPages(conn *workspaces.WorkSpaces, input *workspaces.DescribeIpGroupsInput, fn func(*workspaces.DescribeIpGroupsOutput, bool) bool) error { + return DescribeIpGroupsPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeIpGroupsPagesWithContext(ctx context.Context, conn *workspaces.WorkSpaces, input *workspaces.DescribeIpGroupsInput, fn func(*workspaces.DescribeIpGroupsOutput, bool) bool) error { + for { + output, err := conn.DescribeIpGroupsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/workspaces/waiter/status.go b/aws/internal/service/workspaces/waiter/status.go index 8066543932dc..f6bc8d9e85de 100644 --- a/aws/internal/service/workspaces/waiter/status.go +++ b/aws/internal/service/workspaces/waiter/status.go @@ -4,23 +4,23 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/workspaces" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/workspaces/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -func DirectoryState(conn *workspaces.WorkSpaces, directoryID string) resource.StateRefreshFunc { +func DirectoryState(conn *workspaces.WorkSpaces, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - output, err := conn.DescribeWorkspaceDirectories(&workspaces.DescribeWorkspaceDirectoriesInput{ - DirectoryIds: aws.StringSlice([]string{directoryID}), - }) - if err != nil { - return nil, workspaces.WorkspaceDirectoryStateError, err + output, err := finder.DirectoryByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil } - if len(output.Directories) == 0 { - return output, workspaces.WorkspaceDirectoryStateDeregistered, nil + if err != nil { + return nil, "", err } - directory := output.Directories[0] - return directory, aws.StringValue(directory.State), nil + return output, aws.StringValue(output.State), nil } } diff --git a/aws/internal/service/workspaces/waiter/waiter.go b/aws/internal/service/workspaces/waiter/waiter.go index b8cd3394205e..e79d418c0f6d 100644 --- a/aws/internal/service/workspaces/waiter/waiter.go +++ b/aws/internal/service/workspaces/waiter/waiter.go @@ -8,6 +8,9 @@ import ( ) const ( + DirectoryDeregisterInvalidResourceStateTimeout = 2 * time.Minute + DirectoryRegisterInvalidResourceStateTimeout = 2 * time.Minute + // Maximum amount of time to wait for a Directory to return Registered DirectoryRegisteredTimeout = 10 * time.Minute @@ -29,9 +32,7 @@ const ( func DirectoryRegistered(conn *workspaces.WorkSpaces, directoryID string) (*workspaces.WorkspaceDirectory, error) { stateConf := &resource.StateChangeConf{ - Pending: []string{ - workspaces.WorkspaceDirectoryStateRegistering, - }, + Pending: []string{workspaces.WorkspaceDirectoryStateRegistering}, Target: []string{workspaces.WorkspaceDirectoryStateRegistered}, Refresh: DirectoryState(conn, directoryID), Timeout: DirectoryRegisteredTimeout, @@ -53,9 +54,7 @@ func DirectoryDeregistered(conn *workspaces.WorkSpaces, directoryID string) (*wo workspaces.WorkspaceDirectoryStateRegistered, workspaces.WorkspaceDirectoryStateDeregistering, }, - Target: []string{ - workspaces.WorkspaceDirectoryStateDeregistered, - }, + Target: []string{}, Refresh: DirectoryState(conn, directoryID), Timeout: DirectoryDeregisteredTimeout, } diff --git a/aws/internal/tagresource/README.md b/aws/internal/tagresource/README.md new file mode 100644 index 000000000000..0451b6947423 --- /dev/null +++ b/aws/internal/tagresource/README.md @@ -0,0 +1,13 @@ +# tagresource + +The `tagresource` package is designed to provide a generator and consistent interface for Terraform AWS Provider resources that handle individual resource tags. Most of the heavy lifting is done by the `keyvaluetags` package to smooth over inconsistencies across AWS service APIs, but this generator does implement some final user experience improvements. + +## Code Structure + +```text +aws/internal/tagresource +├── generator/main.go (generates tag resource) +├── tag_resources_test.go (unit tests for shared tag resource logic) +├── tag_resources.go (shared tag resource logic) +└── service_generation_customizations.go (AWS Go SDK service customizations for generator) +``` diff --git a/aws/internal/tagresource/generator/main.go b/aws/internal/tagresource/generator/main.go new file mode 100644 index 000000000000..f9c7d938ca4d --- /dev/null +++ b/aws/internal/tagresource/generator/main.go @@ -0,0 +1,291 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "strings" + "text/template" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource" +) + +var ( + serviceName = flag.String("servicename", "", "lowercase service name") +) + +type TemplateData struct { + ServiceName string +} + +func main() { + flag.Parse() + + if len(*serviceName) == 0 { + flag.Usage() + os.Exit(2) + } + + resourceName := fmt.Sprintf("aws_%s_tag", *serviceName) + resourceFilename := fmt.Sprintf("resource_%s_gen.go", resourceName) + resourceTestFilename := fmt.Sprintf("resource_%s_gen_test.go", resourceName) + + templateData := TemplateData{ + ServiceName: *serviceName, + } + templateFuncMap := template.FuncMap{ + "IdentifierAttributeName": tagresource.ServiceIdentifierAttributeName, + "Title": strings.Title, + } + + if err := generateTemplateFile(resourceFilename, resourceTemplateBody, templateFuncMap, templateData); err != nil { + log.Fatal(err) + } + + if err := generateTemplateFile(resourceTestFilename, resourceTestTemplateBody, templateFuncMap, templateData); err != nil { + log.Fatal(err) + } +} + +func generateTemplateFile(filename string, templateBody string, templateFuncs template.FuncMap, templateData interface{}) error { + tmpl, err := template.New(filename).Funcs(templateFuncs).Parse(templateBody) + + if err != nil { + return fmt.Errorf("error parsing template: %w", err) + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateData) + + if err != nil { + return fmt.Errorf("error executing template: %w", err) + } + + generatedFileContents, err := format.Source(buffer.Bytes()) + + if err != nil { + return fmt.Errorf("error formatting generated file: %w", err) + } + + f, err := os.Create(filename) + + if err != nil { + return fmt.Errorf("error creating file (%s): %w", filename, err) + } + + defer f.Close() + + _, err = f.Write(generatedFileContents) + + if err != nil { + return fmt.Errorf("error writing to file (%s): %w", filename, err) + } + + return nil +} + +const ( + resourceTemplateBody = ` +// Code generated by internal/tagresource/generator/main.go; DO NOT EDIT. + +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/service/{{ .ServiceName }}" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAws{{ .ServiceName | Title }}Tag() *schema.Resource { + return &schema.Resource{ + Create: resourceAws{{ .ServiceName | Title }}TagCreate, + Read: resourceAws{{ .ServiceName | Title }}TagRead, + Update: resourceAws{{ .ServiceName | Title }}TagUpdate, + Delete: resourceAws{{ .ServiceName | Title }}TagDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "{{ .ServiceName | IdentifierAttributeName }}": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceAws{{ .ServiceName | Title }}TagCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).{{ .ServiceName }}conn + + identifier := d.Get("{{ .ServiceName | IdentifierAttributeName }}").(string) + key := d.Get("key").(string) + value := d.Get("value").(string) + + {{ if eq .ServiceName "ec2" }} + if err := keyvaluetags.{{ .ServiceName | Title }}CreateTags(conn, identifier, map[string]string{key: value}); err != nil { + {{- else }} + if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, nil, map[string]string{key: value}); err != nil { + {{- end }} + return fmt.Errorf("error creating %s resource (%s) tag (%s): %w", {{ .ServiceName }}.ServiceID, identifier, key, err) + } + + d.SetId(tagresource.SetResourceID(identifier, key)) + + return resourceAws{{ .ServiceName | Title }}TagRead(d, meta) +} + +func resourceAws{{ .ServiceName | Title }}TagRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).{{ .ServiceName }}conn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + value, err := keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] %s resource (%s) tag (%s) not found, removing from state", {{ .ServiceName }}.ServiceID, identifier, key) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading %s resource (%s) tag (%s): %w", {{ .ServiceName }}.ServiceID, identifier, key, err) + } + + d.Set("{{ .ServiceName | IdentifierAttributeName }}", identifier) + d.Set("key", key) + d.Set("value", value) + + return nil +} + +func resourceAws{{ .ServiceName | Title }}TagUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).{{ .ServiceName }}conn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, nil, map[string]string{key: d.Get("value").(string)}); err != nil { + return fmt.Errorf("error updating %s resource (%s) tag (%s): %w", {{ .ServiceName }}.ServiceID, identifier, key, err) + } + + return resourceAws{{ .ServiceName | Title }}TagRead(d, meta) +} + +func resourceAws{{ .ServiceName | Title }}TagDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).{{ .ServiceName }}conn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, map[string]string{key: d.Get("value").(string)}, nil); err != nil { + return fmt.Errorf("error deleting %s resource (%s) tag (%s): %w", {{ .ServiceName }}.ServiceID, identifier, key, err) + } + + return nil +} +` + resourceTestTemplateBody = ` +// Code generated by internal/tagresource/generator/main.go; DO NOT EDIT. + +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/service/{{ .ServiceName }}" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccCheck{{ .ServiceName | Title }}TagDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).{{ .ServiceName }}conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_{{ .ServiceName }}_tag" { + continue + } + + identifier, key, err := tagresource.GetResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("%s resource (%s) tag (%s) still exists", {{ .ServiceName }}.ServiceID, identifier, key) + } + + return nil +} + +func testAccCheck{{ .ServiceName | Title }}TagExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("%s: missing resource ID", resourceName) + } + + identifier, key, err := tagresource.GetResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).{{ .ServiceName }}conn + + _, err = keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key) + + if err != nil { + return err + } + + return nil + } +} +` +) diff --git a/aws/internal/tagresource/service_generation_customizations.go b/aws/internal/tagresource/service_generation_customizations.go new file mode 100644 index 000000000000..b014a62f61cc --- /dev/null +++ b/aws/internal/tagresource/service_generation_customizations.go @@ -0,0 +1,15 @@ +package tagresource + +import ( + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +// ServiceIdentifierAttributeName determines the schema identifier attribute name. +func ServiceIdentifierAttributeName(serviceName string) string { + switch serviceName { + case "ec2": + return "resource_id" + default: + return toSnakeCase(keyvaluetags.ServiceTagInputIdentifierField(serviceName)) + } +} diff --git a/aws/internal/tagresource/tag_resources.go b/aws/internal/tagresource/tag_resources.go new file mode 100644 index 000000000000..52a4fe540dbf --- /dev/null +++ b/aws/internal/tagresource/tag_resources.go @@ -0,0 +1,41 @@ +package tagresource + +import ( + "fmt" + "regexp" + "strings" +) + +const ( + // Separator used in resource identifiers + resourceIDSeparator = `,` +) + +// GetResourceID parses a given resource identifier for tag identifier and tag key. +func GetResourceID(resourceID string) (string, string, error) { + parts := strings.SplitN(resourceID, resourceIDSeparator, 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("invalid resource identifier (%[1]s), expected ID%[2]sKEY", resourceID, resourceIDSeparator) + } + + return parts[0], parts[1], nil +} + +// SetResourceID creates a resource identifier given a tag identifier and a tag key. +func SetResourceID(identifier string, key string) string { + parts := []string{identifier, key} + resourceID := strings.Join(parts, resourceIDSeparator) + + return resourceID +} + +// toSnakeCase converts a string to snake case. +// +// For example, AWS Go SDK field names are in PascalCase, +// while Terraform schema attribute names are in snake_case. +func toSnakeCase(str string) string { + result := regexp.MustCompile("(.)([A-Z][a-z]+)").ReplaceAllString(str, "${1}_${2}") + result = regexp.MustCompile("([a-z0-9])([A-Z])").ReplaceAllString(result, "${1}_${2}") + return strings.ToLower(result) +} diff --git a/aws/internal/tagresource/tag_resources_test.go b/aws/internal/tagresource/tag_resources_test.go new file mode 100644 index 000000000000..48cc9bec479e --- /dev/null +++ b/aws/internal/tagresource/tag_resources_test.go @@ -0,0 +1,146 @@ +package tagresource + +import ( + "testing" +) + +func TestGetResourceID(t *testing.T) { + testCases := []struct { + Description string + ResourceIdentifier string + ExpectedIdentifier string + ExpectedKey string + ExpectedError func(err error) bool + }{ + { + Description: "empty resource identifier", + ResourceIdentifier: "", + ExpectedError: func(err error) bool { + return err.Error() == "invalid resource identifier (), expected ID,KEY" + }, + }, + { + Description: "missing identifier", + ResourceIdentifier: ",testkey", + ExpectedError: func(err error) bool { + return err.Error() == "invalid resource identifier (,testkey), expected ID,KEY" + }, + }, + { + Description: "missing key", + ResourceIdentifier: "testidentifier,", + ExpectedError: func(err error) bool { + return err.Error() == "invalid resource identifier (testidentifier,), expected ID,KEY" + }, + }, + { + Description: "incorrect separator", + ResourceIdentifier: "testidentifier;testkey", + ExpectedError: func(err error) bool { + return err.Error() == "invalid resource identifier (testidentifier;testkey), expected ID,KEY" + }, + }, + { + Description: "correct", + ResourceIdentifier: "testidentifier,testkey", + ExpectedIdentifier: "testidentifier", + ExpectedKey: "testkey", + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Description, func(t *testing.T) { + gotIdentifier, gotKey, err := GetResourceID(testCase.ResourceIdentifier) + + if err != nil && !testCase.ExpectedError(err) { + t.Fatalf("unexpected error: %s", err) + } + + if testCase.ExpectedError == nil && err != nil { + t.Fatalf("expected no error, got error: %s", err) + } + + if testCase.ExpectedError != nil && err == nil { + t.Fatalf("expected error, got no error") + } + + if gotIdentifier != testCase.ExpectedIdentifier { + t.Errorf("got identifier %s, expected identifier %s", gotIdentifier, testCase.ExpectedIdentifier) + } + + if gotKey != testCase.ExpectedKey { + t.Errorf("got key %s, expected key %s", gotKey, testCase.ExpectedKey) + } + }) + } +} + +func TestSetResourceId(t *testing.T) { + testCases := []struct { + Description string + Identifier string + Key string + ExpectedResourceIdentifier string + }{ + { + Description: "correct", + Identifier: "testidentifier", + Key: "testkey", + ExpectedResourceIdentifier: "testidentifier,testkey", + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Description, func(t *testing.T) { + got := SetResourceID(testCase.Identifier, testCase.Key) + + if got != testCase.ExpectedResourceIdentifier { + t.Errorf("got %s, expected %s", got, testCase.ExpectedResourceIdentifier) + } + }) + } +} + +func TestToSnakeCase(t *testing.T) { + testCases := []struct { + Input string + Expected string + }{ + { + Input: "ARN", + Expected: "arn", + }, + { + Input: "LogGroupName", + Expected: "log_group_name", + }, + { + Input: "ResourceId", + Expected: "resource_id", + }, + { + Input: "ResourceArn", + Expected: "resource_arn", + }, + { + Input: "ResourceARN", + Expected: "resource_arn", + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Input, func(t *testing.T) { + got := toSnakeCase(testCase.Input) + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} diff --git a/aws/internal/tfresource/errors.go b/aws/internal/tfresource/errors.go index 47dd6137b3f0..c5cc8264a5d8 100644 --- a/aws/internal/tfresource/errors.go +++ b/aws/internal/tfresource/errors.go @@ -10,7 +10,7 @@ import ( // Specifically, NotFound returns true if the error or a wrapped error is of type // resource.NotFoundError. func NotFound(err error) bool { - var e *resource.NotFoundError + var e *resource.NotFoundError // nosemgrep: is-not-found-error return errors.As(err, &e) } @@ -23,3 +23,19 @@ func TimedOut(err error) bool { timeoutErr, ok := err.(*resource.TimeoutError) // nolint:errorlint return ok && timeoutErr.LastError == nil } + +// SetLastError sets the LastError field on the error if supported. +// If lastErr is nil it is ignored. +func SetLastError(err, lastErr error) { + switch err := err.(type) { + case *resource.TimeoutError: + if err.LastError == nil { + err.LastError = lastErr + } + + case *resource.UnexpectedStateError: + if err.LastError == nil { + err.LastError = lastErr + } + } +} diff --git a/aws/internal/tfresource/errors_test.go b/aws/internal/tfresource/errors_test.go index 175ef5150cd9..36883294882a 100644 --- a/aws/internal/tfresource/errors_test.go +++ b/aws/internal/tfresource/errors_test.go @@ -1,11 +1,13 @@ -package tfresource +package tfresource_test import ( "errors" "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func TestNotFound(t *testing.T) { @@ -40,7 +42,7 @@ func TestNotFound(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - got := NotFound(testCase.Err) + got := tfresource.NotFound(testCase.Err) if got != testCase.Expected { t.Errorf("got %t, expected %t", got, testCase.Expected) @@ -88,7 +90,7 @@ func TestTimedOut(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - got := TimedOut(testCase.Err) + got := tfresource.TimedOut(testCase.Err) if got != testCase.Expected { t.Errorf("got %t, expected %t", got, testCase.Expected) @@ -96,3 +98,74 @@ func TestTimedOut(t *testing.T) { }) } } + +func TestSetLastError(t *testing.T) { + testCases := []struct { + Name string + Err error + LastErr error + Expected bool + }{ + { + Name: "nil error", + }, + { + Name: "other error", + Err: errors.New("test"), + LastErr: errors.New("last"), + }, + { + Name: "timeout error lastErr is nil", + Err: &resource.TimeoutError{}, + }, + { + Name: "timeout error", + Err: &resource.TimeoutError{}, + LastErr: errors.New("lasttest"), + Expected: true, + }, + { + Name: "timeout error non-nil last error lastErr is nil", + Err: &resource.TimeoutError{LastError: errors.New("test")}, + }, + { + Name: "timeout error non-nil last error no overwrite", + Err: &resource.TimeoutError{LastError: errors.New("test")}, + LastErr: errors.New("lasttest"), + }, + { + Name: "unexpected state error lastErr is nil", + Err: &resource.UnexpectedStateError{}, + }, + { + Name: "unexpected state error", + Err: &resource.UnexpectedStateError{}, + LastErr: errors.New("lasttest"), + Expected: true, + }, + { + Name: "unexpected state error non-nil last error lastErr is nil", + Err: &resource.UnexpectedStateError{LastError: errors.New("test")}, + }, + { + Name: "unexpected state error non-nil last error no overwrite", + Err: &resource.UnexpectedStateError{LastError: errors.New("test")}, + LastErr: errors.New("lasttest"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + tfresource.SetLastError(testCase.Err, testCase.LastErr) + + if testCase.Err != nil { + got := testCase.Err.Error() + contains := strings.Contains(got, "lasttest") + + if (testCase.Expected && !contains) || (!testCase.Expected && contains) { + t.Errorf("got %s", got) + } + } + }) + } +} diff --git a/aws/internal/tfresource/not_found_error.go b/aws/internal/tfresource/not_found_error.go new file mode 100644 index 000000000000..a270d77d2258 --- /dev/null +++ b/aws/internal/tfresource/not_found_error.go @@ -0,0 +1,93 @@ +package tfresource + +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +type EmptyResultError struct { + LastRequest interface{} +} + +var ErrEmptyResult = &EmptyResultError{} + +func NewEmptyResultError(lastRequest interface{}) error { + return &EmptyResultError{ + LastRequest: lastRequest, + } +} + +func (e *EmptyResultError) Error() string { + return "empty result" +} + +func (e *EmptyResultError) Is(err error) bool { + _, ok := err.(*EmptyResultError) + return ok +} + +func (e *EmptyResultError) As(target interface{}) bool { + t, ok := target.(**resource.NotFoundError) + if !ok { + return false + } + + *t = &resource.NotFoundError{ + Message: e.Error(), + LastRequest: e.LastRequest, + } + + return true +} + +type TooManyResultsError struct { + Count int + LastRequest interface{} +} + +var ErrTooManyResults = &TooManyResultsError{} + +func NewTooManyResultsError(count int, lastRequest interface{}) error { + return &TooManyResultsError{ + Count: count, + LastRequest: lastRequest, + } +} + +func (e *TooManyResultsError) Error() string { + return fmt.Sprintf("too many results: wanted 1, got %d", e.Count) +} + +func (e *TooManyResultsError) Is(err error) bool { + _, ok := err.(*TooManyResultsError) + return ok +} + +func (e *TooManyResultsError) As(target interface{}) bool { + t, ok := target.(**resource.NotFoundError) + if !ok { + return false + } + + *t = &resource.NotFoundError{ + Message: e.Error(), + LastRequest: e.LastRequest, + } + + return true +} + +// SingularDataSourceFindError returns a standard error message for a singular data source's non-nil resource find error. +func SingularDataSourceFindError(resourceType string, err error) error { + if NotFound(err) { + if errors.Is(err, &TooManyResultsError{}) { + return fmt.Errorf("multiple %[1]ss matched; use additional constraints to reduce matches to a single %[1]s", resourceType) + } + + return fmt.Errorf("no matching %[1]s found", resourceType) + } + + return fmt.Errorf("error reading %s: %w", resourceType, err) +} diff --git a/aws/internal/tfresource/not_found_error_test.go b/aws/internal/tfresource/not_found_error_test.go new file mode 100644 index 000000000000..a96f7b522ae5 --- /dev/null +++ b/aws/internal/tfresource/not_found_error_test.go @@ -0,0 +1,156 @@ +package tfresource + +import ( + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestEmptyResultErrorAsNotFoundError(t *testing.T) { + lastRequest := 123 + err := NewEmptyResultError(lastRequest) + + var nfe *resource.NotFoundError + ok := errors.As(err, &nfe) + + if !ok { + t.Fatal("expected errors.As() to return true") + } + if nfe.Message != "empty result" { + t.Errorf(`expected Message to be "empty result", got %q`, nfe.Message) + } + if nfe.LastRequest != lastRequest { + t.Errorf("unexpected value for LastRequest") + } +} + +func TestEmptyResultErrorIs(t *testing.T) { + testCases := []struct { + name string + err error + expected bool + }{ + { + name: "compare to nil", + err: nil, + }, + { + name: "other error", + err: errors.New("test"), + }, + { + name: "EmptyResultError with LastRequest", + err: &EmptyResultError{ + LastRequest: 123, + }, + expected: true, + }, + { + name: "ErrEmptyResult", + err: ErrEmptyResult, + expected: true, + }, + { + name: "wrapped other error", + err: fmt.Errorf("test: %w", errors.New("test")), + }, + { + name: "wrapped EmptyResultError with LastRequest", + err: fmt.Errorf("test: %w", &EmptyResultError{ + LastRequest: 123, + }), + expected: true, + }, + { + name: "wrapped ErrEmptyResult", + err: fmt.Errorf("test: %w", ErrEmptyResult), + expected: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := &EmptyResultError{} + ok := errors.Is(testCase.err, err) + if ok != testCase.expected { + t.Errorf("got %t, expected %t", ok, testCase.expected) + } + }) + } +} + +func TestTooManyResultsErrorAsNotFoundError(t *testing.T) { + count := 2 + lastRequest := 123 + err := NewTooManyResultsError(count, lastRequest) + + var nfe *resource.NotFoundError + ok := errors.As(err, &nfe) + + if !ok { + t.Fatal("expected errors.As() to return true") + } + if expected := fmt.Sprintf("too many results: wanted 1, got %d", count); nfe.Message != expected { + t.Errorf(`expected Message to be %q, got %q`, expected, nfe.Message) + } + if nfe.LastRequest != lastRequest { + t.Errorf("unexpected value for LastRequest") + } +} + +func TestTooManyResultsErrorIs(t *testing.T) { + testCases := []struct { + name string + err error + expected bool + }{ + { + name: "compare to nil", + err: nil, + }, + { + name: "other error", + err: errors.New("test"), + }, + { + name: "TooManyResultsError with LastRequest", + err: &TooManyResultsError{ + LastRequest: 123, + }, + expected: true, + }, + { + name: "ErrTooManyResults", + err: ErrTooManyResults, + expected: true, + }, + { + name: "wrapped other error", + err: fmt.Errorf("test: %w", errors.New("test")), + }, + { + name: "wrapped TooManyResultsError with LastRequest", + err: fmt.Errorf("test: %w", &TooManyResultsError{ + LastRequest: 123, + }), + expected: true, + }, + { + name: "wrapped ErrTooManyResults", + err: fmt.Errorf("test: %w", ErrTooManyResults), + expected: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := &TooManyResultsError{} + ok := errors.Is(testCase.err, err) + if ok != testCase.expected { + t.Errorf("got %t, expected %t", ok, testCase.expected) + } + }) + } +} diff --git a/aws/internal/tfresource/retry.go b/aws/internal/tfresource/retry.go new file mode 100644 index 000000000000..d0e6a1d2ccb6 --- /dev/null +++ b/aws/internal/tfresource/retry.go @@ -0,0 +1,176 @@ +package tfresource + +import ( + "context" + "math/rand" + "sync" + "time" + + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// Retryable is a function that is used to decide if a function's error is retryable or not. +// The error argument can be `nil`. +// If the error is retryable, returns a bool value of `true` and an error (not necessarily the error passed as the argument). +// If the error is not retryable, returns a bool value of `false` and either no error (success state) or an error (not necessarily the error passed as the argument). +type Retryable func(error) (bool, error) + +// RetryWhenContext retries the function `f` when the error it returns satisfies `predicate`. +// `f` is retried until `timeout` expires. +func RetryWhenContext(ctx context.Context, timeout time.Duration, f func() (interface{}, error), retryable Retryable) (interface{}, error) { + var output interface{} + + err := resource.Retry(timeout, func() *resource.RetryError { + var err error + + output, err = f() + retry, err := retryable(err) + + if retry { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if TimedOut(err) { + output, err = f() + } + + if err != nil { + return nil, err + } + + return output, nil +} + +// RetryWhen retries the function `f` when the error it returns satisfies `predicate`. +// `f` is retried until `timeout` expires. +func RetryWhen(timeout time.Duration, f func() (interface{}, error), retryable Retryable) (interface{}, error) { + return RetryWhenContext(context.Background(), timeout, f, retryable) +} + +// RetryWhenAwsErrCodeEqualsContext retries the specified function when it returns one of the specified AWS error code. +func RetryWhenAwsErrCodeEqualsContext(ctx context.Context, timeout time.Duration, f func() (interface{}, error), codes ...string) (interface{}, error) { + return RetryWhenContext(ctx, timeout, f, func(err error) (bool, error) { + if tfawserr.ErrCodeEquals(err, codes...) { + return true, err + } + + return false, err + }) +} + +// RetryWhenAwsErrCodeEquals retries the specified function when it returns one of the specified AWS error code. +func RetryWhenAwsErrCodeEquals(timeout time.Duration, f func() (interface{}, error), codes ...string) (interface{}, error) { + return RetryWhenAwsErrCodeEqualsContext(context.Background(), timeout, f, codes...) +} + +// RetryWhenNotFoundContext retries the specified function when it returns a resource.NotFoundError. +func RetryWhenNotFoundContext(ctx context.Context, timeout time.Duration, f func() (interface{}, error)) (interface{}, error) { + return RetryWhenContext(ctx, timeout, f, func(err error) (bool, error) { + if NotFound(err) { + return true, err + } + + return false, err + }) +} + +// RetryWhenNotFound retries the specified function when it returns a resource.NotFoundError. +func RetryWhenNotFound(timeout time.Duration, f func() (interface{}, error)) (interface{}, error) { + return RetryWhenNotFoundContext(context.Background(), timeout, f) +} + +// RetryWhenNewResourceNotFoundContext retries the specified function when it returns a resource.NotFoundError and `isNewResource` is true. +func RetryWhenNewResourceNotFoundContext(ctx context.Context, timeout time.Duration, f func() (interface{}, error), isNewResource bool) (interface{}, error) { + return RetryWhenContext(ctx, timeout, f, func(err error) (bool, error) { + if isNewResource && NotFound(err) { + return true, err + } + + return false, err + }) +} + +// RetryWhenNewResourceNotFound retries the specified function when it returns a resource.NotFoundError and `isNewResource` is true. +func RetryWhenNewResourceNotFound(timeout time.Duration, f func() (interface{}, error), isNewResource bool) (interface{}, error) { + return RetryWhenNewResourceNotFoundContext(context.Background(), timeout, f, isNewResource) +} + +// RetryConfigContext allows configuration of StateChangeConf's various time arguments. +// This is especially useful for AWS services that are prone to throttling, such as Route53, where +// the default durations cause problems. To not use a StateChangeConf argument and revert to the +// default, pass in a zero value (i.e., 0*time.Second). +func RetryConfigContext(ctx context.Context, delay time.Duration, delayRand time.Duration, minTimeout time.Duration, pollInterval time.Duration, timeout time.Duration, f resource.RetryFunc) error { + // These are used to pull the error out of the function; need a mutex to + // avoid a data race. + var resultErr error + var resultErrMu sync.Mutex + + c := &resource.StateChangeConf{ + Pending: []string{"retryableerror"}, + Target: []string{"success"}, + Timeout: timeout, + Refresh: func() (interface{}, string, error) { + rerr := f() + + resultErrMu.Lock() + defer resultErrMu.Unlock() + + if rerr == nil { + resultErr = nil + return 42, "success", nil + } + + resultErr = rerr.Err + + if rerr.Retryable { + return 42, "retryableerror", nil + } + + return nil, "quit", rerr.Err + }, + } + + if delay.Milliseconds() > 0 { + c.Delay = delay + } + + if delayRand.Milliseconds() > 0 { + // Hitting the API at exactly the same time on each iteration of the retry is more likely to + // cause Throttling problems. We introduce randomness in order to help AWS be happier. + rand.Seed(time.Now().UTC().UnixNano()) + + c.Delay = time.Duration(rand.Int63n(delayRand.Milliseconds())) * time.Millisecond + } + + if minTimeout.Milliseconds() > 0 { + c.MinTimeout = minTimeout + } + + if pollInterval.Milliseconds() > 0 { + c.PollInterval = pollInterval + } + + _, waitErr := c.WaitForStateContext(ctx) + + // Need to acquire the lock here to be able to avoid race using resultErr as + // the return value + resultErrMu.Lock() + defer resultErrMu.Unlock() + + // resultErr may be nil because the wait timed out and resultErr was never + // set; this is still an error + if resultErr == nil { + return waitErr + } + // resultErr takes precedence over waitErr if both are set because it is + // more likely to be useful + return resultErr +} diff --git a/aws/internal/tfresource/retry_test.go b/aws/internal/tfresource/retry_test.go new file mode 100644 index 000000000000..d02c74c098af --- /dev/null +++ b/aws/internal/tfresource/retry_test.go @@ -0,0 +1,248 @@ +package tfresource_test + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestRetryWhenAwsErrCodeEquals(t *testing.T) { + var retryCount int32 + + testCases := []struct { + Name string + F func() (interface{}, error) + ExpectError bool + }{ + { + Name: "no error", + F: func() (interface{}, error) { + return nil, nil + }, + }, + { + Name: "non-retryable other error", + F: func() (interface{}, error) { + return nil, errors.New("TestCode") + }, + ExpectError: true, + }, + { + Name: "non-retryable AWS error", + F: func() (interface{}, error) { + return nil, awserr.New("Testing", "Testing", nil) + }, + ExpectError: true, + }, + { + Name: "retryable AWS error timeout", + F: func() (interface{}, error) { + return nil, awserr.New("TestCode1", "TestMessage", nil) + }, + ExpectError: true, + }, + { + Name: "retryable AWS error success", + F: func() (interface{}, error) { + if atomic.CompareAndSwapInt32(&retryCount, 0, 1) { + return nil, awserr.New("TestCode2", "TestMessage", nil) + } + + return nil, nil + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + retryCount = 0 + + _, err := tfresource.RetryWhenAwsErrCodeEquals(5*time.Second, testCase.F, "TestCode1", "TestCode2") + + if testCase.ExpectError && err == nil { + t.Fatal("expected error") + } else if !testCase.ExpectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + }) + } +} + +func TestRetryWhenNewResourceNotFound(t *testing.T) { + var retryCount int32 + + testCases := []struct { + Name string + F func() (interface{}, error) + NewResource bool + ExpectError bool + }{ + { + Name: "no error", + F: func() (interface{}, error) { + return nil, nil + }, + }, + { + Name: "no error new resource", + F: func() (interface{}, error) { + return nil, nil + }, + NewResource: true, + }, + { + Name: "non-retryable other error", + F: func() (interface{}, error) { + return nil, errors.New("TestCode") + }, + ExpectError: true, + }, + { + Name: "non-retryable other error new resource", + F: func() (interface{}, error) { + return nil, errors.New("TestCode") + }, + NewResource: true, + ExpectError: true, + }, + { + Name: "non-retryable AWS error", + F: func() (interface{}, error) { + return nil, awserr.New("Testing", "Testing", nil) + }, + ExpectError: true, + }, + { + Name: "retryable NotFoundError not new resource", + F: func() (interface{}, error) { + return nil, &resource.NotFoundError{} + }, + ExpectError: true, + }, + { + Name: "retryable NotFoundError new resource timeout", + F: func() (interface{}, error) { + return nil, &resource.NotFoundError{} + }, + NewResource: true, + ExpectError: true, + }, + { + Name: "retryable NotFoundError success new resource", + F: func() (interface{}, error) { + if atomic.CompareAndSwapInt32(&retryCount, 0, 1) { + return nil, &resource.NotFoundError{} + } + + return nil, nil + }, + NewResource: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + retryCount = 0 + + _, err := tfresource.RetryWhenNotFound(5*time.Second, testCase.F) + + if testCase.ExpectError && err == nil { + t.Fatal("expected error") + } else if !testCase.ExpectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + }) + } +} + +func TestRetryWhenNotFound(t *testing.T) { + var retryCount int32 + + testCases := []struct { + Name string + F func() (interface{}, error) + ExpectError bool + }{ + { + Name: "no error", + F: func() (interface{}, error) { + return nil, nil + }, + }, + { + Name: "non-retryable other error", + F: func() (interface{}, error) { + return nil, errors.New("TestCode") + }, + ExpectError: true, + }, + { + Name: "non-retryable AWS error", + F: func() (interface{}, error) { + return nil, awserr.New("Testing", "Testing", nil) + }, + ExpectError: true, + }, + { + Name: "retryable NotFoundError timeout", + F: func() (interface{}, error) { + return nil, &resource.NotFoundError{} + }, + ExpectError: true, + }, + { + Name: "retryable NotFoundError success", + F: func() (interface{}, error) { + if atomic.CompareAndSwapInt32(&retryCount, 0, 1) { + return nil, &resource.NotFoundError{} + } + + return nil, nil + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + retryCount = 0 + + _, err := tfresource.RetryWhenNotFound(5*time.Second, testCase.F) + + if testCase.ExpectError && err == nil { + t.Fatal("expected error") + } else if !testCase.ExpectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + }) + } +} + +func TestRetryConfigContext_error(t *testing.T) { + t.Parallel() + + expected := fmt.Errorf("nope") + f := func() *resource.RetryError { + return resource.NonRetryableError(expected) + } + + errCh := make(chan error) + go func() { + errCh <- tfresource.RetryConfigContext(context.Background(), 0*time.Second, 0*time.Second, 0*time.Second, 0*time.Second, 1*time.Second, f) + }() + + select { + case err := <-errCh: + if err != expected { + t.Fatalf("bad: %#v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("timeout") + } +} diff --git a/aws/internal/tfresource/wait.go b/aws/internal/tfresource/wait.go new file mode 100644 index 000000000000..1a9ef7f89c44 --- /dev/null +++ b/aws/internal/tfresource/wait.go @@ -0,0 +1,64 @@ +package tfresource + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +type WaitOpts struct { + ContinuousTargetOccurence int // Number of times the target state has to occur continuously. + Delay time.Duration // Wait this time before starting checks. + MinTimeout time.Duration // Smallest time to wait before refreshes. + PollInterval time.Duration // Override MinTimeout/backoff and only poll this often. +} + +const ( + targetStateError = "ERROR" + targetStateFalse = "FALSE" + targetStateTrue = "TRUE" +) + +// WaitUntilContext waits for the function `f` to return `true`. +// If `f` returns an error, return immediately with that error. +// If `timeout` is exceeded before `f` returns `true`, return an error. +// Waits between calls to `f` using exponential backoff, except when waiting for the target state to reoccur. +func WaitUntilContext(ctx context.Context, timeout time.Duration, f func() (bool, error), opts WaitOpts) error { + refresh := func() (interface{}, string, error) { + done, err := f() + + if err != nil { + return nil, targetStateError, err + } + + if done { + return "", targetStateTrue, nil + } + + return "", targetStateFalse, nil + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{targetStateFalse}, + Target: []string{targetStateTrue}, + Refresh: refresh, + Timeout: timeout, + ContinuousTargetOccurence: opts.ContinuousTargetOccurence, + Delay: opts.Delay, + MinTimeout: opts.MinTimeout, + PollInterval: opts.PollInterval, + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} + +// WaitUntil waits for the function `f` to return `true`. +// If `f` returns an error, return immediately with that error. +// If `timeout` is exceeded before `f` returns `true`, return an error. +// Waits between calls to `f` using exponential backoff, except when waiting for the target state to reoccur. +func WaitUntil(timeout time.Duration, f func() (bool, error), opts WaitOpts) error { + return WaitUntilContext(context.Background(), timeout, f, opts) +} diff --git a/aws/internal/tfresource/wait_test.go b/aws/internal/tfresource/wait_test.go new file mode 100644 index 000000000000..8062b0e14d58 --- /dev/null +++ b/aws/internal/tfresource/wait_test.go @@ -0,0 +1,65 @@ +package tfresource_test + +import ( + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestWaitUntil(t *testing.T) { + var retryCount int32 + + testCases := []struct { + Name string + F func() (bool, error) + ExpectError bool + }{ + { + Name: "no error", + F: func() (bool, error) { + return true, nil + }, + }, + { + Name: "immediate error", + F: func() (bool, error) { + return false, errors.New("TestCode") + }, + ExpectError: true, + }, + { + Name: "never reaches state", + F: func() (bool, error) { + return false, nil + }, + ExpectError: true, + }, + { + Name: "retry then success", + F: func() (bool, error) { + if atomic.CompareAndSwapInt32(&retryCount, 0, 1) { + return true, nil + } + + return false, nil + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + retryCount = 0 + + err := tfresource.WaitUntil(5*time.Second, testCase.F, tfresource.WaitOpts{}) + + if testCase.ExpectError && err == nil { + t.Fatal("expected error") + } else if !testCase.ExpectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + }) + } +} diff --git a/aws/internal/vault/helper/pgpkeys/keybase_test.go b/aws/internal/vault/helper/pgpkeys/keybase_test.go index ded5af5d76dc..c261e6f14c42 100644 --- a/aws/internal/vault/helper/pgpkeys/keybase_test.go +++ b/aws/internal/vault/helper/pgpkeys/keybase_test.go @@ -33,7 +33,7 @@ func TestFetchKeybasePubkeys(t *testing.T) { exp := []string{ "0f801f518ec853daff611e836528efcac6caa3db", - "91a6e7f85d05c65630bef18951852d87348ffc4c", + "c874011f0ab405110d02105534365d9472d7468f", } if !reflect.DeepEqual(fingerprints, exp) { diff --git a/aws/internal/vault/sdk/helper/jsonutil/json_test.go b/aws/internal/vault/sdk/helper/jsonutil/json_test.go index 339ea852ccb9..af91de42777b 100644 --- a/aws/internal/vault/sdk/helper/jsonutil/json_test.go +++ b/aws/internal/vault/sdk/helper/jsonutil/json_test.go @@ -2,7 +2,6 @@ package jsonutil import ( "bytes" - "fmt" "reflect" "testing" ) @@ -14,7 +13,7 @@ func TestJSONUtil_DecodeJSONFromReader(t *testing.T) { err := DecodeJSONFromReader(bytes.NewReader([]byte(input)), &actual) if err != nil { - fmt.Printf("decoding err: %v\n", err) + t.Errorf("decoding err: %v", err) } expected := map[string]interface{}{ diff --git a/aws/lightsail_domain_test.go b/aws/lightsail_domain_test.go index 4a5fe44f521c..34f22cc41061 100644 --- a/aws/lightsail_domain_test.go +++ b/aws/lightsail_domain_test.go @@ -39,15 +39,15 @@ func init() { func testAccPreCheckLightsailDomain(t *testing.T) { testAccPartitionHasServicePreCheck(lightsail.EndpointsID, t) + region := testAccGetLightsailDomainRegion() + + if region == "" { + t.Skip("Lightsail Domains not available in this AWS Partition") + } + // Since we are outside the scope of the Terraform configuration we must // call Configure() to properly initialize the provider configuration. testAccProviderLightsailDomainConfigure.Do(func() { - region := testAccGetLightsailDomainRegion() - - if region == "" { - t.Skip("Lightsail Domains not available in this AWS Partition") - } - config := map[string]interface{}{ "region": region, } diff --git a/aws/opsworks_layers.go b/aws/opsworks_layers.go index cd9afd2e9b17..9513b6a7449c 100644 --- a/aws/opsworks_layers.go +++ b/aws/opsworks_layers.go @@ -213,7 +213,8 @@ func (lt *opsworksLayerType) SchemaResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), } if lt.CustomShortName { @@ -247,34 +248,31 @@ func (lt *opsworksLayerType) SchemaResource() *schema.Resource { return &schema.Resource{ Read: func(d *schema.ResourceData, meta interface{}) error { - client := meta.(*AWSClient).opsworksconn - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - - return lt.Read(d, client, ignoreTagsConfig) + return lt.Read(d, meta) }, Create: func(d *schema.ResourceData, meta interface{}) error { - client := meta.(*AWSClient).opsworksconn - return lt.Create(d, client, meta) + return lt.Create(d, meta) }, Update: func(d *schema.ResourceData, meta interface{}) error { - client := meta.(*AWSClient).opsworksconn - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - - return lt.Update(d, client, ignoreTagsConfig) + return lt.Update(d, meta) }, Delete: func(d *schema.ResourceData, meta interface{}) error { - client := meta.(*AWSClient).opsworksconn - return lt.Delete(d, client) + return lt.Delete(d, meta) }, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, Schema: resourceSchema, + + CustomizeDiff: SetTagsDiff, } } -func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWorks, ignoreTagsConfig *keyvaluetags.IgnoreConfig) error { +func (lt *opsworksLayerType) Read(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).opsworksconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig req := &opsworks.DescribeLayersInput{ LayerIds: []*string{ @@ -284,7 +282,7 @@ func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWo log.Printf("[DEBUG] Reading OpsWorks layer: %s", d.Id()) - resp, err := client.DescribeLayers(req) + resp, err := conn.DescribeLayers(req) if err != nil { if isAWSErr(err, opsworks.ErrCodeResourceNotFoundException, "") { d.SetId("") @@ -334,7 +332,7 @@ func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWo aws.String(d.Id()), }, } - loadBalancers, err := client.DescribeElasticLoadBalancers(ebsRequest) + loadBalancers, err := conn.DescribeElasticLoadBalancers(ebsRequest) if err != nil { return err } @@ -350,21 +348,30 @@ func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWo arn := aws.StringValue(layer.Arn) d.Set("arn", arn) - tags, err := keyvaluetags.OpsworksListTags(client, arn) + tags, err := keyvaluetags.OpsworksListTags(conn, arn) if err != nil { return fmt.Errorf("error listing tags for Opsworks Layer (%s): %s", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } -func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.OpsWorks, meta interface{}) error { - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig +func (lt *opsworksLayerType) Create(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).opsworksconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) attributes, err := lt.AttributeMap(d) if err != nil { @@ -398,7 +405,7 @@ func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.Ops log.Printf("[DEBUG] Creating OpsWorks layer: %s", d.Id()) - resp, err := client.CreateLayer(req) + resp, err := conn.CreateLayer(req) if err != nil { return err } @@ -409,7 +416,7 @@ func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.Ops loadBalancer := aws.String(d.Get("elastic_load_balancer").(string)) if loadBalancer != nil && *loadBalancer != "" { log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancer) - _, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ + _, err := conn.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ ElasticLoadBalancerName: loadBalancer, LayerId: &layerId, }) @@ -426,16 +433,18 @@ func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.Ops Resource: fmt.Sprintf("layer/%s", d.Id()), }.String() - if v, ok := d.GetOk("tags"); ok { - if err := keyvaluetags.OpsworksUpdateTags(client, arn, nil, v.(map[string]interface{})); err != nil { + if len(tags) > 0 { + if err := keyvaluetags.OpsworksUpdateTags(conn, arn, nil, tags); err != nil { return fmt.Errorf("error updating Opsworks stack (%s) tags: %s", arn, err) } } - return lt.Read(d, client, ignoreTagsConfig) + return lt.Read(d, meta) } -func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.OpsWorks, ignoreTagsConfig *keyvaluetags.IgnoreConfig) error { +func (lt *opsworksLayerType) Update(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).opsworksconn + attributes, err := lt.AttributeMap(d) if err != nil { return err @@ -475,7 +484,7 @@ func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.Ops if loadBalancerOld != nil && *loadBalancerOld != "" { log.Printf("[DEBUG] Dettaching load balancer: %s", *loadBalancerOld) - _, err := client.DetachElasticLoadBalancer(&opsworks.DetachElasticLoadBalancerInput{ + _, err := conn.DetachElasticLoadBalancer(&opsworks.DetachElasticLoadBalancerInput{ ElasticLoadBalancerName: loadBalancerOld, LayerId: aws.String(d.Id()), }) @@ -486,7 +495,7 @@ func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.Ops if loadBalancerNew != nil && *loadBalancerNew != "" { log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancerNew) - _, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ + _, err := conn.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ ElasticLoadBalancerName: loadBalancerNew, LayerId: aws.String(d.Id()), }) @@ -496,31 +505,33 @@ func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.Ops } } - _, err = client.UpdateLayer(req) + _, err = conn.UpdateLayer(req) if err != nil { return err } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") arn := d.Get("arn").(string) - if err := keyvaluetags.OpsworksUpdateTags(client, arn, o, n); err != nil { + if err := keyvaluetags.OpsworksUpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating Opsworks Layer (%s) tags: %s", arn, err) } } - return lt.Read(d, client, ignoreTagsConfig) + return lt.Read(d, meta) } -func (lt *opsworksLayerType) Delete(d *schema.ResourceData, client *opsworks.OpsWorks) error { +func (lt *opsworksLayerType) Delete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).opsworksconn + req := &opsworks.DeleteLayerInput{ LayerId: aws.String(d.Id()), } log.Printf("[DEBUG] Deleting OpsWorks layer: %s", d.Id()) - _, err := client.DeleteLayer(req) + _, err := conn.DeleteLayer(req) return err } diff --git a/aws/provider.go b/aws/provider.go index d07e1c7deca5..c4734b79210e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" "github.com/terraform-providers/terraform-provider-aws/aws/internal/mutexkv" ) @@ -88,6 +89,29 @@ func Provider() *schema.Provider { Set: schema.HashString, }, + "default_tags": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Configuration block with settings to default resource tags across all resources.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "tags": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Resource tags to default across all resources", + }, + }, + }, + }, + + "http_proxy": { + Type: schema.TypeString, + Optional: true, + Description: descriptions["http_proxy"], + }, + "endpoints": endpointsSchema(), "ignore_tags": { @@ -168,6 +192,7 @@ func Provider() *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ "aws_acm_certificate": dataSourceAwsAcmCertificate(), "aws_acmpca_certificate_authority": dataSourceAwsAcmpcaCertificateAuthority(), + "aws_acmpca_certificate": dataSourceAwsAcmpcaCertificate(), "aws_ami": dataSourceAwsAmi(), "aws_ami_ids": dataSourceAwsAmiIds(), "aws_api_gateway_api_key": dataSourceAwsApiGatewayApiKey(), @@ -175,6 +200,10 @@ func Provider() *schema.Provider { "aws_api_gateway_resource": dataSourceAwsApiGatewayResource(), "aws_api_gateway_rest_api": dataSourceAwsApiGatewayRestApi(), "aws_api_gateway_vpc_link": dataSourceAwsApiGatewayVpcLink(), + "aws_apigatewayv2_api": dataSourceAwsApiGatewayV2Api(), + "aws_apigatewayv2_apis": dataSourceAwsApiGatewayV2Apis(), + "aws_appmesh_mesh": dataSourceAwsAppmeshMesh(), + "aws_appmesh_virtual_service": dataSourceAwsAppmeshVirtualService(), "aws_arn": dataSourceAwsArn(), "aws_autoscaling_group": dataSourceAwsAutoscalingGroup(), "aws_autoscaling_groups": dataSourceAwsAutoscalingGroups(), @@ -188,28 +217,43 @@ func Provider() *schema.Provider { "aws_billing_service_account": dataSourceAwsBillingServiceAccount(), "aws_caller_identity": dataSourceAwsCallerIdentity(), "aws_canonical_user_id": dataSourceAwsCanonicalUserId(), + "aws_cloudcontrolapi_resource": dataSourceAwsCloudControlApiResource(), "aws_cloudformation_export": dataSourceAwsCloudFormationExport(), "aws_cloudformation_stack": dataSourceAwsCloudFormationStack(), + "aws_cloudformation_type": dataSourceAwsCloudFormationType(), "aws_cloudfront_cache_policy": dataSourceAwsCloudFrontCachePolicy(), "aws_cloudfront_distribution": dataSourceAwsCloudFrontDistribution(), + "aws_cloudfront_function": dataSourceAwsCloudFrontFunction(), + "aws_cloudfront_log_delivery_canonical_user_id": dataSourceAwsCloudFrontLogDeliveryCanonicalUserId(), "aws_cloudfront_origin_request_policy": dataSourceAwsCloudFrontOriginRequestPolicy(), "aws_cloudhsm_v2_cluster": dataSourceCloudHsmV2Cluster(), "aws_cloudtrail_service_account": dataSourceAwsCloudTrailServiceAccount(), + "aws_cloudwatch_event_connection": dataSourceAwsCloudwatchEventConnection(), + "aws_cloudwatch_event_source": dataSourceAwsCloudWatchEventSource(), "aws_cloudwatch_log_group": dataSourceAwsCloudwatchLogGroup(), + "aws_cloudwatch_log_groups": dataSourceAwsCloudwatchLogGroups(), "aws_codeartifact_authorization_token": dataSourceAwsCodeArtifactAuthorizationToken(), "aws_codeartifact_repository_endpoint": dataSourceAwsCodeArtifactRepositoryEndpoint(), "aws_cognito_user_pools": dataSourceAwsCognitoUserPools(), "aws_codecommit_repository": dataSourceAwsCodeCommitRepository(), + "aws_codestarconnections_connection": dataSourceAwsCodeStarConnectionsConnection(), + "aws_connect_contact_flow": dataSourceAwsConnectContactFlow(), + "aws_connect_instance": dataSourceAwsConnectInstance(), "aws_cur_report_definition": dataSourceAwsCurReportDefinition(), + "aws_default_tags": dataSourceAwsDefaultTags(), "aws_db_cluster_snapshot": dataSourceAwsDbClusterSnapshot(), "aws_db_event_categories": dataSourceAwsDbEventCategories(), "aws_db_instance": dataSourceAwsDbInstance(), + "aws_db_proxy": dataSourceAwsDbProxy(), "aws_db_snapshot": dataSourceAwsDbSnapshot(), "aws_db_subnet_group": dataSourceAwsDbSubnetGroup(), "aws_directory_service_directory": dataSourceAwsDirectoryServiceDirectory(), "aws_docdb_engine_version": dataSourceAwsDocdbEngineVersion(), "aws_docdb_orderable_db_instance": dataSourceAwsDocdbOrderableDbInstance(), + "aws_dx_connection": dataSourceAwsDxConnection(), "aws_dx_gateway": dataSourceAwsDxGateway(), + "aws_dx_location": dataSourceAwsDxLocation(), + "aws_dx_locations": dataSourceAwsDxLocations(), "aws_dynamodb_table": dataSourceAwsDynamoDbTable(), "aws_ebs_default_kms_key": dataSourceAwsEbsDefaultKmsKey(), "aws_ebs_encryption_by_default": dataSourceAwsEbsEncryptionByDefault(), @@ -219,6 +263,7 @@ func Provider() *schema.Provider { "aws_ebs_volumes": dataSourceAwsEbsVolumes(), "aws_ec2_coip_pool": dataSourceAwsEc2CoipPool(), "aws_ec2_coip_pools": dataSourceAwsEc2CoipPools(), + "aws_ec2_host": dataSourceAwsEc2Host(), "aws_ec2_instance_type": dataSourceAwsEc2InstanceType(), "aws_ec2_instance_type_offering": dataSourceAwsEc2InstanceTypeOffering(), "aws_ec2_instance_type_offerings": dataSourceAwsEc2InstanceTypeOfferings(), @@ -235,6 +280,7 @@ func Provider() *schema.Provider { "aws_ec2_transit_gateway_dx_gateway_attachment": dataSourceAwsEc2TransitGatewayDxGatewayAttachment(), "aws_ec2_transit_gateway_peering_attachment": dataSourceAwsEc2TransitGatewayPeeringAttachment(), "aws_ec2_transit_gateway_route_table": dataSourceAwsEc2TransitGatewayRouteTable(), + "aws_ec2_transit_gateway_route_tables": dataSourceAwsEc2TransitGatewayRouteTables(), "aws_ec2_transit_gateway_vpc_attachment": dataSourceAwsEc2TransitGatewayVpcAttachment(), "aws_ec2_transit_gateway_vpn_attachment": dataSourceAwsEc2TransitGatewayVpnAttachment(), "aws_ecr_authorization_token": dataSourceAwsEcrAuthorizationToken(), @@ -250,17 +296,25 @@ func Provider() *schema.Provider { "aws_efs_file_system": dataSourceAwsEfsFileSystem(), "aws_efs_mount_target": dataSourceAwsEfsMountTarget(), "aws_eip": dataSourceAwsEip(), + "aws_eks_addon": dataSourceAwsEksAddon(), "aws_eks_cluster": dataSourceAwsEksCluster(), + "aws_eks_clusters": dataSourceAwsEksClusters(), "aws_eks_cluster_auth": dataSourceAwsEksClusterAuth(), + "aws_eks_node_group": dataSourceAwsEksNodeGroup(), + "aws_eks_node_groups": dataSourceAwsEksNodeGroups(), "aws_elastic_beanstalk_application": dataSourceAwsElasticBeanstalkApplication(), "aws_elastic_beanstalk_hosted_zone": dataSourceAwsElasticBeanstalkHostedZone(), "aws_elastic_beanstalk_solution_stack": dataSourceAwsElasticBeanstalkSolutionStack(), "aws_elasticache_cluster": dataSourceAwsElastiCacheCluster(), + "aws_elasticache_replication_group": dataSourceAwsElasticacheReplicationGroup(), + "aws_elasticache_user": dataSourceAwsElastiCacheUser(), "aws_elasticsearch_domain": dataSourceAwsElasticSearchDomain(), "aws_elb": dataSourceAwsElb(), - "aws_elasticache_replication_group": dataSourceAwsElasticacheReplicationGroup(), "aws_elb_hosted_zone_id": dataSourceAwsElbHostedZoneId(), "aws_elb_service_account": dataSourceAwsElbServiceAccount(), + "aws_globalaccelerator_accelerator": dataSourceAwsGlobalAcceleratorAccelerator(), + "aws_glue_connection": dataSourceAwsGlueConnection(), + "aws_glue_data_catalog_encryption_settings": dataSourceAwsGlueDataCatalogEncryptionSettings(), "aws_glue_script": dataSourceAwsGlueScript(), "aws_guardduty_detector": dataSourceAwsGuarddutyDetector(), "aws_iam_account_alias": dataSourceAwsIamAccountAlias(), @@ -269,8 +323,11 @@ func Provider() *schema.Provider { "aws_iam_policy": dataSourceAwsIAMPolicy(), "aws_iam_policy_document": dataSourceAwsIamPolicyDocument(), "aws_iam_role": dataSourceAwsIAMRole(), + "aws_iam_roles": dataSourceAwsIAMRoles(), "aws_iam_server_certificate": dataSourceAwsIAMServerCertificate(), + "aws_iam_session_context": dataSourceAwsIAMSessionContext(), "aws_iam_user": dataSourceAwsIAMUser(), + "aws_iam_users": dataSourceAwsIAMUsers(), "aws_identitystore_group": dataSourceAwsIdentityStoreGroup(), "aws_identitystore_user": dataSourceAwsIdentityStoreUser(), "aws_imagebuilder_component": dataSourceAwsImageBuilderComponent(), @@ -285,10 +342,13 @@ func Provider() *schema.Provider { "aws_internet_gateway": dataSourceAwsInternetGateway(), "aws_iot_endpoint": dataSourceAwsIotEndpoint(), "aws_ip_ranges": dataSourceAwsIPRanges(), + "aws_kinesis_firehose_delivery_stream": dataSourceAwsKinesisFirehoseDeliveryStream(), "aws_kinesis_stream": dataSourceAwsKinesisStream(), + "aws_kinesis_stream_consumer": dataSourceAwsKinesisStreamConsumer(), "aws_kms_alias": dataSourceAwsKmsAlias(), "aws_kms_ciphertext": dataSourceAwsKmsCiphertext(), "aws_kms_key": dataSourceAwsKmsKey(), + "aws_kms_public_key": dataSourceAwsKmsPublicKey(), "aws_kms_secret": dataSourceAwsKmsSecret(), "aws_kms_secrets": dataSourceAwsKmsSecrets(), "aws_lakeformation_data_lake_settings": dataSourceAwsLakeFormationDataLakeSettings(), @@ -306,14 +366,18 @@ func Provider() *schema.Provider { "aws_lex_intent": dataSourceAwsLexIntent(), "aws_lex_slot_type": dataSourceAwsLexSlotType(), "aws_mq_broker": dataSourceAwsMqBroker(), + "aws_msk_broker_nodes": dataSourceAwsMskBrokerNodes(), "aws_msk_cluster": dataSourceAwsMskCluster(), "aws_msk_configuration": dataSourceAwsMskConfiguration(), + "aws_msk_kafka_version": dataSourceAwsMskKafkaVersion(), "aws_nat_gateway": dataSourceAwsNatGateway(), "aws_neptune_orderable_db_instance": dataSourceAwsNeptuneOrderableDbInstance(), "aws_neptune_engine_version": dataSourceAwsNeptuneEngineVersion(), "aws_network_acls": dataSourceAwsNetworkAcls(), "aws_network_interface": dataSourceAwsNetworkInterface(), "aws_network_interfaces": dataSourceAwsNetworkInterfaces(), + "aws_organizations_delegated_administrators": dataSourceAwsOrganizationsDelegatedAdministrators(), + "aws_organizations_delegated_services": dataSourceAwsOrganizationsDelegatedServices(), "aws_organizations_organization": dataSourceAwsOrganizationsOrganization(), "aws_organizations_organizational_units": dataSourceAwsOrganizationsOrganizationalUnits(), "aws_outposts_outpost": dataSourceAwsOutpostsOutpost(), @@ -336,6 +400,7 @@ func Provider() *schema.Provider { "aws_redshift_service_account": dataSourceAwsRedshiftServiceAccount(), "aws_region": dataSourceAwsRegion(), "aws_regions": dataSourceAwsRegions(), + "aws_resourcegroupstaggingapi_resources": dataSourceAwsResourceGroupsTaggingAPIResources(), "aws_route": dataSourceAwsRoute(), "aws_route_table": dataSourceAwsRouteTable(), "aws_route_tables": dataSourceAwsRouteTables(), @@ -351,8 +416,14 @@ func Provider() *schema.Provider { "aws_secretsmanager_secret": dataSourceAwsSecretsManagerSecret(), "aws_secretsmanager_secret_rotation": dataSourceAwsSecretsManagerSecretRotation(), "aws_secretsmanager_secret_version": dataSourceAwsSecretsManagerSecretVersion(), + "aws_servicecatalog_constraint": dataSourceAwsServiceCatalogConstraint(), + "aws_servicecatalog_launch_paths": dataSourceAwsServiceCatalogLaunchPaths(), + "aws_servicecatalog_portfolio_constraints": dataSourceAwsServiceCatalogPortfolioConstraints(), + "aws_servicecatalog_portfolio": dataSourceAwsServiceCatalogPortfolio(), + "aws_servicecatalog_product": dataSourceAwsServiceCatalogProduct(), "aws_servicequotas_service": dataSourceAwsServiceQuotasService(), "aws_servicequotas_service_quota": dataSourceAwsServiceQuotasServiceQuota(), + "aws_service_discovery_dns_namespace": dataSourceServiceDiscoveryDnsNamespace(), "aws_sfn_activity": dataSourceAwsSfnActivity(), "aws_sfn_state_machine": dataSourceAwsSfnStateMachine(), "aws_signer_signing_job": dataSourceAwsSignerSigningJob(), @@ -361,11 +432,13 @@ func Provider() *schema.Provider { "aws_sqs_queue": dataSourceAwsSqsQueue(), "aws_ssm_document": dataSourceAwsSsmDocument(), "aws_ssm_parameter": dataSourceAwsSsmParameter(), + "aws_ssm_parameters_by_path": dataSourceAwsSsmParametersByPath(), "aws_ssm_patch_baseline": dataSourceAwsSsmPatchBaseline(), "aws_ssoadmin_instances": dataSourceAwsSsoAdminInstances(), "aws_ssoadmin_permission_set": dataSourceAwsSsoAdminPermissionSet(), "aws_storagegateway_local_disk": dataSourceAwsStorageGatewayLocalDisk(), "aws_subnet": dataSourceAwsSubnet(), + "aws_subnets": dataSourceAwsSubnets(), "aws_subnet_ids": dataSourceAwsSubnetIDs(), "aws_transfer_server": dataSourceAwsTransferServer(), "aws_vpcs": dataSourceAwsVpcs(), @@ -409,10 +482,17 @@ func Provider() *schema.Provider { "aws_acm_certificate": resourceAwsAcmCertificate(), "aws_acm_certificate_validation": resourceAwsAcmCertificateValidation(), "aws_acmpca_certificate_authority": resourceAwsAcmpcaCertificateAuthority(), + "aws_acmpca_certificate_authority_certificate": resourceAwsAcmpcaCertificateAuthorityCertificate(), + "aws_acmpca_certificate": resourceAwsAcmpcaCertificate(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), "aws_ami_launch_permission": resourceAwsAmiLaunchPermission(), + "aws_amplify_app": resourceAwsAmplifyApp(), + "aws_amplify_backend_environment": resourceAwsAmplifyBackendEnvironment(), + "aws_amplify_branch": resourceAwsAmplifyBranch(), + "aws_amplify_domain_association": resourceAwsAmplifyDomainAssociation(), + "aws_amplify_webhook": resourceAwsAmplifyWebhook(), "aws_api_gateway_account": resourceAwsApiGatewayAccount(), "aws_api_gateway_api_key": resourceAwsApiGatewayApiKey(), "aws_api_gateway_authorizer": resourceAwsApiGatewayAuthorizer(), @@ -453,6 +533,12 @@ func Provider() *schema.Provider { "aws_appautoscaling_target": resourceAwsAppautoscalingTarget(), "aws_appautoscaling_policy": resourceAwsAppautoscalingPolicy(), "aws_appautoscaling_scheduled_action": resourceAwsAppautoscalingScheduledAction(), + "aws_appconfig_application": resourceAwsAppconfigApplication(), + "aws_appconfig_configuration_profile": resourceAwsAppconfigConfigurationProfile(), + "aws_appconfig_deployment": resourceAwsAppconfigDeployment(), + "aws_appconfig_deployment_strategy": resourceAwsAppconfigDeploymentStrategy(), + "aws_appconfig_environment": resourceAwsAppconfigEnvironment(), + "aws_appconfig_hosted_configuration_version": resourceAwsAppconfigHostedConfigurationVersion(), "aws_appmesh_gateway_route": resourceAwsAppmeshGatewayRoute(), "aws_appmesh_mesh": resourceAwsAppmeshMesh(), "aws_appmesh_route": resourceAwsAppmeshRoute(), @@ -460,6 +546,13 @@ func Provider() *schema.Provider { "aws_appmesh_virtual_node": resourceAwsAppmeshVirtualNode(), "aws_appmesh_virtual_router": resourceAwsAppmeshVirtualRouter(), "aws_appmesh_virtual_service": resourceAwsAppmeshVirtualService(), + "aws_apprunner_auto_scaling_configuration_version": resourceAwsAppRunnerAutoScalingConfigurationVersion(), + "aws_apprunner_connection": resourceAwsAppRunnerConnection(), + "aws_apprunner_custom_domain_association": resourceAwsAppRunnerCustomDomainAssociation(), + "aws_apprunner_service": resourceAwsAppRunnerService(), + "aws_appstream_stack": resourceAwsAppStreamStack(), + "aws_appstream_fleet": resourceAwsAppStreamFleet(), + "aws_appstream_image_builder": resourceAwsAppStreamImageBuilder(), "aws_appsync_api_key": resourceAwsAppsyncApiKey(), "aws_appsync_datasource": resourceAwsAppsyncDatasource(), "aws_appsync_function": resourceAwsAppsyncFunction(), @@ -470,6 +563,7 @@ func Provider() *schema.Provider { "aws_athena_workgroup": resourceAwsAthenaWorkgroup(), "aws_autoscaling_attachment": resourceAwsAutoscalingAttachment(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), + "aws_autoscaling_group_tag": resourceAwsAutoscalingGroupTag(), "aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), @@ -483,21 +577,37 @@ func Provider() *schema.Provider { "aws_backup_vault_notifications": resourceAwsBackupVaultNotifications(), "aws_backup_vault_policy": resourceAwsBackupVaultPolicy(), "aws_budgets_budget": resourceAwsBudgetsBudget(), + "aws_budgets_budget_action": resourceAwsBudgetsBudgetAction(), + "aws_chime_voice_connector": resourceAwsChimeVoiceConnector(), + "aws_chime_voice_connector_group": resourceAwsChimeVoiceConnectorGroup(), + "aws_chime_voice_connector_logging": resourceAwsChimeVoiceConnectorLogging(), + "aws_chime_voice_connector_streaming": resourceAwsChimeVoiceConnectorStreaming(), + "aws_chime_voice_connector_origination": resourceAwsChimeVoiceConnectorOrigination(), + "aws_chime_voice_connector_termination": resourceAwsChimeVoiceConnectorTermination(), "aws_cloud9_environment_ec2": resourceAwsCloud9EnvironmentEc2(), + "aws_cloudcontrolapi_resource": resourceAwsCloudControlApiResource(), "aws_cloudformation_stack": resourceAwsCloudFormationStack(), "aws_cloudformation_stack_set": resourceAwsCloudFormationStackSet(), "aws_cloudformation_stack_set_instance": resourceAwsCloudFormationStackSetInstance(), + "aws_cloudformation_type": resourceAwsCloudFormationType(), "aws_cloudfront_cache_policy": resourceAwsCloudFrontCachePolicy(), "aws_cloudfront_distribution": resourceAwsCloudFrontDistribution(), + "aws_cloudfront_function": resourceAwsCloudFrontFunction(), + "aws_cloudfront_key_group": resourceAwsCloudFrontKeyGroup(), + "aws_cloudfront_monitoring_subscription": resourceAwsCloudFrontMonitoringSubscription(), "aws_cloudfront_origin_access_identity": resourceAwsCloudFrontOriginAccessIdentity(), "aws_cloudfront_origin_request_policy": resourceAwsCloudFrontOriginRequestPolicy(), "aws_cloudfront_public_key": resourceAwsCloudFrontPublicKey(), "aws_cloudfront_realtime_log_config": resourceAwsCloudFrontRealtimeLogConfig(), "aws_cloudtrail": resourceAwsCloudTrail(), "aws_cloudwatch_event_bus": resourceAwsCloudWatchEventBus(), + "aws_cloudwatch_event_bus_policy": resourceAwsCloudWatchEventBusPolicy(), "aws_cloudwatch_event_permission": resourceAwsCloudWatchEventPermission(), "aws_cloudwatch_event_rule": resourceAwsCloudWatchEventRule(), "aws_cloudwatch_event_target": resourceAwsCloudWatchEventTarget(), + "aws_cloudwatch_event_archive": resourceAwsCloudWatchEventArchive(), + "aws_cloudwatch_event_connection": resourceAwsCloudWatchEventConnection(), + "aws_cloudwatch_event_api_destination": resourceAwsCloudWatchEventApiDestination(), "aws_cloudwatch_log_destination": resourceAwsCloudWatchLogDestination(), "aws_cloudwatch_log_destination_policy": resourceAwsCloudWatchLogDestinationPolicy(), "aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(), @@ -512,22 +622,26 @@ func Provider() *schema.Provider { "aws_config_configuration_recorder_status": resourceAwsConfigConfigurationRecorderStatus(), "aws_config_conformance_pack": resourceAwsConfigConformancePack(), "aws_config_delivery_channel": resourceAwsConfigDeliveryChannel(), + "aws_config_organization_conformance_pack": resourceAwsConfigOrganizationConformancePack(), "aws_config_organization_custom_rule": resourceAwsConfigOrganizationCustomRule(), "aws_config_organization_managed_rule": resourceAwsConfigOrganizationManagedRule(), "aws_config_remediation_configuration": resourceAwsConfigRemediationConfiguration(), "aws_cognito_identity_pool": resourceAwsCognitoIdentityPool(), "aws_cognito_identity_pool_roles_attachment": resourceAwsCognitoIdentityPoolRolesAttachment(), "aws_cognito_identity_provider": resourceAwsCognitoIdentityProvider(), + "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), "aws_cognito_user_group": resourceAwsCognitoUserGroup(), "aws_cognito_user_pool": resourceAwsCognitoUserPool(), "aws_cognito_user_pool_client": resourceAwsCognitoUserPoolClient(), "aws_cognito_user_pool_domain": resourceAwsCognitoUserPoolDomain(), + "aws_cognito_user_pool_ui_customization": resourceAwsCognitoUserPoolUICustomization(), "aws_cloudhsm_v2_cluster": resourceAwsCloudHsmV2Cluster(), "aws_cloudhsm_v2_hsm": resourceAwsCloudHsmV2Hsm(), - "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), "aws_cloudwatch_composite_alarm": resourceAwsCloudWatchCompositeAlarm(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_cloudwatch_dashboard": resourceAwsCloudWatchDashboard(), + "aws_cloudwatch_metric_stream": resourceAwsCloudWatchMetricStream(), + "aws_cloudwatch_query_definition": resourceAwsCloudWatchQueryDefinition(), "aws_codedeploy_app": resourceAwsCodeDeployApp(), "aws_codedeploy_deployment_config": resourceAwsCodeDeployDeploymentConfig(), "aws_codedeploy_deployment_group": resourceAwsCodeDeployDeploymentGroup(), @@ -544,7 +658,10 @@ func Provider() *schema.Provider { "aws_codepipeline": resourceAwsCodePipeline(), "aws_codepipeline_webhook": resourceAwsCodePipelineWebhook(), "aws_codestarconnections_connection": resourceAwsCodeStarConnectionsConnection(), + "aws_codestarconnections_host": resourceAwsCodeStarConnectionsHost(), "aws_codestarnotifications_notification_rule": resourceAwsCodeStarNotificationsNotificationRule(), + "aws_connect_contact_flow": resourceAwsConnectContactFlow(), + "aws_connect_instance": resourceAwsConnectInstance(), "aws_cur_report_definition": resourceAwsCurReportDefinition(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_datapipeline_pipeline": resourceAwsDataPipelinePipeline(), @@ -566,6 +683,7 @@ func Provider() *schema.Provider { "aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_proxy": resourceAwsDbProxy(), "aws_db_proxy_default_target_group": resourceAwsDbProxyDefaultTargetGroup(), + "aws_db_proxy_endpoint": resourceAwsDbProxyEndpoint(), "aws_db_proxy_target": resourceAwsDbProxyTarget(), "aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_snapshot": resourceAwsDbSnapshot(), @@ -604,11 +722,14 @@ func Provider() *schema.Provider { "aws_dx_transit_virtual_interface": resourceAwsDxTransitVirtualInterface(), "aws_dynamodb_table": resourceAwsDynamoDbTable(), "aws_dynamodb_table_item": resourceAwsDynamoDbTableItem(), + "aws_dynamodb_tag": resourceAwsDynamodbTag(), "aws_dynamodb_global_table": resourceAwsDynamoDbGlobalTable(), + "aws_dynamodb_kinesis_streaming_destination": resourceAwsDynamoDbKinesisStreamingDestination(), "aws_ebs_default_kms_key": resourceAwsEbsDefaultKmsKey(), "aws_ebs_encryption_by_default": resourceAwsEbsEncryptionByDefault(), "aws_ebs_snapshot": resourceAwsEbsSnapshot(), "aws_ebs_snapshot_copy": resourceAwsEbsSnapshotCopy(), + "aws_ebs_snapshot_import": resourceAwsEbsSnapshotImport(), "aws_ebs_volume": resourceAwsEbsVolume(), "aws_ec2_availability_zone_group": resourceAwsEc2AvailabilityZoneGroup(), "aws_ec2_capacity_reservation": resourceAwsEc2CapacityReservation(), @@ -618,9 +739,11 @@ func Provider() *schema.Provider { "aws_ec2_client_vpn_network_association": resourceAwsEc2ClientVpnNetworkAssociation(), "aws_ec2_client_vpn_route": resourceAwsEc2ClientVpnRoute(), "aws_ec2_fleet": resourceAwsEc2Fleet(), + "aws_ec2_host": resourceAwsEc2Host(), "aws_ec2_local_gateway_route": resourceAwsEc2LocalGatewayRoute(), "aws_ec2_local_gateway_route_table_vpc_association": resourceAwsEc2LocalGatewayRouteTableVpcAssociation(), "aws_ec2_managed_prefix_list": resourceAwsEc2ManagedPrefixList(), + "aws_ec2_managed_prefix_list_entry": resourceAwsEc2ManagedPrefixListEntry(), "aws_ec2_tag": resourceAwsEc2Tag(), "aws_ec2_traffic_mirror_filter": resourceAwsEc2TrafficMirrorFilter(), "aws_ec2_traffic_mirror_filter_rule": resourceAwsEc2TrafficMirrorFilterRule(), @@ -637,13 +760,18 @@ func Provider() *schema.Provider { "aws_ec2_transit_gateway_vpc_attachment": resourceAwsEc2TransitGatewayVpcAttachment(), "aws_ec2_transit_gateway_vpc_attachment_accepter": resourceAwsEc2TransitGatewayVpcAttachmentAccepter(), "aws_ecr_lifecycle_policy": resourceAwsEcrLifecyclePolicy(), + "aws_ecrpublic_repository": resourceAwsEcrPublicRepository(), + "aws_ecr_registry_policy": resourceAwsEcrRegistryPolicy(), + "aws_ecr_replication_configuration": resourceAwsEcrReplicationConfiguration(), "aws_ecr_repository": resourceAwsEcrRepository(), "aws_ecr_repository_policy": resourceAwsEcrRepositoryPolicy(), "aws_ecs_capacity_provider": resourceAwsEcsCapacityProvider(), "aws_ecs_cluster": resourceAwsEcsCluster(), "aws_ecs_service": resourceAwsEcsService(), + "aws_ecs_tag": resourceAwsEcsTag(), "aws_ecs_task_definition": resourceAwsEcsTaskDefinition(), "aws_efs_access_point": resourceAwsEfsAccessPoint(), + "aws_efs_backup_policy": resourceAwsEfsBackupPolicy(), "aws_efs_file_system": resourceAwsEfsFileSystem(), "aws_efs_file_system_policy": resourceAwsEfsFileSystemPolicy(), "aws_efs_mount_target": resourceAwsEfsMountTarget(), @@ -651,19 +779,25 @@ func Provider() *schema.Provider { "aws_eip": resourceAwsEip(), "aws_eip_association": resourceAwsEipAssociation(), "aws_eks_cluster": resourceAwsEksCluster(), + "aws_eks_addon": resourceAwsEksAddon(), "aws_eks_fargate_profile": resourceAwsEksFargateProfile(), + "aws_eks_identity_provider_config": resourceAwsEksIdentityProviderConfig(), "aws_eks_node_group": resourceAwsEksNodeGroup(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), + "aws_elasticache_global_replication_group": resourceAwsElasticacheGlobalReplicationGroup(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), "aws_elasticache_replication_group": resourceAwsElasticacheReplicationGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), "aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(), + "aws_elasticache_user": resourceAwsElasticacheUser(), + "aws_elasticache_user_group": resourceAwsElasticacheUserGroup(), "aws_elastic_beanstalk_application": resourceAwsElasticBeanstalkApplication(), "aws_elastic_beanstalk_application_version": resourceAwsElasticBeanstalkApplicationVersion(), "aws_elastic_beanstalk_configuration_template": resourceAwsElasticBeanstalkConfigurationTemplate(), "aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(), "aws_elasticsearch_domain": resourceAwsElasticSearchDomain(), "aws_elasticsearch_domain_policy": resourceAwsElasticSearchDomainPolicy(), + "aws_elasticsearch_domain_saml_options": resourceAwsElasticSearchDomainSAMLOptions(), "aws_elastictranscoder_pipeline": resourceAwsElasticTranscoderPipeline(), "aws_elastictranscoder_preset": resourceAwsElasticTranscoderPreset(), "aws_elb": resourceAwsElb(), @@ -674,7 +808,9 @@ func Provider() *schema.Provider { "aws_emr_managed_scaling_policy": resourceAwsEMRManagedScalingPolicy(), "aws_emr_security_configuration": resourceAwsEMRSecurityConfiguration(), "aws_flow_log": resourceAwsFlowLog(), + "aws_fsx_backup": resourceAwsFsxBackup(), "aws_fsx_lustre_file_system": resourceAwsFsxLustreFileSystem(), + "aws_fsx_ontap_file_system": resourceAwsFsxOntapFileSystem(), "aws_fsx_windows_file_system": resourceAwsFsxWindowsFileSystem(), "aws_fms_admin_account": resourceAwsFmsAdminAccount(), "aws_fms_policy": resourceAwsFmsPolicy(), @@ -747,6 +883,7 @@ func Provider() *schema.Provider { "aws_inspector_resource_group": resourceAWSInspectorResourceGroup(), "aws_instance": resourceAwsInstance(), "aws_internet_gateway": resourceAwsInternetGateway(), + "aws_iot_authorizer": resourceAwsIoTAuthorizer(), "aws_iot_certificate": resourceAwsIotCertificate(), "aws_iot_policy": resourceAwsIotPolicy(), "aws_iot_policy_attachment": resourceAwsIotPolicyAttachment(), @@ -758,8 +895,10 @@ func Provider() *schema.Provider { "aws_key_pair": resourceAwsKeyPair(), "aws_kinesis_analytics_application": resourceAwsKinesisAnalyticsApplication(), "aws_kinesisanalyticsv2_application": resourceAwsKinesisAnalyticsV2Application(), + "aws_kinesisanalyticsv2_application_snapshot": resourceAwsKinesisAnalyticsV2ApplicationSnapshot(), "aws_kinesis_firehose_delivery_stream": resourceAwsKinesisFirehoseDeliveryStream(), "aws_kinesis_stream": resourceAwsKinesisStream(), + "aws_kinesis_stream_consumer": resourceAwsKinesisStreamConsumer(), "aws_kinesis_video_stream": resourceAwsKinesisVideoStream(), "aws_kms_alias": resourceAwsKmsAlias(), "aws_kms_external_key": resourceAwsKmsExternalKey(), @@ -787,6 +926,7 @@ func Provider() *schema.Provider { "aws_licensemanager_license_configuration": resourceAwsLicenseManagerLicenseConfiguration(), "aws_lightsail_domain": resourceAwsLightsailDomain(), "aws_lightsail_instance": resourceAwsLightsailInstance(), + "aws_lightsail_instance_public_ports": resourceAwsLightsailInstancePublicPorts(), "aws_lightsail_key_pair": resourceAwsLightsailKeyPair(), "aws_lightsail_static_ip": resourceAwsLightsailStaticIp(), "aws_lightsail_static_ip_attachment": resourceAwsLightsailStaticIpAttachment(), @@ -795,6 +935,13 @@ func Provider() *schema.Provider { "aws_load_balancer_backend_server_policy": resourceAwsLoadBalancerBackendServerPolicies(), "aws_load_balancer_listener_policy": resourceAwsLoadBalancerListenerPolicies(), "aws_lb_ssl_negotiation_policy": resourceAwsLBSSLNegotiationPolicy(), + "aws_macie2_account": resourceAwsMacie2Account(), + "aws_macie2_classification_job": resourceAwsMacie2ClassificationJob(), + "aws_macie2_custom_data_identifier": resourceAwsMacie2CustomDataIdentifier(), + "aws_macie2_findings_filter": resourceAwsMacie2FindingsFilter(), + "aws_macie2_invitation_accepter": resourceAwsMacie2InvitationAccepter(), + "aws_macie2_member": resourceAwsMacie2Member(), + "aws_macie2_organization_admin_account": resourceAwsMacie2OrganizationAdminAccount(), "aws_macie_member_account_association": resourceAwsMacieMemberAccountAssociation(), "aws_macie_s3_bucket_association": resourceAwsMacieS3BucketAssociation(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), @@ -807,10 +954,12 @@ func Provider() *schema.Provider { "aws_msk_cluster": resourceAwsMskCluster(), "aws_msk_configuration": resourceAwsMskConfiguration(), "aws_msk_scram_secret_association": resourceAwsMskScramSecretAssociation(), + "aws_mwaa_environment": resourceAwsMwaaEnvironment(), "aws_nat_gateway": resourceAwsNatGateway(), "aws_network_acl": resourceAwsNetworkAcl(), "aws_default_network_acl": resourceAwsDefaultNetworkAcl(), "aws_neptune_cluster": resourceAwsNeptuneCluster(), + "aws_neptune_cluster_endpoint": resourceAwsNeptuneClusterEndpoint(), "aws_neptune_cluster_instance": resourceAwsNeptuneClusterInstance(), "aws_neptune_cluster_parameter_group": resourceAwsNeptuneClusterParameterGroup(), "aws_neptune_cluster_snapshot": resourceAwsNeptuneClusterSnapshot(), @@ -843,6 +992,7 @@ func Provider() *schema.Provider { "aws_opsworks_rds_db_instance": resourceAwsOpsworksRdsDbInstance(), "aws_organizations_organization": resourceAwsOrganizationsOrganization(), "aws_organizations_account": resourceAwsOrganizationsAccount(), + "aws_organizations_delegated_administrator": resourceAwsOrganizationsDelegatedAdministrator(), "aws_organizations_policy": resourceAwsOrganizationsPolicy(), "aws_organizations_policy_attachment": resourceAwsOrganizationsPolicyAttachment(), "aws_organizations_organizational_unit": resourceAwsOrganizationsOrganizationalUnit(), @@ -850,7 +1000,9 @@ func Provider() *schema.Provider { "aws_prometheus_workspace": resourceAwsPrometheusWorkspace(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_qldb_ledger": resourceAwsQLDBLedger(), + "aws_quicksight_data_source": resourceAwsQuickSightDataSource(), "aws_quicksight_group": resourceAwsQuickSightGroup(), + "aws_quicksight_group_membership": resourceAwsQuickSightGroupMembership(), "aws_quicksight_user": resourceAwsQuickSightUser(), "aws_ram_principal_association": resourceAwsRamPrincipalAssociation(), "aws_ram_resource_association": resourceAwsRamResourceAssociation(), @@ -860,6 +1012,7 @@ func Provider() *schema.Provider { "aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), "aws_rds_cluster_parameter_group": resourceAwsRDSClusterParameterGroup(), + "aws_rds_cluster_role_association": resourceAwsRDSClusterRoleAssociation(), "aws_rds_global_cluster": resourceAwsRDSGlobalCluster(), "aws_redshift_cluster": resourceAwsRedshiftCluster(), "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), @@ -869,8 +1022,10 @@ func Provider() *schema.Provider { "aws_redshift_snapshot_schedule": resourceAwsRedshiftSnapshotSchedule(), "aws_redshift_snapshot_schedule_association": resourceAwsRedshiftSnapshotScheduleAssociation(), "aws_redshift_event_subscription": resourceAwsRedshiftEventSubscription(), + "aws_redshift_scheduled_action": resourceAwsRedshiftScheduledAction(), "aws_resourcegroups_group": resourceAwsResourceGroupsGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), + "aws_route53_hosted_zone_dnssec": resourceAwsRoute53HostedZoneDnssec(), "aws_route53_key_signing_key": resourceAwsRoute53KeySigningKey(), "aws_route53_query_log": resourceAwsRoute53QueryLog(), "aws_route53_record": resourceAwsRoute53Record(), @@ -880,27 +1035,50 @@ func Provider() *schema.Provider { "aws_route53_health_check": resourceAwsRoute53HealthCheck(), "aws_route53_resolver_dnssec_config": resourceAwsRoute53ResolverDnssecConfig(), "aws_route53_resolver_endpoint": resourceAwsRoute53ResolverEndpoint(), + "aws_route53_resolver_firewall_config": resourceAwsRoute53ResolverFirewallConfig(), + "aws_route53_resolver_firewall_domain_list": resourceAwsRoute53ResolverFirewallDomainList(), + "aws_route53_resolver_firewall_rule": resourceAwsRoute53ResolverFirewallRule(), + "aws_route53_resolver_firewall_rule_group": resourceAwsRoute53ResolverFirewallRuleGroup(), + "aws_route53_resolver_firewall_rule_group_association": resourceAwsRoute53ResolverFirewallRuleGroupAssociation(), "aws_route53_resolver_query_log_config": resourceAwsRoute53ResolverQueryLogConfig(), "aws_route53_resolver_query_log_config_association": resourceAwsRoute53ResolverQueryLogConfigAssociation(), "aws_route53_resolver_rule_association": resourceAwsRoute53ResolverRuleAssociation(), "aws_route53_resolver_rule": resourceAwsRoute53ResolverRule(), + "aws_route53recoverycontrolconfig_cluster": resourceAwsRoute53RecoveryControlConfigCluster(), + "aws_route53recoverycontrolconfig_control_panel": resourceAwsRoute53RecoveryControlConfigControlPanel(), + "aws_route53recoverycontrolconfig_routing_control": resourceAwsRoute53RecoveryControlConfigRoutingControl(), + "aws_route53recoverycontrolconfig_safety_rule": resourceAwsRoute53RecoveryControlConfigSafetyRule(), + "aws_route53recoveryreadiness_cell": resourceAwsRoute53RecoveryReadinessCell(), + "aws_route53recoveryreadiness_readiness_check": resourceAwsRoute53RecoveryReadinessReadinessCheck(), + "aws_route53recoveryreadiness_recovery_group": resourceAwsRoute53RecoveryReadinessRecoveryGroup(), + "aws_route53recoveryreadiness_resource_set": resourceAwsRoute53RecoveryReadinessResourceSet(), "aws_route": resourceAwsRoute(), "aws_route_table": resourceAwsRouteTable(), "aws_default_route_table": resourceAwsDefaultRouteTable(), "aws_route_table_association": resourceAwsRouteTableAssociation(), + "aws_sagemaker_app": resourceAwsSagemakerApp(), "aws_sagemaker_app_image_config": resourceAwsSagemakerAppImageConfig(), "aws_sagemaker_code_repository": resourceAwsSagemakerCodeRepository(), + "aws_sagemaker_device_fleet": resourceAwsSagemakerDeviceFleet(), "aws_sagemaker_domain": resourceAwsSagemakerDomain(), "aws_sagemaker_endpoint": resourceAwsSagemakerEndpoint(), "aws_sagemaker_endpoint_configuration": resourceAwsSagemakerEndpointConfiguration(), "aws_sagemaker_feature_group": resourceAwsSagemakerFeatureGroup(), + "aws_sagemaker_flow_definition": resourceAwsSagemakerFlowDefinition(), "aws_sagemaker_image": resourceAwsSagemakerImage(), "aws_sagemaker_image_version": resourceAwsSagemakerImageVersion(), + "aws_sagemaker_human_task_ui": resourceAwsSagemakerHumanTaskUi(), "aws_sagemaker_model": resourceAwsSagemakerModel(), "aws_sagemaker_model_package_group": resourceAwsSagemakerModelPackageGroup(), "aws_sagemaker_notebook_instance_lifecycle_configuration": resourceAwsSagemakerNotebookInstanceLifeCycleConfiguration(), "aws_sagemaker_notebook_instance": resourceAwsSagemakerNotebookInstance(), + "aws_sagemaker_studio_lifecycle_config": resourceAwsSagemakerStudioLifecycleConfig(), "aws_sagemaker_user_profile": resourceAwsSagemakerUserProfile(), + "aws_sagemaker_workforce": resourceAwsSagemakerWorkforce(), + "aws_sagemaker_workteam": resourceAwsSagemakerWorkteam(), + "aws_schemas_discoverer": resourceAwsSchemasDiscoverer(), + "aws_schemas_registry": resourceAwsSchemasRegistry(), + "aws_schemas_schema": resourceAwsSchemasSchema(), "aws_secretsmanager_secret": resourceAwsSecretsManagerSecret(), "aws_secretsmanager_secret_policy": resourceAwsSecretsManagerSecretPolicy(), "aws_secretsmanager_secret_version": resourceAwsSecretsManagerSecretVersion(), @@ -930,6 +1108,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_s3_bucket_metric": resourceAwsS3BucketMetric(), "aws_s3_bucket_inventory": resourceAwsS3BucketInventory(), + "aws_s3_object_copy": resourceAwsS3ObjectCopy(), "aws_s3control_bucket": resourceAwsS3ControlBucket(), "aws_s3control_bucket_policy": resourceAwsS3ControlBucketPolicy(), "aws_s3control_bucket_lifecycle_configuration": resourceAwsS3ControlBucketLifecycleConfiguration(), @@ -940,17 +1119,35 @@ func Provider() *schema.Provider { "aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_securityhub_account": resourceAwsSecurityHubAccount(), "aws_securityhub_action_target": resourceAwsSecurityHubActionTarget(), + "aws_securityhub_insight": resourceAwsSecurityHubInsight(), + "aws_securityhub_invite_accepter": resourceAwsSecurityHubInviteAccepter(), "aws_securityhub_member": resourceAwsSecurityHubMember(), "aws_securityhub_organization_admin_account": resourceAwsSecurityHubOrganizationAdminAccount(), + "aws_securityhub_organization_configuration": resourceAwsSecurityHubOrganizationConfiguration(), "aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(), + "aws_securityhub_standards_control": resourceAwsSecurityHubStandardsControl(), "aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(), + "aws_servicecatalog_budget_resource_association": resourceAwsServiceCatalogBudgetResourceAssociation(), + "aws_servicecatalog_constraint": resourceAwsServiceCatalogConstraint(), + "aws_servicecatalog_organizations_access": resourceAwsServiceCatalogOrganizationsAccess(), "aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(), + "aws_servicecatalog_portfolio_share": resourceAwsServiceCatalogPortfolioShare(), + "aws_servicecatalog_product": resourceAwsServiceCatalogProduct(), + "aws_servicecatalog_provisioned_product": resourceAwsServiceCatalogProvisionedProduct(), + "aws_servicecatalog_service_action": resourceAwsServiceCatalogServiceAction(), + "aws_servicecatalog_tag_option": resourceAwsServiceCatalogTagOption(), + "aws_servicecatalog_tag_option_resource_association": resourceAwsServiceCatalogTagOptionResourceAssociation(), + "aws_servicecatalog_principal_portfolio_association": resourceAwsServiceCatalogPrincipalPortfolioAssociation(), + "aws_servicecatalog_product_portfolio_association": resourceAwsServiceCatalogProductPortfolioAssociation(), + "aws_servicecatalog_provisioning_artifact": resourceAwsServiceCatalogProvisioningArtifact(), + "aws_service_discovery_instance": resourceAwsServiceDiscoveryInstance(), "aws_service_discovery_http_namespace": resourceAwsServiceDiscoveryHttpNamespace(), "aws_service_discovery_private_dns_namespace": resourceAwsServiceDiscoveryPrivateDnsNamespace(), "aws_service_discovery_public_dns_namespace": resourceAwsServiceDiscoveryPublicDnsNamespace(), "aws_service_discovery_service": resourceAwsServiceDiscoveryService(), "aws_servicequotas_service_quota": resourceAwsServiceQuotasServiceQuota(), "aws_shield_protection": resourceAwsShieldProtection(), + "aws_shield_protection_group": resourceAwsShieldProtectionGroup(), "aws_signer_signing_job": resourceAwsSignerSigningJob(), "aws_signer_signing_profile": resourceAwsSignerSigningProfile(), "aws_signer_signing_profile_permission": resourceAwsSignerSigningProfilePermission(), @@ -971,6 +1168,7 @@ func Provider() *schema.Provider { "aws_ssoadmin_permission_set_inline_policy": resourceAwsSsoAdminPermissionSetInlinePolicy(), "aws_storagegateway_cache": resourceAwsStorageGatewayCache(), "aws_storagegateway_cached_iscsi_volume": resourceAwsStorageGatewayCachedIscsiVolume(), + "aws_storagegateway_file_system_association": resourceAwsStorageGatewayFileSystemAssociation(), "aws_storagegateway_gateway": resourceAwsStorageGatewayGateway(), "aws_storagegateway_nfs_file_share": resourceAwsStorageGatewayNfsFileShare(), "aws_storagegateway_smb_file_share": resourceAwsStorageGatewaySmbFileShare(), @@ -995,7 +1193,10 @@ func Provider() *schema.Provider { "aws_subnet": resourceAwsSubnet(), "aws_swf_domain": resourceAwsSwfDomain(), "aws_synthetics_canary": resourceAwsSyntheticsCanary(), + "aws_timestreamwrite_database": resourceAwsTimestreamWriteDatabase(), + "aws_timestreamwrite_table": resourceAwsTimestreamWriteTable(), "aws_transfer_server": resourceAwsTransferServer(), + "aws_transfer_access": resourceAwsTransferAccess(), "aws_transfer_ssh_key": resourceAwsTransferSshKey(), "aws_transfer_user": resourceAwsTransferUser(), "aws_volume_attachment": resourceAwsVolumeAttachment(), @@ -1137,9 +1338,12 @@ func init() { "being executed. If the API request still fails, an error is\n" + "thrown.", + "http_proxy": "The address of an HTTP proxy to use when accessing the AWS API. " + + "Can also be configured using the `HTTP_PROXY` or `HTTPS_PROXY` environment variables.", + "endpoint": "Use this to override the default service endpoint URL", - "insecure": "Explicitly allow the provider to perform \"insecure\" SSL requests. If omitted," + + "insecure": "Explicitly allow the provider to perform \"insecure\" SSL requests. If omitted, " + "default value is `false`", "skip_credentials_validation": "Skip the credentials validation via STS API. " + @@ -1169,18 +1373,23 @@ func init() { "acmpca", "amplify", "apigateway", + "appconfig", "applicationautoscaling", "applicationinsights", "appmesh", + "apprunner", "appstream", "appsync", "athena", + "auditmanager", "autoscaling", "autoscalingplans", "backup", "batch", "budgets", + "chime", "cloud9", + "cloudcontrolapi", "cloudformation", "cloudfront", "cloudhsm", @@ -1204,6 +1413,7 @@ func init() { "datapipeline", "datasync", "dax", + "detective", "devicefarm", "directconnect", "dlm", @@ -1252,6 +1462,7 @@ func init() { "lexmodels", "licensemanager", "lightsail", + "location", "macie", "macie2", "managedblockchain", @@ -1262,6 +1473,7 @@ func init() { "mediapackage", "mediastore", "mediastoredata", + "memorydb", "mq", "mwaa", "neptune", @@ -1282,11 +1494,14 @@ func init() { "resourcegroupstaggingapi", "route53", "route53domains", + "route53recoverycontrolconfig", + "route53recoveryreadiness", "route53resolver", "s3", "s3control", "s3outposts", "sagemaker", + "schemas", "sdb", "secretsmanager", "securityhub", @@ -1326,10 +1541,12 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa Token: d.Get("token").(string), Region: d.Get("region").(string), CredsFilename: d.Get("shared_credentials_file").(string), + DefaultTagsConfig: expandProviderDefaultTags(d.Get("default_tags").([]interface{})), Endpoints: make(map[string]string), MaxRetries: d.Get("max_retries").(int), IgnoreTagsConfig: expandProviderIgnoreTags(d.Get("ignore_tags").([]interface{})), Insecure: d.Get("insecure").(bool), + HTTPProxy: d.Get("http_proxy").(string), SkipCredsValidation: d.Get("skip_credentials_validation").(bool), SkipGetEC2Platforms: d.Get("skip_get_ec2_platforms").(bool), SkipRegionValidation: d.Get("skip_region_validation").(bool), @@ -1448,20 +1665,25 @@ func assumeRoleSchema() *schema.Schema { Description: "Unique identifier that might be required for assuming a role in another account.", }, "policy": { - Type: schema.TypeString, - Optional: true, - Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + Type: schema.TypeString, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + ValidateFunc: validation.StringIsJSON, }, "policy_arns": { Type: schema.TypeSet, Optional: true, Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateArn, + }, }, "role_arn": { - Type: schema.TypeString, - Optional: true, - Description: "Amazon Resource Name of an IAM Role to assume prior to making API calls.", + Type: schema.TypeString, + Optional: true, + Description: "Amazon Resource Name of an IAM Role to assume prior to making API calls.", + ValidateFunc: validateArn, }, "session_name": { Type: schema.TypeString, @@ -1506,6 +1728,20 @@ func endpointsSchema() *schema.Schema { } } +func expandProviderDefaultTags(l []interface{}) *keyvaluetags.DefaultConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + defaultConfig := &keyvaluetags.DefaultConfig{} + m := l[0].(map[string]interface{}) + + if v, ok := m["tags"].(map[string]interface{}); ok { + defaultConfig.Tags = keyvaluetags.New(v) + } + return defaultConfig +} + func expandProviderIgnoreTags(l []interface{}) *keyvaluetags.IgnoreConfig { if len(l) == 0 || l[0] == nil { return nil diff --git a/aws/provider_test.go b/aws/provider_test.go index 77627e2155ab..bae5d844fce8 100644 --- a/aws/provider_test.go +++ b/aws/provider_test.go @@ -7,6 +7,7 @@ import ( "os" "reflect" "regexp" + "sort" "strings" "sync" "testing" @@ -17,13 +18,15 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/organizations" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/terraform-providers/terraform-provider-aws/aws/internal/envvar" + organizationsfinder "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/organizations/finder" + stsfinder "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sts/finder" ) const ( @@ -47,7 +50,6 @@ const ( ) const rfc3339RegexPattern = `^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?([Zz]|([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$` -const uuidRegexPattern = `[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[ab89][a-f0-9]{3}-[a-f0-9]{12}` // TestAccSkip implements a wrapper for (*testing.T).Skip() to prevent unused linting reports // @@ -364,6 +366,17 @@ func testAccCheckResourceAttrHostnameWithPort(resourceName, attributeName, servi } } +// testAccCheckResourceAttrPrivateDnsName ensures the Terraform state exactly matches a private DNS name +// +// For example: ip-172-16-10-100.us-west-2.compute.internal +func testAccCheckResourceAttrPrivateDnsName(resourceName, attributeName string, privateIpAddress **string) resource.TestCheckFunc { + return func(s *terraform.State) error { + privateDnsName := fmt.Sprintf("ip-%s.%s", resourceAwsEc2DashIP(**privateIpAddress), resourceAwsEc2RegionalPrivateDnsSuffix(testAccGetRegion())) + + return resource.TestCheckResourceAttr(resourceName, attributeName, privateDnsName)(s) + } +} + // testAccMatchResourceAttrAccountID ensures the Terraform state regexp matches an account ID func testAccMatchResourceAttrAccountID(resourceName, attributeName string) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -699,10 +712,10 @@ func testAccGetThirdRegionPartition() string { } func testAccAlternateAccountPreCheck(t *testing.T) { - envvar.TestFailIfAllEmpty(t, []string{envvar.AwsAlternateProfile, envvar.AwsAlternateAccessKeyId}, "credentials for running acceptance testing in alternate AWS account") + envvar.TestSkipIfAllEmpty(t, []string{envvar.AwsAlternateProfile, envvar.AwsAlternateAccessKeyId}, "credentials for running acceptance testing in alternate AWS account") if os.Getenv(envvar.AwsAlternateAccessKeyId) != "" { - envvar.TestFailIfEmpty(t, envvar.AwsAlternateSecretAccessKey, "static credentials value when using "+envvar.AwsAlternateAccessKeyId) + envvar.TestSkipIfEmpty(t, envvar.AwsAlternateSecretAccessKey, "static credentials value when using "+envvar.AwsAlternateAccessKeyId) } } @@ -793,6 +806,43 @@ func testAccOrganizationsEnabledPreCheck(t *testing.T) { } } +func testAccOrganizationManagementAccountPreCheck(t *testing.T) { + organization, err := organizationsfinder.Organization(testAccProvider.Meta().(*AWSClient).organizationsconn) + + if err != nil { + t.Fatalf("error describing AWS Organization: %s", err) + } + + callerIdentity, err := stsfinder.CallerIdentity(testAccProvider.Meta().(*AWSClient).stsconn) + + if err != nil { + t.Fatalf("error getting current identity: %s", err) + } + + if aws.StringValue(organization.MasterAccountId) != aws.StringValue(callerIdentity.Account) { + t.Skip("this AWS account must be the management account of an AWS Organization") + } +} + +func testAccPreCheckHasIAMRole(t *testing.T, roleName string) { + conn := testAccProvider.Meta().(*AWSClient).iamconn + + input := &iam.GetRoleInput{ + RoleName: aws.String(roleName), + } + _, err := conn.GetRole(input) + + if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { + t.Skipf("skipping acceptance test: required IAM role \"%s\" is not present", roleName) + } + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance test: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + func testAccPreCheckIamServiceLinkedRole(t *testing.T, pathPrefix string) { conn := testAccProvider.Meta().(*AWSClient).iamconn @@ -887,6 +937,38 @@ func testAccMultipleRegionProviderConfig(regions int) string { return config.String() } +func testAccProviderConfigDefaultAndIgnoreTagsKeyPrefixes1(key1, value1, keyPrefix1 string) string { + //lintignore:AT004 + return fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + } + } + ignore_tags { + key_prefixes = [%q] + } +} +`, key1, value1, keyPrefix1) +} + +func testAccProviderConfigDefaultAndIgnoreTagsKeys1(key1, value1 string) string { + //lintignore:AT004 + return fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %[1]q = %q + } + } + ignore_tags { + keys = [%[1]q] + } +} +`, key1, value1) +} + func testAccProviderConfigIgnoreTagsKeyPrefixes1(keyPrefix1 string) string { //lintignore:AT004 return fmt.Sprintf(` @@ -973,6 +1055,28 @@ func testAccAwsRegionProviderFunc(region string, providers *[]*schema.Provider) } } +func testAccDeleteResource(resource *schema.Resource, d *schema.ResourceData, meta interface{}) error { + if resource.DeleteContext != nil || resource.DeleteWithoutTimeout != nil { + var diags diag.Diagnostics + + if resource.DeleteContext != nil { + diags = resource.DeleteContext(context.Background(), d, meta) + } else { + diags = resource.DeleteWithoutTimeout(context.Background(), d, meta) + } + + for i := range diags { + if diags[i].Severity == diag.Error { + return fmt.Errorf("error deleting resource: %s", diags[i].Summary) + } + } + + return nil + } + + return resource.Delete(d, meta) +} + func testAccCheckResourceDisappears(provider *schema.Provider, resource *schema.Resource, resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[resourceName] @@ -985,19 +1089,7 @@ func testAccCheckResourceDisappears(provider *schema.Provider, resource *schema. return fmt.Errorf("resource ID missing: %s", resourceName) } - if resource.DeleteContext != nil { - diags := resource.DeleteContext(context.Background(), resource.Data(resourceState.Primary), provider.Meta()) - - for i := range diags { - if diags[i].Severity == diag.Error { - return fmt.Errorf("error deleting resource: %s", diags[i].Summary) - } - } - - return nil - } - - return resource.Delete(resource.Data(resourceState.Primary), provider.Meta()) + return testAccDeleteResource(resource, resource.Data(resourceState.Primary), provider.Meta()) } } @@ -1022,12 +1114,13 @@ func testAccCheckWithProviders(f func(*terraform.State, *schema.Provider) error, func testAccErrorCheckSkipMessagesContaining(t *testing.T, messages ...string) resource.ErrorCheckFunc { return func(err error) error { if err == nil { - return err + return nil } for _, message := range messages { - if strings.Contains(err.Error(), message) { - t.Skipf("skipping test for %s/%s: %s", testAccGetPartition(), testAccGetRegion(), err.Error()) + errorMessage := err.Error() + if strings.Contains(errorMessage, message) { + t.Skipf("skipping test for %s/%s: %s", testAccGetPartition(), testAccGetRegion(), errorMessage) } } @@ -1035,86 +1128,209 @@ func testAccErrorCheckSkipMessagesContaining(t *testing.T, messages ...string) r } } -// Check service API call error for reasons to skip acceptance testing -// These include missing API endpoints and unsupported API calls -func testAccPreCheckSkipError(err error) bool { - // GovCloud has endpoints that respond with (no message provided after the error code): - // AccessDeniedException: - // Ignore these API endpoints that exist but are not officially enabled - if isAWSErr(err, "AccessDeniedException", "") { - return true +type ServiceErrorCheckFunc func(*testing.T) resource.ErrorCheckFunc + +var serviceErrorCheckFuncs map[string]ServiceErrorCheckFunc + +func RegisterServiceErrorCheckFunc(endpointID string, f ServiceErrorCheckFunc) { + if serviceErrorCheckFuncs == nil { + serviceErrorCheckFuncs = make(map[string]ServiceErrorCheckFunc) } - // Ignore missing API endpoints - if isAWSErr(err, "RequestError", "send request failed") { + + if _, ok := serviceErrorCheckFuncs[endpointID]; ok { + // already registered + panic(fmt.Sprintf("Cannot re-register a service! ServiceErrorCheckFunc exists for %s", endpointID)) //lintignore:R009 + } + + serviceErrorCheckFuncs[endpointID] = f +} + +func testAccErrorCheck(t *testing.T, endpointIDs ...string) resource.ErrorCheckFunc { + return func(err error) error { + if err == nil { + return nil + } + + for _, endpointID := range endpointIDs { + if f, ok := serviceErrorCheckFuncs[endpointID]; ok { + ef := f(t) + err = ef(err) + } + + if err == nil { + break + } + } + + if testAccErrorCheckCommon(err) { + t.Skipf("skipping test for %s/%s: %s", testAccGetPartition(), testAccGetRegion(), err.Error()) + } + + return err + } +} + +// NOTE: This function cannot use the standard tfawserr helpers +// as it is receiving error strings from the SDK testing framework, +// not actual error types from the resource logic. +func testAccErrorCheckCommon(err error) bool { + if strings.Contains(err.Error(), "is not supported in this") { return true } - // Ignore unsupported API calls - if isAWSErr(err, "UnknownOperationException", "") { + + if strings.Contains(err.Error(), "is currently not supported") { return true } - if isAWSErr(err, "UnsupportedOperation", "") { + + if strings.Contains(err.Error(), "InvalidAction") { return true } - if isAWSErr(err, "InvalidInputException", "Unknown operation") { + + if strings.Contains(err.Error(), "Unknown operation") { return true } - if isAWSErr(err, "InvalidAction", "is not valid") { + + if strings.Contains(err.Error(), "UnknownOperationException") { return true } - if isAWSErr(err, "InvalidAction", "Unavailable Operation") { + + if strings.Contains(err.Error(), "UnsupportedOperation") { return true } + return false } -// Check sweeper API call error for reasons to skip sweeping +// Check service API call error for reasons to skip acceptance testing // These include missing API endpoints and unsupported API calls -func testSweepSkipSweepError(err error) bool { +func testAccPreCheckSkipError(err error) bool { + // GovCloud has endpoints that respond with (no message provided after the error code): + // AccessDeniedException: + // Ignore these API endpoints that exist but are not officially enabled + if isAWSErr(err, "AccessDeniedException", "") { + return true + } // Ignore missing API endpoints if isAWSErr(err, "RequestError", "send request failed") { return true } // Ignore unsupported API calls - if isAWSErr(err, "UnsupportedOperation", "") { - return true - } - // Ignore more unsupported API calls - // InvalidParameterValue: Use of cache security groups is not permitted in this API version for your account. - if isAWSErr(err, "InvalidParameterValue", "not permitted in this API version for your account") { - return true - } - // InvalidParameterValue: Access Denied to API Version: APIGlobalDatabases - if isAWSErr(err, "InvalidParameterValue", "Access Denied to API Version") { + if isAWSErr(err, "UnknownOperationException", "") { return true } - // GovCloud has endpoints that respond with (no message provided): - // AccessDeniedException: - // Since acceptance test sweepers are best effort and this response is very common, - // we allow bypassing this error globally instead of individual test sweeper fixes. - if isAWSErr(err, "AccessDeniedException", "") { + if isAWSErr(err, "UnsupportedOperation", "") { return true } - // Example: BadRequestException: vpc link not supported for region us-gov-west-1 - if isAWSErr(err, "BadRequestException", "not supported") { + if isAWSErr(err, "InvalidInputException", "Unknown operation") { return true } - // Example: InvalidAction: The action DescribeTransitGatewayAttachments is not valid for this web service if isAWSErr(err, "InvalidAction", "is not valid") { return true } - // For example from GovCloud SES.SetActiveReceiptRuleSet. if isAWSErr(err, "InvalidAction", "Unavailable Operation") { return true } return false } -// Check sweeper API call error for reasons to skip a specific resource -// These include AccessDenied or AccessDeniedException for individual resources, e.g. managed by central IT -func testSweepSkipResourceError(err error) bool { - // Since acceptance test sweepers are best effort, we allow bypassing this error globally - // instead of individual test sweeper fixes. - return tfawserr.ErrCodeContains(err, "AccessDenied") +func TestAccAWSProvider_DefaultTags_EmptyConfigurationBlock(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTagsEmptyConfigurationBlock(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_None(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags0(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_One(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags1("test", "value"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{"test": "value"}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_Multiple(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags2("test1", "value1", "test2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{ + "test1": "value1", + "test2": "value2", + }), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultAndIgnoreTags_EmptyConfigurationBlocks(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultAndIgnoreTagsEmptyConfigurationBlock(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + testAccCheckAWSProviderIgnoreTagsKeys(&providers, []string{}), + testAccCheckAWSProviderIgnoreTagsKeyPrefixes(&providers, []string{}), + ), + }, + }, + }) } func TestAccAWSProvider_Endpoints(t *testing.T) { @@ -1128,6 +1344,7 @@ func TestAccAWSProvider_Endpoints(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1146,6 +1363,7 @@ func TestAccAWSProvider_IgnoreTags_EmptyConfigurationBlock(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1165,6 +1383,7 @@ func TestAccAWSProvider_IgnoreTags_KeyPrefixes_None(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1183,6 +1402,7 @@ func TestAccAWSProvider_IgnoreTags_KeyPrefixes_One(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1201,6 +1421,7 @@ func TestAccAWSProvider_IgnoreTags_KeyPrefixes_Multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1219,6 +1440,7 @@ func TestAccAWSProvider_IgnoreTags_Keys_None(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1237,6 +1459,7 @@ func TestAccAWSProvider_IgnoreTags_Keys_One(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1255,6 +1478,7 @@ func TestAccAWSProvider_IgnoreTags_Keys_Multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1273,6 +1497,7 @@ func TestAccAWSProvider_Region_AwsC2S(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1294,6 +1519,7 @@ func TestAccAWSProvider_Region_AwsChina(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1315,6 +1541,7 @@ func TestAccAWSProvider_Region_AwsCommercial(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1336,6 +1563,7 @@ func TestAccAWSProvider_Region_AwsGovCloudUs(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1357,6 +1585,7 @@ func TestAccAWSProvider_Region_AwsSC2S(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1378,6 +1607,7 @@ func TestAccAWSProvider_AssumeRole_Empty(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t), ProviderFactories: testAccProviderFactoriesInternal(&providers), CheckDestroy: nil, Steps: []resource.TestStep{ @@ -1612,6 +1842,69 @@ func testAccCheckAWSProviderIgnoreTagsKeys(providers *[]*schema.Provider, expect } } +func testAccCheckProviderDefaultTags_Tags(providers *[]*schema.Provider, expectedTags map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if providers == nil { + return fmt.Errorf("no providers initialized") + } + + for _, provider := range *providers { + if provider == nil || provider.Meta() == nil || provider.Meta().(*AWSClient) == nil { + continue + } + + providerClient := provider.Meta().(*AWSClient) + defaultTagsConfig := providerClient.DefaultTagsConfig + + if defaultTagsConfig == nil || len(defaultTagsConfig.Tags) == 0 { + if len(expectedTags) != 0 { + return fmt.Errorf("expected keys (%d) length, got: 0", len(expectedTags)) + } + + continue + } + + actualTags := defaultTagsConfig.Tags + + if len(actualTags) != len(expectedTags) { + return fmt.Errorf("expected tags (%d) length, got: %d", len(expectedTags), len(actualTags)) + } + + for _, expectedElement := range expectedTags { + var found bool + + for _, actualElement := range actualTags { + if aws.StringValue(actualElement.Value) == expectedElement { + found = true + break + } + } + + if !found { + return fmt.Errorf("expected tags element, but was missing: %s", expectedElement) + } + } + + for _, actualElement := range actualTags { + var found bool + + for _, expectedElement := range expectedTags { + if aws.StringValue(actualElement.Value) == expectedElement { + found = true + break + } + } + + if !found { + return fmt.Errorf("unexpected tags element: %s", actualElement) + } + } + } + + return nil + } +} + func testAccCheckAWSProviderPartition(providers *[]*schema.Provider, expectedPartition string) resource.TestCheckFunc { return func(s *terraform.State) error { if providers == nil { @@ -1708,72 +2001,147 @@ func testAccDefaultSubnetCount(t *testing.T) int { return len(output.Subnets) } -func testAccAWSProviderConfigEndpoints(endpoints string) string { +func testAccAWSProviderConfigDefaultTags_Tags0() string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true +} +`) +} - endpoints { - %[1]s +func testAccAWSProviderConfigDefaultTags_Tags1(tag1, value1 string) string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + } } + + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true +} +`, tag1, value1)) } -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigDefaultTags_Tags2(tag1, value1, tag2, value2 string) string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + %q = %q + } + } -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -`, endpoints) +`, tag1, value1, tag2, value2)) } -func testAccAWSProviderConfigIgnoreTagsEmptyConfigurationBlock() string { +func testAccAWSProviderConfigDefaultTagsEmptyConfigurationBlock() string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { - ignore_tags {} + default_tags {} skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true } +`) +} -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigDefaultAndIgnoreTagsEmptyConfigurationBlock() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + default_tags {} + ignore_tags {} -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -` +`) } -func testAccAWSProviderConfigIgnoreTagsKeyPrefixes0() string { +func testAccAWSProviderConfigEndpoints(endpoints string) string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true + + endpoints { + %[1]s + } +} +`, endpoints)) } -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigIgnoreTagsEmptyConfigurationBlock() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + ignore_tags {} -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -` +`) +} + +func testAccAWSProviderConfigIgnoreTagsKeyPrefixes0() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true +} +`) } func testAccAWSProviderConfigIgnoreTagsKeyPrefixes1(tagPrefix1 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { key_prefixes = [%[1]q] @@ -1784,19 +2152,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tagPrefix1) +`, tagPrefix1)) } func testAccAWSProviderConfigIgnoreTagsKeyPrefixes2(tagPrefix1, tagPrefix2 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { key_prefixes = [%[1]q, %[2]q] @@ -1807,38 +2170,28 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tagPrefix1, tagPrefix2) +`, tagPrefix1, tagPrefix2)) } func testAccAWSProviderConfigIgnoreTagsKeys0() string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -` +`) } func testAccAWSProviderConfigIgnoreTagsKeys1(tag1 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { keys = [%[1]q] @@ -1849,19 +2202,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tag1) +`, tag1)) } func testAccAWSProviderConfigIgnoreTagsKeys2(tag1, tag2 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { keys = [%[1]q, %[2]q] @@ -1872,19 +2220,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tag1, tag2) +`, tag1, tag2)) } func testAccAWSProviderConfigRegion(region string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { region = %[1]q skip_credentials_validation = true @@ -1892,14 +2235,7 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, region) +`, region)) } func testAccAssumeRoleARNPreCheck(t *testing.T) { @@ -1927,6 +2263,36 @@ provider "aws" { data "aws_caller_identity" "current" {} ` //lintignore:AT004 +const testAccProviderConfigBase = ` +data "aws_partition" "provider_test" {} + +# Required to initialize the provider +data "aws_arn" "test" { + arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" +} +` + +func testCheckResourceAttrIsSortedCsv(resourceName, attributeName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + is, err := primaryInstanceState(s, resourceName) + if err != nil { + return err + } + + v, ok := is.Attributes[attributeName] + if !ok { + return fmt.Errorf("%s: No attribute %q found", resourceName, attributeName) + } + + splitV := strings.Split(v, ",") + if !sort.StringsAreSorted(splitV) { + return fmt.Errorf("%s: Expected attribute %q to be sorted, got %q", resourceName, attributeName, v) + } + + return nil + } +} + // composeConfig can be called to concatenate multiple strings to build test configurations func composeConfig(config ...string) string { var str strings.Builder @@ -1937,3 +2303,63 @@ func composeConfig(config ...string) string { return str.String() } + +type domainName string + +// The top level domain ".test" is reserved by IANA for testing purposes: +// https://datatracker.ietf.org/doc/html/rfc6761 +const domainNameTestTopLevelDomain domainName = "test" + +// testAccRandomSubdomain creates a random three-level domain name in the form +// "..test" +// The top level domain ".test" is reserved by IANA for testing purposes: +// https://datatracker.ietf.org/doc/html/rfc6761 +func testAccRandomSubdomain() string { + return string(testAccRandomDomain().RandomSubdomain()) +} + +// testAccRandomDomainName creates a random two-level domain name in the form +// ".test" +// The top level domain ".test" is reserved by IANA for testing purposes: +// https://datatracker.ietf.org/doc/html/rfc6761 +func testAccRandomDomainName() string { + return string(testAccRandomDomain()) +} + +// testAccRandomFQDomainName creates a random fully-qualified two-level domain name in the form +// ".test." +// The top level domain ".test" is reserved by IANA for testing purposes: +// https://datatracker.ietf.org/doc/html/rfc6761 +func testAccRandomFQDomainName() string { + return string(testAccRandomDomain().FQDN()) +} + +func (d domainName) Subdomain(name string) domainName { + return domainName(fmt.Sprintf("%s.%s", name, d)) +} + +func (d domainName) RandomSubdomain() domainName { + return d.Subdomain(acctest.RandString(8)) +} + +func (d domainName) FQDN() domainName { + return domainName(fmt.Sprintf("%s.", d)) +} + +func (d domainName) String() string { + return string(d) +} + +func testAccRandomDomain() domainName { + return domainNameTestTopLevelDomain.RandomSubdomain() +} + +// testAccDefaultEmailAddress is the default email address to set as a +// resource or data source parameter for acceptance tests. +const testAccDefaultEmailAddress = "no-reply@hashicorp.com" + +// testAccRandomEmailAddress generates a random email address in the form +// "tf-acc-test-@" +func testAccRandomEmailAddress(domainName string) string { + return fmt.Sprintf("%s@%s", acctest.RandomWithPrefix("tf-acc-test"), domainName) +} diff --git a/aws/random_test.go b/aws/random_test.go new file mode 100644 index 000000000000..8e7cb0070801 --- /dev/null +++ b/aws/random_test.go @@ -0,0 +1,55 @@ +package aws + +import ( + "bytes" + crand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + + "golang.org/x/crypto/ssh" +) + +// RandSSHKeyPair generates a public and private SSH key pair. The public key is +// returned in OpenSSH format, and the private key is PEM encoded. +// Copied from github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest, +// with the addition of the key size +func RandSSHKeyPairSize(keySize int, comment string) (string, string, error) { + privateKey, privateKeyPEM, err := genPrivateKey(keySize) + if err != nil { + return "", "", err + } + + publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return "", "", err + } + keyMaterial := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))) + return fmt.Sprintf("%s %s", keyMaterial, comment), privateKeyPEM, nil +} + +func genPrivateKey(keySize int) (*rsa.PrivateKey, string, error) { + privateKey, err := rsa.GenerateKey(crand.Reader, keySize) + if err != nil { + return nil, "", err + } + + privateKeyPEM, err := pemEncode(x509.MarshalPKCS1PrivateKey(privateKey), "RSA PRIVATE KEY") + if err != nil { + return nil, "", err + } + + return privateKey, privateKeyPEM, nil +} + +func pemEncode(b []byte, block string) (string, error) { + var buf bytes.Buffer + pb := &pem.Block{Type: block, Bytes: b} + if err := pem.Encode(&buf, pb); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/aws/resource_aws_accessanalyzer_analyzer.go b/aws/resource_aws_accessanalyzer_analyzer.go index c07154b97fc5..afc53938bdd9 100644 --- a/aws/resource_aws_accessanalyzer_analyzer.go +++ b/aws/resource_aws_accessanalyzer_analyzer.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/accessanalyzer" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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" @@ -46,7 +47,8 @@ func resourceAwsAccessAnalyzerAnalyzer() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "type": { Type: schema.TypeString, Optional: true, @@ -58,17 +60,21 @@ func resourceAwsAccessAnalyzerAnalyzer() *schema.Resource { }, false), }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAccessAnalyzerAnalyzerCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).accessanalyzerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) analyzerName := d.Get("analyzer_name").(string) input := &accessanalyzer.CreateAnalyzerInput{ AnalyzerName: aws.String(analyzerName), ClientToken: aws.String(resource.UniqueId()), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AccessanalyzerTags(), + Tags: tags.IgnoreAws().AccessanalyzerTags(), Type: aws.String(d.Get("type").(string)), } @@ -102,6 +108,7 @@ func resourceAwsAccessAnalyzerAnalyzerCreate(d *schema.ResourceData, meta interf func resourceAwsAccessAnalyzerAnalyzerRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).accessanalyzerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &accessanalyzer.GetAnalyzerInput{ @@ -110,7 +117,7 @@ func resourceAwsAccessAnalyzerAnalyzerRead(d *schema.ResourceData, meta interfac output, err := conn.GetAnalyzer(input) - if isAWSErr(err, accessanalyzer.ErrCodeResourceNotFoundException, "") { + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, accessanalyzer.ErrCodeResourceNotFoundException) { log.Printf("[WARN] Access Analyzer Analyzer (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -127,8 +134,15 @@ func resourceAwsAccessAnalyzerAnalyzerRead(d *schema.ResourceData, meta interfac d.Set("analyzer_name", output.Analyzer.Name) d.Set("arn", output.Analyzer.Arn) - if err := d.Set("tags", keyvaluetags.AccessanalyzerKeyValueTags(output.Analyzer.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.AccessanalyzerKeyValueTags(output.Analyzer.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } d.Set("type", output.Analyzer.Type) @@ -139,8 +153,8 @@ func resourceAwsAccessAnalyzerAnalyzerRead(d *schema.ResourceData, meta interfac func resourceAwsAccessAnalyzerAnalyzerUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).accessanalyzerconn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AccessanalyzerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating Access Analyzer Analyzer (%s) tags: %s", d.Id(), err) } diff --git a/aws/resource_aws_accessanalyzer_analyzer_test.go b/aws/resource_aws_accessanalyzer_analyzer_test.go index a0b94018a153..2ae070d71734 100644 --- a/aws/resource_aws_accessanalyzer_analyzer_test.go +++ b/aws/resource_aws_accessanalyzer_analyzer_test.go @@ -20,6 +20,7 @@ func testAccAWSAccessAnalyzerAnalyzer_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAccessAnalyzer(t) }, + ErrorCheck: testAccErrorCheck(t, accessanalyzer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAccessAnalyzerAnalyzerDestroy, Steps: []resource.TestStep{ @@ -51,6 +52,7 @@ func testAccAWSAccessAnalyzerAnalyzer_disappears(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAccessAnalyzer(t) }, + ErrorCheck: testAccErrorCheck(t, accessanalyzer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAccessAnalyzerAnalyzerDestroy, Steps: []resource.TestStep{ @@ -75,6 +77,7 @@ func testAccAWSAccessAnalyzerAnalyzer_Tags(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAccessAnalyzer(t) }, + ErrorCheck: testAccErrorCheck(t, accessanalyzer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAccessAnalyzerAnalyzerDestroy, Steps: []resource.TestStep{ @@ -125,6 +128,7 @@ func testAccAWSAccessAnalyzerAnalyzer_Type_Organization(t *testing.T) { testAccPreCheckAWSAccessAnalyzer(t) testAccOrganizationsAccountPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, accessanalyzer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAccessAnalyzerAnalyzerDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_acm_certificate.go b/aws/resource_aws_acm_certificate.go index e6326c969097..56044fe46ea7 100644 --- a/aws/resource_aws_acm_certificate.go +++ b/aws/resource_aws_acm_certificate.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "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" @@ -163,44 +164,48 @@ func resourceAwsAcmCertificate() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, - CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // Attempt to calculate the domain validation options based on domains present in domain_name and subject_alternative_names - if diff.Get("validation_method").(string) == "DNS" && (diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names")) { - domainValidationOptionsList := []interface{}{map[string]interface{}{ - // AWS Provider 3.0 -- plan-time validation prevents "domain_name" - // argument to accept a string with trailing period; thus, trim of trailing period - // no longer required here - "domain_name": diff.Get("domain_name").(string), - }} - - if sanSet, ok := diff.Get("subject_alternative_names").(*schema.Set); ok { - for _, sanRaw := range sanSet.List() { - san, ok := sanRaw.(string) - - if !ok { - continue + CustomizeDiff: customdiff.Sequence( + func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // Attempt to calculate the domain validation options based on domains present in domain_name and subject_alternative_names + if diff.Get("validation_method").(string) == "DNS" && (diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names")) { + domainValidationOptionsList := []interface{}{map[string]interface{}{ + // AWS Provider 3.0 -- plan-time validation prevents "domain_name" + // argument to accept a string with trailing period; thus, trim of trailing period + // no longer required here + "domain_name": diff.Get("domain_name").(string), + }} + + if sanSet, ok := diff.Get("subject_alternative_names").(*schema.Set); ok { + for _, sanRaw := range sanSet.List() { + san, ok := sanRaw.(string) + + if !ok { + continue + } + + m := map[string]interface{}{ + // AWS Provider 3.0 -- plan-time validation prevents "subject_alternative_names" + // argument to accept strings with trailing period; thus, trim of trailing period + // no longer required here + "domain_name": san, + } + + domainValidationOptionsList = append(domainValidationOptionsList, m) } - - m := map[string]interface{}{ - // AWS Provider 3.0 -- plan-time validation prevents "subject_alternative_names" - // argument to accept strings with trailing period; thus, trim of trailing period - // no longer required here - "domain_name": san, - } - - domainValidationOptionsList = append(domainValidationOptionsList, m) } - } - if err := diff.SetNew("domain_validation_options", schema.NewSet(acmDomainValidationOptionsHash, domainValidationOptionsList)); err != nil { - return fmt.Errorf("error setting new domain_validation_options diff: %w", err) + if err := diff.SetNew("domain_validation_options", schema.NewSet(acmDomainValidationOptionsHash, domainValidationOptionsList)); err != nil { + return fmt.Errorf("error setting new domain_validation_options diff: %w", err) + } } - } - return nil - }, + return nil + }, + SetTagsDiff, + ), } } @@ -225,6 +230,8 @@ func resourceAwsAcmCertificateCreate(d *schema.ResourceData, meta interface{}) e func resourceAwsAcmCertificateCreateImported(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).acmconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &acm.ImportCertificateInput{ Certificate: []byte(d.Get("certificate_body").(string)), @@ -235,8 +242,8 @@ func resourceAwsAcmCertificateCreateImported(d *schema.ResourceData, meta interf input.CertificateChain = []byte(v.(string)) } - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - input.Tags = keyvaluetags.New(v).IgnoreAws().AcmTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AcmTags() } output, err := conn.ImportCertificate(input) @@ -252,14 +259,17 @@ func resourceAwsAcmCertificateCreateImported(d *schema.ResourceData, meta interf func resourceAwsAcmCertificateCreateRequested(d *schema.ResourceData, meta interface{}) error { acmconn := meta.(*AWSClient).acmconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + params := &acm.RequestCertificateInput{ DomainName: aws.String(d.Get("domain_name").(string)), IdempotencyToken: aws.String(resource.PrefixedUniqueId("tf")), // 32 character limit Options: expandAcmCertificateOptions(d.Get("options").([]interface{})), } - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - params.Tags = keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AcmTags() + if len(tags) > 0 { + params.Tags = tags.IgnoreAws().AcmTags() } if caARN, ok := d.GetOk("certificate_authority_arn"); ok { @@ -292,6 +302,7 @@ func resourceAwsAcmCertificateCreateRequested(d *schema.ResourceData, meta inter func resourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) error { acmconn := meta.(*AWSClient).acmconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig params := &acm.DescribeCertificateInput{ @@ -301,12 +312,24 @@ func resourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) err return resource.Retry(AcmCertificateDnsValidationAssignmentTimeout, func() *resource.RetryError { resp, err := acmconn.DescribeCertificate(params) + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { - if isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { - d.SetId("") - return nil - } - return resource.NonRetryableError(fmt.Errorf("Error describing certificate: %s", err)) + return resource.NonRetryableError(fmt.Errorf("error reading ACM Certificate (%s): %w", d.Id(), err)) + } + + if resp == nil || resp.Certificate == nil { + return resource.NonRetryableError(fmt.Errorf("error reading ACM Certificate (%s): empty response", d.Id())) + } + + if !d.IsNewResource() && aws.StringValue(resp.Certificate.Status) == acm.CertificateStatusValidationTimedOut { + log.Printf("[WARN] ACM Certificate (%s) validation timed out, removing from state", d.Id()) + d.SetId("") + return nil } d.Set("domain_name", resp.Certificate.DomainName) @@ -344,13 +367,21 @@ func resourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) err return resource.NonRetryableError(fmt.Errorf("error listing tags for ACM Certificate (%s): %s", d.Id(), err)) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return resource.NonRetryableError(fmt.Errorf("error setting tags: %s", err)) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return resource.NonRetryableError(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return resource.NonRetryableError(fmt.Errorf("error setting tags_all: %w", err)) } return nil }) } + func resourceAwsAcmCertificateValidationMethod(certificate *acm.CertificateDetail) string { if aws.StringValue(certificate.Type) == acm.CertificateTypeAmazonIssued { for _, domainValidation := range certificate.DomainValidationOptions { @@ -392,8 +423,8 @@ func resourceAwsAcmCertificateUpdate(d *schema.ResourceData, meta interface{}) e } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AcmUpdateTags(conn, d.Id(), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } diff --git a/aws/resource_aws_acm_certificate_test.go b/aws/resource_aws_acm_certificate_test.go index 399a5d83b905..ca3150687750 100644 --- a/aws/resource_aws_acm_certificate_test.go +++ b/aws/resource_aws_acm_certificate_test.go @@ -32,9 +32,9 @@ func testSweepAcmCertificates(region string) error { conn := client.(*AWSClient).acmconn var sweeperErrs *multierror.Error - err = conn.ListCertificatesPages(&acm.ListCertificatesInput{}, func(page *acm.ListCertificatesOutput, isLast bool) bool { + err = conn.ListCertificatesPages(&acm.ListCertificatesInput{}, func(page *acm.ListCertificatesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, certificate := range page.CertificateSummaryList { @@ -67,7 +67,7 @@ func testSweepAcmCertificates(region string) error { } } - return !isLast + return !lastPage }) if err != nil { @@ -122,6 +122,7 @@ func TestAccAWSAcmCertificate_emailValidation(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -154,6 +155,7 @@ func TestAccAWSAcmCertificate_dnsValidation(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -188,6 +190,7 @@ func TestAccAWSAcmCertificate_root(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -219,18 +222,21 @@ func TestAccAWSAcmCertificate_root(t *testing.T) { func TestAccAWSAcmCertificate_privateCert(t *testing.T) { certificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" resourceName := "aws_acm_certificate.cert" - rName := acctest.RandomWithPrefix("tf-acc-test") + + commonName := testAccRandomDomain() + certificateDomainName := commonName.RandomSubdomain().String() resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_privateCert(rName), + Config: testAccAcmCertificateConfig_privateCert(commonName.String(), certificateDomainName), Check: resource.ComposeTestCheckFunc( testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), - resource.TestCheckResourceAttr(resourceName, "domain_name", fmt.Sprintf("%s.terraformtesting.com", rName)), + resource.TestCheckResourceAttr(resourceName, "domain_name", certificateDomainName), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "0"), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusFailed), // FailureReason: PCA_INVALID_STATE (PCA State: PENDING_CERTIFICATE) resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), @@ -256,6 +262,7 @@ func TestAccAWSAcmCertificate_root_TrailingPeriod(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -274,6 +281,7 @@ func TestAccAWSAcmCertificate_rootAndWildcardSan(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -313,6 +321,7 @@ func TestAccAWSAcmCertificate_SubjectAlternativeNames_EmptyString(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -332,6 +341,7 @@ func TestAccAWSAcmCertificate_san_single(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -374,6 +384,7 @@ func TestAccAWSAcmCertificate_san_multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -420,6 +431,7 @@ func TestAccAWSAcmCertificate_san_TrailingPeriod(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -460,6 +472,7 @@ func TestAccAWSAcmCertificate_wildcard(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -495,6 +508,7 @@ func TestAccAWSAcmCertificate_wildcardAndRootSan(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -534,6 +548,7 @@ func TestAccAWSAcmCertificate_disableCTLogging(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -571,6 +586,7 @@ func TestAccAWSAcmCertificate_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -626,8 +642,11 @@ func TestAccAWSAcmCertificate_imported_DomainName(t *testing.T) { newCaCertificate := tlsRsaX509SelfSignedCaCertificatePem(newCaKey) newCertificate := tlsRsaX509LocallySignedCertificatePem(newCaKey, newCaCertificate, key, commonName) + withoutChainDomain := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -647,11 +666,11 @@ func TestAccAWSAcmCertificate_imported_DomainName(t *testing.T) { ), }, { - Config: testAccAcmCertificateConfigPrivateKeyWithoutChain("example2.com"), + Config: testAccAcmCertificateConfigPrivateKeyWithoutChain(withoutChainDomain), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), - resource.TestCheckResourceAttr(resourceName, "domain_name", "example2.com"), + resource.TestCheckResourceAttr(resourceName, "domain_name", withoutChainDomain), ), }, { @@ -671,6 +690,7 @@ func TestAccAWSAcmCertificate_imported_IpAddress(t *testing.T) { // Reference: h resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -699,6 +719,7 @@ func TestAccAWSAcmCertificate_PrivateKey_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -734,7 +755,7 @@ resource "aws_acm_certificate" "cert" { } -func testAccAcmCertificateConfig_privateCert(rName string) string { +func testAccAcmCertificateConfig_privateCert(commonName, certificateDomainName string) string { return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -745,16 +766,16 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } } resource "aws_acm_certificate" "cert" { - domain_name = "%s.terraformtesting.com" + domain_name = %[2]q certificate_authority_arn = aws_acmpca_certificate_authority.test.arn } -`, rName) +`, commonName, certificateDomainName) } func testAccAcmCertificateConfig_subjectAlternativeNames(domainName, subjectAlternativeNames, validationMethod string) string { diff --git a/aws/resource_aws_acm_certificate_validation.go b/aws/resource_aws_acm_certificate_validation.go index 89ffc3459e6a..e35a802e3afc 100644 --- a/aws/resource_aws_acm_certificate_validation.go +++ b/aws/resource_aws_acm_certificate_validation.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -177,19 +178,32 @@ func resourceAwsAcmCertificateValidationRead(d *schema.ResourceData, meta interf resp, err := acmconn.DescribeCertificate(params) - if err != nil && isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM Certificate (%s) not found, removing from state", d.Id()) d.SetId("") return nil - } else if err != nil { - return fmt.Errorf("Error describing certificate: %w", err) } - if aws.StringValue(resp.Certificate.Status) != acm.CertificateStatusIssued { - log.Printf("[INFO] Certificate status not issued, was %s, tainting validation", aws.StringValue(resp.Certificate.Status)) + if err != nil { + return fmt.Errorf("error describing ACM Certificate (%s): %w", d.Id(), err) + } + + if resp == nil || resp.Certificate == nil { + return fmt.Errorf("error describing ACM Certificate (%s): empty response", d.Id()) + } + + if status := aws.StringValue(resp.Certificate.Status); status != acm.CertificateStatusIssued { + if d.IsNewResource() { + return fmt.Errorf("ACM Certificate (%s) status not issued: %s", d.Id(), status) + } + + log.Printf("[WARN] ACM Certificate (%s) status not issued (%s), removing from state", d.Id(), status) d.SetId("") - } else { - d.SetId(aws.TimeValue(resp.Certificate.IssuedAt).String()) + return nil } + + d.SetId(aws.TimeValue(resp.Certificate.IssuedAt).String()) + return nil } diff --git a/aws/resource_aws_acm_certificate_validation_test.go b/aws/resource_aws_acm_certificate_validation_test.go index cb9d001435ae..e541f762a0e5 100644 --- a/aws/resource_aws_acm_certificate_validation_test.go +++ b/aws/resource_aws_acm_certificate_validation_test.go @@ -6,6 +6,7 @@ import ( "strconv" "testing" + "github.com/aws/aws-sdk-go/service/acm" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) @@ -17,6 +18,7 @@ func TestAccAWSAcmCertificateValidation_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -37,6 +39,7 @@ func TestAccAWSAcmCertificateValidation_timeout(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -56,6 +59,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdns(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -81,6 +85,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsEmail(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -99,6 +104,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRoot(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -120,6 +126,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRootAndWildcard(t * resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -142,6 +149,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsSan(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -163,6 +171,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcard(t *testing resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ @@ -184,6 +193,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcardAndRoot(t * resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acm.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_acmpca_certificate.go b/aws/resource_aws_acmpca_certificate.go new file mode 100644 index 000000000000..457a01b3905f --- /dev/null +++ b/aws/resource_aws_acmpca_certificate.go @@ -0,0 +1,287 @@ +package aws + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "regexp" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/waiter" +) + +func resourceAwsAcmpcaCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmpcaCertificateCreate, + Read: resourceAwsAcmpcaCertificateRead, + Delete: resourceAwsAcmpcaCertificateRevoke, + + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + re := regexp.MustCompile(`arn:.+:certificate-authority/[^/]+`) + authorityArn := re.FindString(d.Id()) + if authorityArn == "" { + return nil, fmt.Errorf("Unexpected format for ID (%q), expected ACM PCA Certificate ARN", d.Id()) + } + + d.Set("certificate_authority_arn", authorityArn) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "certificate_signing_request": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "signing_algorithm": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(acmpca.SigningAlgorithm_Values(), false), + }, + "validity": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(acmpca.ValidityPeriodType_Values(), false), + }, + "value": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, + }, + }, + }, + }, + "template_arn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAcmPcaTemplateArn, + }, + }, + } +} + +func resourceAwsAcmpcaCertificateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + certificateAuthorityArn := d.Get("certificate_authority_arn").(string) + input := &acmpca.IssueCertificateInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + Csr: []byte(d.Get("certificate_signing_request").(string)), + IdempotencyToken: aws.String(resource.UniqueId()), + SigningAlgorithm: aws.String(d.Get("signing_algorithm").(string)), + } + validity, err := expandAcmpcaValidity(d.Get("validity").([]interface{})) + if err != nil { + return fmt.Errorf("error issuing ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + input.Validity = validity + + if v, ok := d.Get("template_arn").(string); ok && v != "" { + input.TemplateArn = aws.String(v) + } + + var output *acmpca.IssueCertificateOutput + err = resource.Retry(waiter.CertificateAuthorityActiveTimeout, func() *resource.RetryError { + var err error + output, err = conn.IssueCertificate(input) + if tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "The certificate authority is not in a valid state for issuing certificates") { + return resource.RetryableError(err) + } + if err != nil { + return resource.NonRetryableError(err) + } + return nil + }) + if isResourceTimeoutError(err) { + output, err = conn.IssueCertificate(input) + } + + if err != nil { + return fmt.Errorf("error issuing ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + + d.SetId(aws.StringValue(output.CertificateArn)) + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: output.CertificateArn, + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + err = conn.WaitUntilCertificateIssued(getCertificateInput) + if err != nil { + return fmt.Errorf("error waiting for ACM PCA Certificate Authority (%s) to issue Certificate (%s), error: %w", certificateAuthorityArn, d.Id(), err) + } + + return resourceAwsAcmpcaCertificateRead(d, meta) +} + +func resourceAwsAcmpcaCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(d.Id()), + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + log.Printf("[DEBUG] Reading ACM PCA Certificate: %s", getCertificateInput) + + certificateOutput, err := conn.GetCertificate(getCertificateInput) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM PCA Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading ACM PCA Certificate (%s): %w", d.Id(), err) + } + + if certificateOutput == nil { + return fmt.Errorf("error reading ACM PCA Certificate (%s): empty response", d.Id()) + } + + d.Set("arn", d.Id()) + d.Set("certificate", certificateOutput.Certificate) + d.Set("certificate_chain", certificateOutput.CertificateChain) + + return nil +} + +func resourceAwsAcmpcaCertificateRevoke(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + block, _ := pem.Decode([]byte(d.Get("certificate").(string))) + if block == nil { + log.Printf("[WARN] Failed to parse ACM PCA Certificate (%s)", d.Id()) + return nil + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("Failed to parse ACM PCA Certificate (%s): %w", d.Id(), err) + } + + input := &acmpca.RevokeCertificateInput{ + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + CertificateSerial: aws.String(fmt.Sprintf("%x", cert.SerialNumber)), + RevocationReason: aws.String(acmpca.RevocationReasonUnspecified), + } + _, err = conn.RevokeCertificate(input) + + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) || + tfawserr.ErrCodeEquals(err, acmpca.ErrCodeRequestAlreadyProcessedException) || + tfawserr.ErrCodeEquals(err, acmpca.ErrCodeRequestInProgressException) || + tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidRequestException, "Self-signed certificate can not be revoked") { + return nil + } + if err != nil { + return fmt.Errorf("error revoking ACM PCA Certificate (%s): %w", d.Id(), err) + } + + return nil +} + +func validateAcmPcaTemplateArn(v interface{}, k string) (ws []string, errors []error) { + wsARN, errorsARN := validateArn(v, k) + ws = append(ws, wsARN...) + errors = append(errors, errorsARN...) + + if len(errors) == 0 { + value := v.(string) + parsedARN, _ := arn.Parse(value) + + if parsedARN.Service != acmpca.ServiceName { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: service must be \""+acmpca.ServiceName+"\", was %q)", k, value, parsedARN.Service)) + } + + if parsedARN.Region != "" { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: region must be empty, was %q)", k, value, parsedARN.Region)) + } + + if parsedARN.AccountID != "" { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: account ID must be empty, was %q)", k, value, parsedARN.AccountID)) + } + + if !strings.HasPrefix(parsedARN.Resource, "template/") { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: expected resource to start with \"template/\", was %q)", k, value, parsedARN.Resource)) + } + } + + return ws, errors +} + +func expandAcmpcaValidity(l []interface{}) (*acmpca.Validity, error) { + if len(l) == 0 { + return nil, nil + } + + m := l[0].(map[string]interface{}) + + valueType := m["type"].(string) + result := &acmpca.Validity{ + Type: aws.String(valueType), + } + + i, err := expandAcmpcaValidityValue(valueType, m["value"].(string)) + if err != nil { + return nil, fmt.Errorf("error parsing value %q: %w", m["value"].(string), err) + } + result.Value = aws.Int64(i) + + return result, nil +} + +func expandAcmpcaValidityValue(valueType, v string) (int64, error) { + if valueType == acmpca.ValidityPeriodTypeEndDate { + date, err := time.Parse(time.RFC3339, v) + if err != nil { + return 0, err + } + v = date.UTC().Format("20060102150405") // YYYYMMDDHHMMSS + } + + return strconv.ParseInt(v, 10, 64) +} diff --git a/aws/resource_aws_acmpca_certificate_authority.go b/aws/resource_aws_acmpca_certificate_authority.go index a2ea02cfae9b..c667f22507e2 100644 --- a/aws/resource_aws_acmpca_certificate_authority.go +++ b/aws/resource_aws_acmpca_certificate_authority.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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" @@ -235,6 +236,12 @@ func resourceAwsAcmpcaCertificateAuthority() *schema.Resource { Optional: true, ValidateFunc: validation.StringLenBetween(0, 255), }, + "s3_object_acl": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(acmpca.S3ObjectAcl_Values(), false), + }, }, }, }, @@ -255,7 +262,8 @@ func resourceAwsAcmpcaCertificateAuthority() *schema.Resource { Default: 30, ValidateFunc: validation.IntBetween(7, 30), }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "type": { Type: schema.TypeString, Optional: true, @@ -267,12 +275,15 @@ func resourceAwsAcmpcaCertificateAuthority() *schema.Resource { }, false), }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).acmpcaconn - tags := keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AcmpcaTags() + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &acmpca.CreateCertificateAuthorityInput{ CertificateAuthorityConfiguration: expandAcmpcaCertificateAuthorityConfiguration(d.Get("certificate_authority_configuration").([]interface{})), @@ -282,10 +293,10 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in } if len(tags) > 0 { - input.Tags = tags + input.Tags = tags.IgnoreAws().AcmpcaTags() } - log.Printf("[DEBUG] Creating ACMPCA Certificate Authority: %s", input) + log.Printf("[DEBUG] Creating ACM PCA Certificate Authority: %s", input) var output *acmpca.CreateCertificateAuthorityOutput err := resource.Retry(1*time.Minute, func() *resource.RetryError { var err error @@ -303,7 +314,7 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in output, err = conn.CreateCertificateAuthority(input) } if err != nil { - return fmt.Errorf("error creating ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error creating ACM PCA Certificate Authority: %s", err) } d.SetId(aws.StringValue(output.CertificateAuthorityArn)) @@ -311,7 +322,7 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in _, err = waiter.CertificateAuthorityCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) if err != nil { - return fmt.Errorf("error waiting for ACMPCA Certificate Authority %q to be active or pending certificate: %s", d.Id(), err) + return fmt.Errorf("error waiting for ACM PCA Certificate Authority %q to be active or pending certificate: %s", d.Id(), err) } return resourceAwsAcmpcaCertificateAuthorityRead(d, meta) @@ -319,22 +330,27 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).acmpcaconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig certificateAuthority, err := finder.CertificateAuthorityByARN(conn, d.Id()) - if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority (%s): %w", d.Id(), err) } if certificateAuthority == nil || aws.StringValue(certificateAuthority.Status) == acmpca.CertificateAuthorityStatusDeleted { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + if d.IsNewResource() { + return fmt.Errorf("error reading ACM PCA Certificate Authority (%s): not found or deleted", d.Id()) + } + + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -361,20 +377,20 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte CertificateAuthorityArn: aws.String(d.Id()), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) - if err != nil { - if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) - d.SetId("") - return nil - } - // Returned when in PENDING_CERTIFICATE status - // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. - if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %s", err) - } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + // Returned when in PENDING_CERTIFICATE status + // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. + if err != nil && !tfawserr.ErrCodeEquals(err, acmpca.ErrCodeInvalidStateException) { + return fmt.Errorf("error reading ACM PCA Certificate Authority (%s) Certificate: %w", d.Id(), err) } d.Set("certificate", "") @@ -388,18 +404,20 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte CertificateAuthorityArn: aws.String(d.Id()), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) - if err != nil { - if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) - d.SetId("") - return nil - } - if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %s", err) - } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + // Returned when in PENDING_CERTIFICATE status + // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. + if err != nil && !tfawserr.ErrCodeEquals(err, acmpca.ErrCodeInvalidStateException) { + return fmt.Errorf("error reading ACM PCA Certificate Authority (%s) Certificate Signing Request: %w", d.Id(), err) } d.Set("certificate_signing_request", "") @@ -410,11 +428,18 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte tags, err := keyvaluetags.AcmpcaListTags(conn, d.Id()) if err != nil { - return fmt.Errorf("error listing tags for ACMPCA Certificate Authority (%s): %s", d.Id(), err) + return fmt.Errorf("error listing tags for ACM PCA Certificate Authority (%s): %s", d.Id(), err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -442,18 +467,18 @@ func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta in } if updateCertificateAuthority { - log.Printf("[DEBUG] Updating ACMPCA Certificate Authority: %s", input) + log.Printf("[DEBUG] Updating ACM PCA Certificate Authority: %s", input) _, err := conn.UpdateCertificateAuthority(input) if err != nil { - return fmt.Errorf("error updating ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error updating ACM PCA Certificate Authority: %s", err) } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AcmpcaUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating ACMPCA Certificate Authority (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating ACM PCA Certificate Authority (%s) tags: %s", d.Id(), err) } } @@ -463,21 +488,34 @@ func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta in func resourceAwsAcmpcaCertificateAuthorityDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).acmpcaconn - input := &acmpca.DeleteCertificateAuthorityInput{ + // The Certificate Authority must be in PENDING_CERTIFICATE or DISABLED state before deleting. + updateInput := &acmpca.UpdateCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + Status: aws.String(acmpca.CertificateAuthorityStatusDisabled), + } + _, err := conn.UpdateCertificateAuthority(updateInput) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } + if err != nil && !tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "The certificate authority must be in the ACTIVE or DISABLED state to be updated") { + return fmt.Errorf("error setting ACM PCA Certificate Authority (%s) to DISABLED status before deleting: %w", d.Id(), err) + } + + deleteInput := &acmpca.DeleteCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(d.Id()), } if v, exists := d.GetOk("permanent_deletion_time_in_days"); exists { - input.PermanentDeletionTimeInDays = aws.Int64(int64(v.(int))) + deleteInput.PermanentDeletionTimeInDays = aws.Int64(int64(v.(int))) } - log.Printf("[DEBUG] Deleting ACMPCA Certificate Authority: %s", input) - _, err := conn.DeleteCertificateAuthority(input) + log.Printf("[DEBUG] Deleting ACM PCA Certificate Authority: %s", deleteInput) + _, err = conn.DeleteCertificateAuthority(deleteInput) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } if err != nil { - if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - return nil - } - return fmt.Errorf("error deleting ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error deleting ACM PCA Certificate Authority (%s): %w", d.Id(), err) } return nil @@ -570,6 +608,9 @@ func expandAcmpcaCrlConfiguration(l []interface{}) *acmpca.CrlConfiguration { if v, ok := m["s3_bucket_name"]; ok && v.(string) != "" { config.S3BucketName = aws.String(v.(string)) } + if v, ok := m["s3_object_acl"]; ok && v.(string) != "" { + config.S3ObjectAcl = aws.String(v.(string)) + } return config } @@ -636,6 +677,7 @@ func flattenAcmpcaCrlConfiguration(config *acmpca.CrlConfiguration) []interface{ "enabled": aws.BoolValue(config.Enabled), "expiration_in_days": int(aws.Int64Value(config.ExpirationInDays)), "s3_bucket_name": aws.StringValue(config.S3BucketName), + "s3_object_acl": aws.StringValue(config.S3ObjectAcl), } return []interface{}{m} diff --git a/aws/resource_aws_acmpca_certificate_authority_certificate.go b/aws/resource_aws_acmpca_certificate_authority_certificate.go new file mode 100644 index 000000000000..26bfaba4a8ee --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority_certificate.go @@ -0,0 +1,89 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAcmpcaCertificateAuthorityCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmpcaCertificateAuthorityCertificateCreate, + Read: resourceAwsAcmpcaCertificateAuthorityCertificateRead, + Delete: schema.Noop, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "certificate": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 32768), + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "certificate_chain": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 2097152), + }, + }, + } +} + +func resourceAwsAcmpcaCertificateAuthorityCertificateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + certificateAuthorityArn := d.Get("certificate_authority_arn").(string) + + input := &acmpca.ImportCertificateAuthorityCertificateInput{ + Certificate: []byte(d.Get("certificate").(string)), + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + if v, ok := d.Get("certificate_chain").(string); ok && v != "" { + input.CertificateChain = []byte(v) + } + + _, err := conn.ImportCertificateAuthorityCertificate(input) + if err != nil { + return fmt.Errorf("error associating ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + + d.SetId(certificateAuthorityArn) + + return resourceAwsAcmpcaCertificateAuthorityCertificateRead(d, meta) +} + +func resourceAwsAcmpcaCertificateAuthorityCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + output, err := finder.CertificateAuthorityCertificateByARN(conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ACM PCA Certificate Authority Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate (%s): %w", d.Id(), err) + } + + d.Set("certificate_authority_arn", d.Id()) + d.Set("certificate", output.Certificate) + d.Set("certificate_chain", output.CertificateChain) + + return nil +} diff --git a/aws/resource_aws_acmpca_certificate_authority_certificate_test.go b/aws/resource_aws_acmpca_certificate_authority_certificate_test.go new file mode 100644 index 000000000000..79acc1112e0f --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority_certificate_test.go @@ -0,0 +1,284 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + resourceName := "aws_acmpca_certificate_authority_certificate.test" + + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(commonName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + resourceName := "aws_acmpca_certificate_authority_certificate.test" + updatedResourceName := "aws_acmpca_certificate_authority_certificate.updated" + + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(commonName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(commonName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(updatedResourceName, &v), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate", "aws_acmpca_certificate.updated", "certificate"), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate_chain", "aws_acmpca_certificate.updated", "certificate_chain"), + ), + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + resourceName := "aws_acmpca_certificate_authority_certificate.test" + + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(commonName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName string, certificate *acmpca.GetCertificateAuthorityCertificateOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + + output, err := finder.CertificateAuthorityCertificateByARN(conn, rs.Primary.ID) + if err != nil { + return err + } + if tfresource.NotFound(err) { + return fmt.Errorf("ACM PCA Certificate (%s) does not exist", rs.Primary.ID) + } + + *certificate = *output + + return nil + } +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(commonName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +data "aws_partition" "current" {} +`, commonName) +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(commonName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "updated" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.updated.certificate + certificate_chain = aws_acmpca_certificate.updated.certificate_chain +} + +resource "aws_acmpca_certificate" "updated" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +data "aws_partition" "current" {} +`, commonName) +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(commonName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "SUBORDINATE" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "sub.%[1]s" + } + } +} + +resource "aws_acmpca_certificate_authority" "root" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +resource "aws_acmpca_certificate_authority_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + + certificate = aws_acmpca_certificate.root.certificate + certificate_chain = aws_acmpca_certificate.root.certificate_chain +} + +resource "aws_acmpca_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.root.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 2 + } +} + +data "aws_partition" "current" {} +`, commonName) +} diff --git a/aws/resource_aws_acmpca_certificate_authority_migrate.go b/aws/resource_aws_acmpca_certificate_authority_migrate.go index 1e039f8975bd..156ade901ffb 100644 --- a/aws/resource_aws_acmpca_certificate_authority_migrate.go +++ b/aws/resource_aws_acmpca_certificate_authority_migrate.go @@ -10,7 +10,7 @@ import ( func resourceAwsAcmpcaCertificateAuthorityMigrateState(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { switch v { case 0: - log.Println("[INFO] Found ACMPCA Certificate Authority state v0; migrating to v1") + log.Println("[INFO] Found ACM PCA Certificate Authority state v0; migrating to v1") return migrateAcmpcaCertificateAuthorityStateV0toV1(is) default: return is, fmt.Errorf("Unexpected schema version: %d", v) @@ -19,7 +19,7 @@ func resourceAwsAcmpcaCertificateAuthorityMigrateState(v int, is *terraform.Inst func migrateAcmpcaCertificateAuthorityStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { if is.Empty() || is.Attributes == nil { - log.Println("[DEBUG] Empty ACMPCA Certificate Authority state; nothing to migrate.") + log.Println("[DEBUG] Empty ACM PCA Certificate Authority state; nothing to migrate.") return is, nil } diff --git a/aws/resource_aws_acmpca_certificate_authority_test.go b/aws/resource_aws_acmpca_certificate_authority_test.go index 915d38c260be..8af63b0387ee 100644 --- a/aws/resource_aws_acmpca_certificate_authority_test.go +++ b/aws/resource_aws_acmpca_certificate_authority_test.go @@ -31,13 +31,13 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { certificateAuthorities, err := listAcmpcaCertificateAuthorities(conn) if err != nil { if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping ACMPCA Certificate Authorities sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping ACM PCA Certificate Authorities sweep for %s: %s", region, err) return nil } - return fmt.Errorf("Error retrieving ACMPCA Certificate Authorities: %w", err) + return fmt.Errorf("Error retrieving ACM PCA Certificate Authorities: %w", err) } if len(certificateAuthorities) == 0 { - log.Print("[DEBUG] No ACMPCA Certificate Authorities to sweep") + log.Print("[DEBUG] No ACM PCA Certificate Authorities to sweep") return nil } @@ -47,7 +47,7 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { arn := aws.StringValue(certificateAuthority.Arn) if aws.StringValue(certificateAuthority.Status) == acmpca.CertificateAuthorityStatusActive { - log.Printf("[INFO] Disabling ACMPCA Certificate Authority: %s", arn) + log.Printf("[INFO] Disabling ACM PCA Certificate Authority: %s", arn) _, err := conn.UpdateCertificateAuthority(&acmpca.UpdateCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(arn), Status: aws.String(acmpca.CertificateAuthorityStatusDisabled), @@ -56,14 +56,14 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { continue } if err != nil { - sweeperErr := fmt.Errorf("error disabling ACMPCA Certificate Authority (%s): %w", arn, err) + sweeperErr := fmt.Errorf("error disabling ACM PCA Certificate Authority (%s): %w", arn, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue } } - log.Printf("[INFO] Deleting ACMPCA Certificate Authority: %s", arn) + log.Printf("[INFO] Deleting ACM PCA Certificate Authority: %s", arn) _, err := conn.DeleteCertificateAuthority(&acmpca.DeleteCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(arn), PermanentDeletionTimeInDays: aws.Int64(int64(7)), @@ -72,7 +72,7 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { continue } if err != nil { - sweeperErr := fmt.Errorf("error deleting ACMPCA Certificate Authority (%s): %w", arn, err) + sweeperErr := fmt.Errorf("error deleting ACM PCA Certificate Authority (%s): %w", arn, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue @@ -86,13 +86,16 @@ func TestAccAwsAcmpcaCertificateAuthority_basic(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+`)), @@ -100,7 +103,7 @@ func TestAccAwsAcmpcaCertificateAuthority_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.key_algorithm", "RSA_4096"), resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.signing_algorithm", "SHA512WITHRSA"), resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.subject.#", "1"), - resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.subject.0.common_name", "terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.subject.0.common_name", commonName), resource.TestCheckResourceAttr(resourceName, "certificate", ""), resource.TestCheckResourceAttr(resourceName, "certificate_chain", ""), resource.TestCheckResourceAttrSet(resourceName, "certificate_signing_request"), @@ -130,13 +133,16 @@ func TestAccAwsAcmpcaCertificateAuthority_disappears(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), testAccCheckResourceDisappears(testAccProvider, resourceAwsAcmpcaCertificateAuthority(), resourceName), @@ -149,16 +155,18 @@ func TestAccAwsAcmpcaCertificateAuthority_disappears(t *testing.T) { func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority - rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(rName, acmpca.CertificateAuthorityTypeRoot, true), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(commonName, acmpca.CertificateAuthorityTypeRoot, true), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "type", acmpca.CertificateAuthorityTypeRoot), @@ -168,7 +176,7 @@ func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { ), }, { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(rName, acmpca.CertificateAuthorityTypeRoot, true), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(commonName, acmpca.CertificateAuthorityTypeRoot, true), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "type", acmpca.CertificateAuthorityTypeRoot), @@ -177,7 +185,7 @@ func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { ), }, { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(rName, acmpca.CertificateAuthorityTypeRoot, false), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(commonName, acmpca.CertificateAuthorityTypeRoot, false), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "enabled", "false"), @@ -196,24 +204,57 @@ func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { }) } +func TestAccAwsAcmpcaCertificateAuthority_DeleteFromActiveState(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + resourceName := "aws_acmpca_certificate_authority.test" + + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_WithRootCertificate(commonName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "type", acmpca.CertificateAuthorityTypeRoot), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + // Since the status of the CA is changed by importing the certificate in + // aws_acmpca_certificate_authority_certificate, the value of `status` is no longer accurate + // resource.TestCheckResourceAttr(resourceName, "status", acmpca.CertificateAuthorityStatusActive), + ), + }, + }, + }) +} + func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_CustomCname(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_acmpca_certificate_authority.test" + domain := testAccRandomDomain() + commonName := domain.String() + customCName := domain.Subdomain("crl").String() + customCName2 := domain.Subdomain("crl2").String() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ // Test creating revocation configuration on resource creation { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl.terraformtesting.com"), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, commonName, customCName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), - resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", customCName), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), @@ -230,12 +271,12 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test updating revocation configuration { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl2.terraformtesting.com"), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, commonName, customCName2), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), - resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl2.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", customCName2), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), @@ -243,7 +284,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test removing custom cname on resource update { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, commonName, true), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -256,12 +297,12 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test adding custom cname on resource update { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl.terraformtesting.com"), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, commonName, customCName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), - resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", customCName), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), @@ -269,7 +310,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test removing revocation configuration on resource update { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -286,14 +327,17 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ // Test creating revocation configuration on resource creation { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, commonName, true), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -315,7 +359,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test disabling revocation configuration { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, false), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, commonName, false), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -325,7 +369,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test enabling revocation configuration { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, commonName, true), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -338,7 +382,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test removing revocation configuration on resource update { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -355,14 +399,17 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ // Test creating revocation configuration on resource creation { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, 1), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, commonName, 1), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -371,6 +418,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_object_acl", "PUBLIC_READ"), ), }, // Test importing revocation configuration @@ -384,7 +432,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test updating revocation configuration { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, 2), + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, commonName, 2), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -396,7 +444,7 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }, // Test removing revocation configuration on resource update { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), @@ -408,17 +456,72 @@ func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfigurati }) } +func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_S3ObjectAcl(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority.test" + + commonName := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + // Test creating revocation configuration on resource creation + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_s3ObjectAcl(rName, commonName, "BUCKET_OWNER_FULL_CONTROL"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_object_acl", "BUCKET_OWNER_FULL_CONTROL"), + ), + }, + // Test importing revocation configuration + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "permanent_deletion_time_in_days", + }, + }, + // Test updating revocation configuration + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_s3ObjectAcl(rName, commonName, "PUBLIC_READ"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_object_acl", "PUBLIC_READ"), + ), + }, + }, + }) +} + func TestAccAwsAcmpcaCertificateAuthority_Tags(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority resourceName := "aws_acmpca_certificate_authority.test" + commonName := testAccRandomDomainName() + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, Steps: []resource.TestStep{ { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -426,7 +529,7 @@ func TestAccAwsAcmpcaCertificateAuthority_Tags(t *testing.T) { ), }, { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -434,7 +537,7 @@ func TestAccAwsAcmpcaCertificateAuthority_Tags(t *testing.T) { ), }, { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), @@ -443,7 +546,7 @@ func TestAccAwsAcmpcaCertificateAuthority_Tags(t *testing.T) { ), }, { - Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single, + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single(commonName), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -484,12 +587,11 @@ func testAccCheckAwsAcmpcaCertificateAuthorityDestroy(s *terraform.State) error } if output != nil && output.CertificateAuthority != nil && aws.StringValue(output.CertificateAuthority.Arn) == rs.Primary.ID && aws.StringValue(output.CertificateAuthority.Status) != acmpca.CertificateAuthorityStatusDeleted { - return fmt.Errorf("ACMPCA Certificate Authority %q still exists in non-DELETED state: %s", rs.Primary.ID, aws.StringValue(output.CertificateAuthority.Status)) + return fmt.Errorf("ACM PCA Certificate Authority %q still exists in non-DELETED state: %s", rs.Primary.ID, aws.StringValue(output.CertificateAuthority.Status)) } } return nil - } func testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName string, certificateAuthority *acmpca.CertificateAuthority) resource.TestCheckFunc { @@ -511,7 +613,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName string, certif } if output == nil || output.CertificateAuthority == nil { - return fmt.Errorf("ACMPCA Certificate Authority %q does not exist", rs.Primary.ID) + return fmt.Errorf("ACM PCA Certificate Authority %q does not exist", rs.Primary.ID) } *certificateAuthority = *output.CertificateAuthority @@ -530,7 +632,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateAuthorityArn: aws.String(arn), }) if err != nil { - return fmt.Errorf("error getting ACMPCA Certificate Authority (%s) CSR: %s", arn, err) + return fmt.Errorf("error getting ACM PCA Certificate Authority (%s) CSR: %s", arn, err) } issueCertResp, err := conn.IssueCertificate(&acmpca.IssueCertificateInput{ @@ -545,7 +647,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a }, }) if err != nil { - return fmt.Errorf("error issuing ACMPCA Certificate Authority (%s) Root CA certificate from CSR: %s", arn, err) + return fmt.Errorf("error issuing ACM PCA Certificate Authority (%s) Root CA certificate from CSR: %s", arn, err) } // Wait for certificate status to become ISSUED. @@ -554,7 +656,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateArn: issueCertResp.CertificateArn, }) if err != nil { - return fmt.Errorf("error waiting for ACMPCA Certificate Authority (%s) Root CA certificate to become ISSUED: %s", arn, err) + return fmt.Errorf("error waiting for ACM PCA Certificate Authority (%s) Root CA certificate to become ISSUED: %s", arn, err) } getCertResp, err := conn.GetCertificate(&acmpca.GetCertificateInput{ @@ -562,7 +664,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateArn: issueCertResp.CertificateArn, }) if err != nil { - return fmt.Errorf("error getting ACMPCA Certificate Authority (%s) issued Root CA certificate: %s", arn, err) + return fmt.Errorf("error getting ACM PCA Certificate Authority (%s) issued Root CA certificate: %s", arn, err) } _, err = conn.ImportCertificateAuthorityCertificate(&acmpca.ImportCertificateAuthorityCertificateInput{ @@ -570,7 +672,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a Certificate: []byte(aws.StringValue(getCertResp.Certificate)), }) if err != nil { - return fmt.Errorf("error importing ACMPCA Certificate Authority (%s) Root CA certificate: %s", arn, err) + return fmt.Errorf("error importing ACM PCA Certificate Authority (%s) Root CA certificate: %s", arn, err) } return err @@ -609,7 +711,7 @@ func listAcmpcaCertificateAuthorities(conn *acmpca.ACMPCA) ([]*acmpca.Certificat return certificateAuthorities, nil } -func testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(rName, certificateAuthorityType string, enabled bool) string { +func testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(commonName, certificateAuthorityType string, enabled bool) string { return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { enabled = %[1]t @@ -621,30 +723,72 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "%[3]s.com" + common_name = %[3]q } } } -`, enabled, certificateAuthorityType, rName) +`, enabled, certificateAuthorityType, commonName) } -const testAccAwsAcmpcaCertificateAuthorityConfig_Required = ` +func testAccAwsAcmpcaCertificateAuthorityConfig_WithRootCertificate(commonName string) string { + return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + certificate_authority_configuration { key_algorithm = "RSA_4096" signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } } -` -func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, customCname string) string { +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +data "aws_partition" "current" {} +`, commonName) +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_Required(commonName string) string { return fmt.Sprintf(` -%s +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} +`, commonName) +} +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, commonName, customCname string) string { + return composeConfig( + testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), + fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -653,13 +797,13 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } revocation_configuration { crl_configuration { - custom_cname = "%s" + custom_cname = %[2]q enabled = true expiration_in_days = 1 s3_bucket_name = aws_s3_bucket.test.id @@ -668,13 +812,13 @@ resource "aws_acmpca_certificate_authority" "test" { depends_on = [aws_s3_bucket_policy.test] } -`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), customCname) +`, commonName, customCname)) } -func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName string, enabled bool) string { - return fmt.Sprintf(` -%s - +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, commonName string, enabled bool) string { + return composeConfig( + testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), + fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -683,25 +827,52 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } revocation_configuration { crl_configuration { - enabled = %t + enabled = %[2]t expiration_in_days = 1 s3_bucket_name = aws_s3_bucket.test.id } } } -`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), enabled) +`, commonName, enabled)) } -func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName string, expirationInDays int) string { - return fmt.Sprintf(` -%s +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, commonName string, expirationInDays int) string { + return composeConfig( + testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), + fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } + revocation_configuration { + crl_configuration { + enabled = true + expiration_in_days = %[2]d + s3_bucket_name = aws_s3_bucket.test.id + } + } +} +`, commonName, expirationInDays)) +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_s3ObjectAcl(rName, commonName, s3ObjectAcl string) string { + return composeConfig( + testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), + fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -710,25 +881,28 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } revocation_configuration { crl_configuration { enabled = true - expiration_in_days = %d + expiration_in_days = 1 s3_bucket_name = aws_s3_bucket.test.id + s3_object_acl = %[2]q } } + + depends_on = [aws_s3_bucket_policy.test] } -`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), expirationInDays) +`, commonName, s3ObjectAcl)) } func testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "test" { - bucket = "%s" + bucket = %[1]q force_destroy = true } @@ -760,7 +934,8 @@ resource "aws_s3_bucket_policy" "test" { `, rName) } -const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single = ` +func testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single(commonName string) string { + return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -769,7 +944,7 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } @@ -777,9 +952,11 @@ resource "aws_acmpca_certificate_authority" "test" { tag1 = "tag1value" } } -` +`, commonName) +} -const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated = ` +func testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated(commonName string) string { + return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -788,7 +965,7 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } @@ -796,9 +973,11 @@ resource "aws_acmpca_certificate_authority" "test" { tag1 = "tag1value-updated" } } -` +`, commonName) +} -const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple = ` +func testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple(commonName string) string { + return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -807,7 +986,7 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "terraformtesting.com" + common_name = %[1]q } } @@ -816,4 +995,5 @@ resource "aws_acmpca_certificate_authority" "test" { tag2 = "tag2value" } } -` +`, commonName) +} diff --git a/aws/resource_aws_acmpca_certificate_test.go b/aws/resource_aws_acmpca_certificate_test.go new file mode 100644 index 000000000000..0ce74355e376 --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_test.go @@ -0,0 +1,517 @@ +package aws + +import ( + "fmt" + "regexp" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAwsAcmpcaCertificate_RootCertificate(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + certificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" + + domain := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_RootCertificate(domain), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttr(resourceName, "certificate_chain", ""), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", certificateAuthorityResourceName, "arn"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_signing_request"), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "YEARS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA512WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/RootCACertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_SubordinateCertificate(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + rootCertificateAuthorityResourceName := "aws_acmpca_certificate_authority.root" + subordinateCertificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" + + domain := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_SubordinateCertificate(domain), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", rootCertificateAuthorityResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_signing_request", subordinateCertificateAuthorityResourceName, "certificate_signing_request"), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "YEARS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA512WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/SubordinateCACertificate_PathLen0/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_EndEntityCertificate(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + + csrDomain := testAccRandomDomainName() + csr, _ := tlsRsaX509CertificateRequestPem(4096, csrDomain) + domain := testAccRandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_EndEntityCertificate(domain, tlsPemEscapeNewlines(csr)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "DAYS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_Validity_EndDate(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + + csrDomain := testAccRandomDomainName() + csr, _ := tlsRsaX509CertificateRequestPem(4096, csrDomain) + domain := testAccRandomDomainName() + later := time.Now().Add(time.Minute * 10).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_Validity_EndDate(domain, tlsPemEscapeNewlines(csr), later), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", later), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "END_DATE"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_Validity_Absolute(t *testing.T) { + resourceName := "aws_acmpca_certificate.test" + + csrDomain := testAccRandomDomainName() + csr, _ := tlsRsaX509CertificateRequestPem(4096, csrDomain) + domain := testAccRandomDomainName() + later := time.Now().Add(time.Minute * 10).Unix() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, acmpca.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_Validity_Absolute(domain, tlsPemEscapeNewlines(csr), later), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", strconv.FormatInt(later, 10)), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "ABSOLUTE"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func testAccCheckAwsAcmpcaCertificateDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_acmpca_certificate" { + continue + } + + input := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(rs.Primary.ID), + CertificateAuthorityArn: aws.String(rs.Primary.Attributes["certificate_authority_arn"]), + } + + output, err := conn.GetCertificate(input) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } + if tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "not in the correct state to have issued certificates") { + // This is returned when checking root certificates and the certificate has not been associated with the certificate authority + return nil + } + if err != nil { + return err + } + + if output != nil { + return fmt.Errorf("ACM PCA Certificate (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAcmpcaCertificateExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + input := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(rs.Primary.ID), + CertificateAuthorityArn: aws.String(rs.Primary.Attributes["certificate_authority_arn"]), + } + + output, err := conn.GetCertificate(input) + + if err != nil { + return err + } + + if output == nil || output.Certificate == nil { + return fmt.Errorf("ACM PCA Certificate %q does not exist", rs.Primary.ID) + } + + return nil + } +} + +func testAccAwsAcmpcaCertificateConfig_RootCertificate(domain string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +data "aws_partition" "current" {} +`, domain) +} + +func testAccAwsAcmpcaCertificateConfig_SubordinateCertificate(domain string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(domain), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "SUBORDINATE" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "sub.%[1]s" + } + } +} +`, domain)) +} + +func testAccAwsAcmpcaCertificateConfig_EndEntityCertificate(domain, csr string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(domain), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[1]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "DAYS" + value = 1 + } +} +`, csr)) +} + +func testAccAwsAcmpcaCertificateConfig_Validity_EndDate(domain, csr, expiry string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(domain), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[1]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "END_DATE" + value = %[2]q + } +} +`, csr, expiry)) +} + +func testAccAwsAcmpcaCertificateConfig_Validity_Absolute(domain, csr string, expiry int64) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(domain), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[1]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "ABSOLUTE" + value = %[2]d + } +} +`, csr, expiry)) +} + +func testAccAcmpcaCertificateBaseRootCAConfig(domain string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "root" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = %[1]q + } + } +} + +resource "aws_acmpca_certificate_authority_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + + certificate = aws_acmpca_certificate.root.certificate + certificate_chain = aws_acmpca_certificate.root.certificate_chain +} + +resource "aws_acmpca_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.root.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 2 + } +} + +data "aws_partition" "current" {} + `, domain) +} + +func TestValidateAcmPcaTemplateArn(t *testing.T) { + validNames := []string{ + "arn:aws:acm-pca:::template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + "arn:aws-us-gov:acm-pca:::template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws-us-gov:acm-pca:::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + } + for _, v := range validNames { + _, errors := validateAcmPcaTemplateArn(v, "template_arn") + if len(errors) != 0 { + t.Fatalf("%q should be a valid ACM PCA ARN: %q", v, errors) + } + } + + invalidNames := []string{ + "arn", + "arn:aws:s3:::my_corporate_bucket/exampleobject.png", // lintignore:AWSAT005 + "arn:aws:acm-pca:us-west-2::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT003,AWSAT005 + "arn:aws:acm-pca::123456789012:template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws:acm-pca:::not-a-template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + } + for _, v := range invalidNames { + _, errors := validateAcmPcaTemplateArn(v, "template_arn") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid ARN", v) + } + } +} + +func TestExpandAcmpcaValidityValue(t *testing.T) { + testCases := []struct { + Type string + Value string + Expected int64 + }{ + { + Type: acmpca.ValidityPeriodTypeEndDate, + Value: "2021-02-26T16:04:00Z", + Expected: 20210226160400, + }, + { + Type: acmpca.ValidityPeriodTypeEndDate, + Value: "2021-02-26T16:04:00-08:00", + Expected: 20210227000400, + }, + { + Type: acmpca.ValidityPeriodTypeAbsolute, + Value: "1614385420", + Expected: 1614385420, + }, + { + Type: acmpca.ValidityPeriodTypeYears, + Value: "2", + Expected: 2, + }, + } + + for _, testcase := range testCases { + i, _ := expandAcmpcaValidityValue(testcase.Type, testcase.Value) + if i != testcase.Expected { + t.Errorf("%s, %q: expected %d, got %d", testcase.Type, testcase.Value, testcase.Expected, i) + } + } + +} diff --git a/aws/resource_aws_alb_target_group_test.go b/aws/resource_aws_alb_target_group_test.go index c1061cb746e9..facd145683f1 100644 --- a/aws/resource_aws_alb_target_group_test.go +++ b/aws/resource_aws_alb_target_group_test.go @@ -46,41 +46,42 @@ func TestALBTargetGroupCloudwatchSuffixFromARN(t *testing.T) { func TestAccAWSALBTargetGroup_basic(t *testing.T) { var conf elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "slow_start", "0"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "target_type", "instance"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.enabled", "true"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.type", "lb_cookie"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.cookie_duration", "10000"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "60"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8081"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTP"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200-299"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.TestName", "TestAccAWSALBTargetGroup_basic"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "slow_start", "0"), + resource.TestCheckResourceAttr(resourceName, "target_type", "instance"), + resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "10000"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "60"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8081"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200-299"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.TestName", rName), ), }, }, @@ -89,18 +90,20 @@ func TestAccAWSALBTargetGroup_basic(t *testing.T) { func TestAccAWSALBTargetGroup_namePrefix(t *testing.T) { var conf elbv2.TargetGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_namePrefix, + Config: testAccAWSALBTargetGroupConfig_namePrefix(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestMatchResourceAttr("aws_alb_target_group.test", "name", regexp.MustCompile("^tf-")), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestMatchResourceAttr(resourceName, "name", regexp.MustCompile("^tf-")), ), }, }, @@ -109,17 +112,19 @@ func TestAccAWSALBTargetGroup_namePrefix(t *testing.T) { func TestAccAWSALBTargetGroup_generatedName(t *testing.T) { var conf elbv2.TargetGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_generatedName, + Config: testAccAWSALBTargetGroupConfig_generatedName(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), ), }, }, @@ -128,27 +133,28 @@ func TestAccAWSALBTargetGroup_generatedName(t *testing.T) { func TestAccAWSALBTargetGroup_changeNameForceNew(t *testing.T) { var before, after elbv2.TargetGroup - targetGroupNameBefore := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) - targetGroupNameAfter := fmt.Sprintf("test-target-group-%s", acctest.RandString(4)) + rName := acctest.RandomWithPrefix("tf-acc-test") + rNameAfter := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupNameBefore), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &before), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupNameBefore), + testAccCheckAWSALBTargetGroupExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "name", rName), ), }, { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupNameAfter), + Config: testAccAWSALBTargetGroupConfig_basic(rNameAfter), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &after), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupNameAfter), + testAccCheckAWSALBTargetGroupExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "name", rNameAfter), ), }, }, @@ -157,26 +163,27 @@ func TestAccAWSALBTargetGroup_changeNameForceNew(t *testing.T) { func TestAccAWSALBTargetGroup_changeProtocolForceNew(t *testing.T) { var before, after elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &before), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), + testAccCheckAWSALBTargetGroupExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), ), }, { - Config: testAccAWSALBTargetGroupConfig_updatedProtocol(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_updatedProtocol(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &after), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTP"), + testAccCheckAWSALBTargetGroupExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTP"), ), }, }, @@ -185,26 +192,27 @@ func TestAccAWSALBTargetGroup_changeProtocolForceNew(t *testing.T) { func TestAccAWSALBTargetGroup_changePortForceNew(t *testing.T) { var before, after elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &before), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), + testAccCheckAWSALBTargetGroupExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "port", "443"), ), }, { - Config: testAccAWSALBTargetGroupConfig_updatedPort(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_updatedPort(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &after), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "442"), + testAccCheckAWSALBTargetGroupExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "port", "442"), ), }, }, @@ -213,24 +221,25 @@ func TestAccAWSALBTargetGroup_changePortForceNew(t *testing.T) { func TestAccAWSALBTargetGroup_changeVpcForceNew(t *testing.T) { var before, after elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &before), + testAccCheckAWSALBTargetGroupExists(resourceName, &before), ), }, { - Config: testAccAWSALBTargetGroupConfig_updatedVpc(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_updatedVpc(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &after), + testAccCheckAWSALBTargetGroupExists(resourceName, &after), ), }, }, @@ -239,29 +248,30 @@ func TestAccAWSALBTargetGroup_changeVpcForceNew(t *testing.T) { func TestAccAWSALBTargetGroup_tags(t *testing.T) { var conf elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.TestName", "TestAccAWSALBTargetGroup_basic"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.TestName", rName), ), }, { - Config: testAccAWSALBTargetGroupConfig_updateTags(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_updateTags(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.%", "2"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.Environment", "Production"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "tags.Type", "ALB Target Group"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Environment", "Production"), + resource.TestCheckResourceAttr(resourceName, "tags.Type", "ALB Target Group"), ), }, }, @@ -270,60 +280,61 @@ func TestAccAWSALBTargetGroup_tags(t *testing.T) { func TestAccAWSALBTargetGroup_updateHealthCheck(t *testing.T) { var conf elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_basic(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.type", "lb_cookie"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.cookie_duration", "10000"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "60"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8081"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTP"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "3"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200-299"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "10000"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "60"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8081"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "3"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200-299"), ), }, { - Config: testAccAWSALBTargetGroupConfig_updateHealthCheck(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_updateHealthCheck(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.type", "lb_cookie"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.cookie_duration", "10000"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health2"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "30"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8082"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTPS"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "10000"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health2"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "30"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8082"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200"), ), }, }, @@ -332,83 +343,84 @@ func TestAccAWSALBTargetGroup_updateHealthCheck(t *testing.T) { func TestAccAWSALBTargetGroup_updateSticknessEnabled(t *testing.T) { var conf elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_stickiness(targetGroupName, false, false), + Config: testAccAWSALBTargetGroupConfig_stickiness(rName, false, false), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health2"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "30"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8082"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTPS"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health2"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "30"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8082"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200"), ), }, { - Config: testAccAWSALBTargetGroupConfig_stickiness(targetGroupName, true, true), + Config: testAccAWSALBTargetGroupConfig_stickiness(rName, true, true), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.enabled", "true"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.type", "lb_cookie"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.cookie_duration", "10000"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health2"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "30"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8082"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTPS"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "10000"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health2"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "30"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8082"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200"), ), }, { - Config: testAccAWSALBTargetGroupConfig_stickiness(targetGroupName, true, false), + Config: testAccAWSALBTargetGroupConfig_stickiness(rName, true, false), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "port", "443"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "protocol", "HTTPS"), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "vpc_id"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "deregistration_delay", "200"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.enabled", "false"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.type", "lb_cookie"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "stickiness.0.cookie_duration", "10000"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.#", "1"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.path", "/health2"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.interval", "30"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.port", "8082"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.protocol", "HTTPS"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.timeout", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.healthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.unhealthy_threshold", "4"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "health_check.0.matcher", "200"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"), + resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"), + resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "10000"), + resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.path", "/health2"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "30"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8082"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "4"), + resource.TestCheckResourceAttr(resourceName, "health_check.0.matcher", "200"), ), }, }, @@ -417,26 +429,27 @@ func TestAccAWSALBTargetGroup_updateSticknessEnabled(t *testing.T) { func TestAccAWSALBTargetGroup_setAndUpdateSlowStart(t *testing.T) { var before, after elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_updateSlowStart(targetGroupName, 30), + Config: testAccAWSALBTargetGroupConfig_updateSlowStart(rName, 30), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &before), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "slow_start", "30"), + testAccCheckAWSALBTargetGroupExists(resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "slow_start", "30"), ), }, { - Config: testAccAWSALBTargetGroupConfig_updateSlowStart(targetGroupName, 60), + Config: testAccAWSALBTargetGroupConfig_updateSlowStart(rName, 60), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &after), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "slow_start", "60"), + testAccCheckAWSALBTargetGroupExists(resourceName, &after), + resource.TestCheckResourceAttr(resourceName, "slow_start", "60"), ), }, }, @@ -445,39 +458,40 @@ func TestAccAWSALBTargetGroup_setAndUpdateSlowStart(t *testing.T) { func TestAccAWSALBTargetGroup_updateLoadBalancingAlgorithmType(t *testing.T) { var conf elbv2.TargetGroup - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_alb_target_group.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IDRefreshName: "aws_alb_target_group.test", - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(targetGroupName, false, ""), + Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(rName, false, ""), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "load_balancing_algorithm_type", "round_robin"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "load_balancing_algorithm_type", "round_robin"), ), }, { - Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(targetGroupName, true, "round_robin"), + Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(rName, true, "round_robin"), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "load_balancing_algorithm_type", "round_robin"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "load_balancing_algorithm_type", "round_robin"), ), }, { - Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(targetGroupName, true, "least_outstanding_requests"), + Config: testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(rName, true, "least_outstanding_requests"), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckAWSALBTargetGroupExists("aws_alb_target_group.test", &conf), - resource.TestCheckResourceAttrSet("aws_alb_target_group.test", "arn"), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "name", targetGroupName), - resource.TestCheckResourceAttr("aws_alb_target_group.test", "load_balancing_algorithm_type", "least_outstanding_requests"), + testAccCheckAWSALBTargetGroupExists(resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "load_balancing_algorithm_type", "least_outstanding_requests"), ), }, }, @@ -552,6 +566,7 @@ func TestAccAWSALBTargetGroup_lambda(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ @@ -585,6 +600,7 @@ func TestAccAWSALBTargetGroup_lambdaMultiValueHeadersEnabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ @@ -628,33 +644,34 @@ func TestAccAWSALBTargetGroup_lambdaMultiValueHeadersEnabled(t *testing.T) { } func TestAccAWSALBTargetGroup_missingPortProtocolVpc(t *testing.T) { - targetGroupName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) + rName := fmt.Sprintf("test-target-group-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elbv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSALBTargetGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSALBTargetGroupConfig_missing_port(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_missing_port(rName), ExpectError: regexp.MustCompile(`port should be set when target type is`), }, { - Config: testAccAWSALBTargetGroupConfig_missing_protocol(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_missing_protocol(rName), ExpectError: regexp.MustCompile(`protocol should be set when target type is`), }, { - Config: testAccAWSALBTargetGroupConfig_missing_vpc(targetGroupName), + Config: testAccAWSALBTargetGroupConfig_missing_vpc(rName), ExpectError: regexp.MustCompile(`vpc_id should be set when target type is`), }, }, }) } -func testAccAWSALBTargetGroupConfig_basic(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_basic(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id @@ -678,7 +695,7 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -686,15 +703,15 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_updatedPort(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_updatedPort(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 442 protocol = "HTTPS" vpc_id = aws_vpc.test.id @@ -718,7 +735,7 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -726,15 +743,15 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_updatedProtocol(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_updatedProtocol(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTP" vpc_id = aws_vpc.test2.id @@ -758,7 +775,7 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -766,7 +783,7 @@ resource "aws_vpc" "test2" { cidr_block = "10.10.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic-2" + Name = "%[1]s-2" } } @@ -774,15 +791,15 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_updatedVpc(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_updatedVpc(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id @@ -806,7 +823,7 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccAWSALBTargetGroup_basic" + TestName = %[1]q } } @@ -814,15 +831,15 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_updateTags(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_updateTags(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id @@ -855,15 +872,15 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_updateHealthCheck(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_updateHealthCheck(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id @@ -891,12 +908,12 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-basic" + Name = %[1]q } -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_stickiness(targetGroupName string, addStickinessBlock bool, enabled bool) string { +func testAccAWSALBTargetGroupConfig_stickiness(rName string, addStickinessBlock bool, enabled bool) string { var stickinessBlock string if addStickinessBlock { @@ -910,14 +927,14 @@ func testAccAWSALBTargetGroupConfig_stickiness(targetGroupName string, addSticki return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id deregistration_delay = 200 - %s + %[2]s health_check { path = "/health2" @@ -935,12 +952,12 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-stickiness" + Name = %[1]q } -}`, targetGroupName, stickinessBlock) +}`, rName, stickinessBlock) } -func testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(targetGroupName string, nonDefault bool, algoType string) string { +func testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(rName string, nonDefault bool, algoType string) string { var algoTypeParam string if nonDefault { @@ -949,33 +966,33 @@ func testAccAWSALBTargetGroupConfig_loadBalancingAlgorithm(targetGroupName strin return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" vpc_id = aws_vpc.test.id - %s + %[2]s } resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-load-balancing-algo" + Name = %[1]q } -}`, targetGroupName, algoTypeParam) +}`, rName, algoTypeParam) } -func testAccAWSALBTargetGroupConfig_updateSlowStart(targetGroupName string, slowStartDuration int) string { +func testAccAWSALBTargetGroupConfig_updateSlowStart(rName string, slowStartDuration int) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTP" vpc_id = aws_vpc.test.id deregistration_delay = 200 - slow_start = %d + slow_start = %[2]d stickiness { type = "lb_cookie" @@ -994,7 +1011,7 @@ resource "aws_alb_target_group" "test" { } tags = { - TestName = "TestAccAWSALBTargetGroup_SlowStart" + TestName = %[1]q } } @@ -1002,12 +1019,13 @@ resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-slowstart" + Name = %[1]q } -}`, targetGroupName, slowStartDuration) +}`, rName, slowStartDuration) } -const testAccAWSALBTargetGroupConfig_namePrefix = ` +func testAccAWSALBTargetGroupConfig_namePrefix(rName string) string { + return fmt.Sprintf(` resource "aws_alb_target_group" "test" { name_prefix = "tf-" port = 80 @@ -1018,12 +1036,14 @@ resource "aws_alb_target_group" "test" { resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-name-prefix" + Name = %[1]q } } -` +`, rName) +} -const testAccAWSALBTargetGroupConfig_generatedName = ` +func testAccAWSALBTargetGroupConfig_generatedName(rName string) string { + return fmt.Sprintf(` resource "aws_alb_target_group" "test" { port = 80 protocol = "HTTP" @@ -1042,17 +1062,18 @@ resource "aws_alb_target_group" "test" { resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" tags = { - Name = "terraform-testacc-alb-target-group-generated-name" + Name = %[1]q } } -` +`, rName) +} -func testAccAWSALBTargetGroupConfig_lambda(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_lambda(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q target_type = "lambda" -}`, targetGroupName) +}`, rName) } func testAccAWSALBTargetGroupConfig_lambdaMultiValueHeadersEnabled(rName string, lambdaMultiValueHadersEnabled bool) string { @@ -1065,38 +1086,38 @@ resource "aws_alb_target_group" "test" { `, lambdaMultiValueHadersEnabled, rName) } -func testAccAWSALBTargetGroupConfig_missing_port(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_missing_port(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q protocol = "HTTPS" vpc_id = aws_vpc.test.id } resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_missing_protocol(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_missing_protocol(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 vpc_id = aws_vpc.test.id } resource "aws_vpc" "test" { cidr_block = "10.0.0.0/16" -}`, targetGroupName) +}`, rName) } -func testAccAWSALBTargetGroupConfig_missing_vpc(targetGroupName string) string { +func testAccAWSALBTargetGroupConfig_missing_vpc(rName string) string { return fmt.Sprintf(` resource "aws_alb_target_group" "test" { - name = "%s" + name = %[1]q port = 443 protocol = "HTTPS" } -`, targetGroupName) +`, rName) } diff --git a/aws/resource_aws_ami.go b/aws/resource_aws_ami.go index 1fe711e8fd3a..f30b9ce11d67 100644 --- a/aws/resource_aws_ami.go +++ b/aws/resource_aws_ami.go @@ -46,22 +46,16 @@ func resourceAwsAmi() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "image_location": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, "architecture": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: ec2.ArchitectureValuesX8664, + ValidateFunc: validation.StringInSlice(ec2.ArchitectureValues_Values(), false), + }, + "arn": { Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: ec2.ArchitectureTypeX8664, - ValidateFunc: validation.StringInSlice([]string{ - ec2.ArchitectureTypeX8664, - ec2.ArchitectureValuesI386, - ec2.ArchitectureValuesArm64, - }, false), + Computed: true, }, "description": { Type: schema.TypeString, @@ -85,45 +79,38 @@ func resourceAwsAmi() *schema.Resource { Default: true, ForceNew: true, }, - "device_name": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "encrypted": { Type: schema.TypeBool, Optional: true, ForceNew: true, }, - "iops": { Type: schema.TypeInt, Optional: true, ForceNew: true, }, - "snapshot_id": { Type: schema.TypeString, Optional: true, ForceNew: true, }, - "throughput": { Type: schema.TypeInt, Optional: true, Computed: true, ForceNew: true, }, - "volume_size": { Type: schema.TypeInt, Optional: true, Computed: true, ForceNew: true, }, - "volume_type": { Type: schema.TypeString, Optional: true, @@ -157,7 +144,6 @@ func resourceAwsAmi() *schema.Resource { Type: schema.TypeString, Required: true, }, - "virtual_name": { Type: schema.TypeString, Required: true, @@ -172,6 +158,24 @@ func resourceAwsAmi() *schema.Resource { return hashcode.String(buf.String()) }, }, + "hypervisor": { + Type: schema.TypeString, + Computed: true, + }, + "image_location": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "image_owner_alias": { + Type: schema.TypeString, + Computed: true, + }, + "image_type": { + Type: schema.TypeString, + Computed: true, + }, "kernel_id": { Type: schema.TypeString, Optional: true, @@ -190,6 +194,22 @@ func resourceAwsAmi() *schema.Resource { Required: true, ForceNew: true, }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "platform_details": { + Type: schema.TypeString, + Computed: true, + }, + "platform": { + Type: schema.TypeString, + Computed: true, + }, + "public": { + Type: schema.TypeBool, + Computed: true, + }, "ramdisk_id": { Type: schema.TypeString, Optional: true, @@ -210,37 +230,39 @@ func resourceAwsAmi() *schema.Resource { ForceNew: true, Default: "simple", }, - "tags": tagsSchema(), - "virtualization_type": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: ec2.VirtualizationTypeParavirtual, - ValidateFunc: validation.StringInSlice([]string{ - ec2.VirtualizationTypeParavirtual, - ec2.VirtualizationTypeHvm, - }, false), - }, - "arn": { + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + "usage_operation": { Type: schema.TypeString, Computed: true, }, + "virtualization_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: ec2.VirtualizationTypeParavirtual, + ValidateFunc: validation.StringInSlice(ec2.VirtualizationType_Values(), false), + }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &ec2.RegisterImageInput{ - Name: aws.String(d.Get("name").(string)), - Description: aws.String(d.Get("description").(string)), Architecture: aws.String(d.Get("architecture").(string)), + Description: aws.String(d.Get("description").(string)), + EnaSupport: aws.Bool(d.Get("ena_support").(bool)), ImageLocation: aws.String(d.Get("image_location").(string)), + Name: aws.String(d.Get("name").(string)), RootDeviceName: aws.String(d.Get("root_device_name").(string)), SriovNetSupport: aws.String(d.Get("sriov_net_support").(string)), VirtualizationType: aws.String(d.Get("virtualization_type").(string)), - EnaSupport: aws.Bool(d.Get("ena_support").(bool)), } if kernelId := d.Get("kernel_id").(string); kernelId != "" { @@ -287,11 +309,11 @@ func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { return err } - id := *res.ImageId + id := aws.StringValue(res.ImageId) d.SetId(id) - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - if err := keyvaluetags.Ec2CreateTags(client, id, v); err != nil { + if len(tags) > 0 { + if err := keyvaluetags.Ec2CreateTags(client, id, tags); err != nil { return fmt.Errorf("error adding tags: %s", err) } } @@ -306,6 +328,7 @@ func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig id := d.Id() @@ -339,14 +362,18 @@ func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Unable to find AMI after retries: %s", err) } - if len(res.Images) != 1 { + if res == nil || len(res.Images) != 1 { + if d.IsNewResource() { + return fmt.Errorf("error reading EC2 AMI (%s): empty response", d.Id()) + } + log.Printf("[WARN] AMI (%s) not found, removing from state", d.Id()) d.SetId("") return nil } image := res.Images[0] - state := *image.State + state := aws.StringValue(image.State) if state == ec2.ImageStatePending { // This could happen if a user manually adds an image we didn't create @@ -358,10 +385,14 @@ func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { if err != nil { return err } - state = *image.State + state = aws.StringValue(image.State) } if state == ec2.ImageStateDeregistered { + if d.IsNewResource() { + return fmt.Errorf("error reading EC2 AMI (%s): deregistered", d.Id()) + } + log.Printf("[WARN] AMI (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -371,23 +402,31 @@ func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("AMI has become %s", state) } - d.Set("name", image.Name) + d.Set("architecture", image.Architecture) d.Set("description", image.Description) + d.Set("ena_support", image.EnaSupport) + d.Set("hypervisor", image.Hypervisor) d.Set("image_location", image.ImageLocation) - d.Set("architecture", image.Architecture) + d.Set("image_owner_alias", image.ImageOwnerAlias) + d.Set("image_type", image.ImageType) d.Set("kernel_id", image.KernelId) + d.Set("name", image.Name) + d.Set("owner_id", image.OwnerId) + d.Set("platform_details", image.PlatformDetails) + d.Set("platform", image.Platform) + d.Set("public", image.Public) d.Set("ramdisk_id", image.RamdiskId) d.Set("root_device_name", image.RootDeviceName) d.Set("root_snapshot_id", amiRootSnapshotId(image)) d.Set("sriov_net_support", image.SriovNetSupport) + d.Set("usage_operation", image.UsageOperation) d.Set("virtualization_type", image.VirtualizationType) - d.Set("ena_support", image.EnaSupport) imageArn := arn.ARN{ Partition: meta.(*AWSClient).partition, Region: meta.(*AWSClient).region, Resource: fmt.Sprintf("image/%s", d.Id()), - Service: "ec2", + Service: ec2.ServiceName, }.String() d.Set("arn", imageArn) @@ -400,18 +439,25 @@ func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting ephemeral_block_device: %w", err) } - if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(image.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags := keyvaluetags.Ec2KeyValueTags(image.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } func resourceAwsAmiUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*AWSClient).ec2conn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Ec2UpdateTags(client, d.Id(), o, n); err != nil { return fmt.Errorf("error updating AMI (%s) tags: %s", d.Id(), err) @@ -500,7 +546,7 @@ func AMIStateRefreshFunc(client *ec2.EC2, id string) resource.StateRefreshFunc { } // AMI is valid, so return it's state - return resp.Images[0], *resp.Images[0].State, nil + return resp.Images[0], aws.StringValue(resp.Images[0].State), nil } } diff --git a/aws/resource_aws_ami_copy.go b/aws/resource_aws_ami_copy.go index 07e0e92ebec3..d314fd818a9f 100644 --- a/aws/resource_aws_ami_copy.go +++ b/aws/resource_aws_ami_copy.go @@ -26,6 +26,10 @@ func resourceAwsAmiCopy() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, "description": { Type: schema.TypeString, Optional: true, @@ -91,6 +95,16 @@ func resourceAwsAmiCopy() *schema.Resource { return hashcode.String(buf.String()) }, }, + "ena_support": { + Type: schema.TypeBool, + Computed: true, + }, + "encrypted": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, "ephemeral_block_device": { Type: schema.TypeSet, Optional: true, @@ -117,20 +131,22 @@ func resourceAwsAmiCopy() *schema.Resource { return hashcode.String(buf.String()) }, }, - "ena_support": { - Type: schema.TypeBool, + "hypervisor": { + Type: schema.TypeString, Computed: true, }, - "encrypted": { - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - }, "image_location": { Type: schema.TypeString, Computed: true, }, + "image_owner_alias": { + Type: schema.TypeString, + Computed: true, + }, + "image_type": { + Type: schema.TypeString, + Computed: true, + }, "kernel_id": { Type: schema.TypeString, Computed: true, @@ -155,6 +171,22 @@ func resourceAwsAmiCopy() *schema.Resource { Required: true, ForceNew: true, }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "platform": { + Type: schema.TypeString, + Computed: true, + }, + "platform_details": { + Type: schema.TypeString, + Computed: true, + }, + "public": { + Type: schema.TypeBool, + Computed: true, + }, "ramdisk_id": { Type: schema.TypeString, Computed: true, @@ -177,21 +209,29 @@ func resourceAwsAmiCopy() *schema.Resource { Required: true, ForceNew: true, }, + "destination_outpost_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, "sriov_net_support": { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "virtualization_type": { Type: schema.TypeString, Computed: true, }, - "arn": { + "usage_operation": { Type: schema.TypeString, Computed: true, }, }, + CustomizeDiff: SetTagsDiff, + // The remaining operations are shared with the generic aws_ami resource, // since the aws_ami_copy resource only differs in how it's created. Read: resourceAwsAmiRead, @@ -202,19 +242,25 @@ func resourceAwsAmiCopy() *schema.Resource { func resourceAwsAmiCopyCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &ec2.CopyImageInput{ - Name: aws.String(d.Get("name").(string)), Description: aws.String(d.Get("description").(string)), + Encrypted: aws.Bool(d.Get("encrypted").(bool)), + Name: aws.String(d.Get("name").(string)), SourceImageId: aws.String(d.Get("source_ami_id").(string)), SourceRegion: aws.String(d.Get("source_ami_region").(string)), - Encrypted: aws.Bool(d.Get("encrypted").(bool)), } if v, ok := d.GetOk("kms_key_id"); ok { req.KmsKeyId = aws.String(v.(string)) } + if v, ok := d.GetOk("destination_outpost_arn"); ok { + req.DestinationOutpostArn = aws.String(v.(string)) + } + res, err := client.CopyImage(req) if err != nil { return err @@ -223,8 +269,8 @@ func resourceAwsAmiCopyCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(aws.StringValue(res.ImageId)) d.Set("manage_ebs_snapshots", true) - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - if err := keyvaluetags.Ec2CreateTags(client, d.Id(), v); err != nil { + if len(tags) > 0 { + if err := keyvaluetags.Ec2CreateTags(client, d.Id(), tags); err != nil { return fmt.Errorf("error adding tags: %s", err) } } diff --git a/aws/resource_aws_ami_copy_test.go b/aws/resource_aws_ami_copy_test.go index 1d3cbc34b05f..ae7bf4582e18 100644 --- a/aws/resource_aws_ami_copy_test.go +++ b/aws/resource_aws_ami_copy_test.go @@ -19,6 +19,7 @@ func TestAccAWSAMICopy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMICopyDestroy, Steps: []resource.TestStep{ @@ -28,6 +29,11 @@ func TestAccAWSAMICopy_basic(t *testing.T) { testAccCheckAWSAMICopyExists(resourceName, &image), testAccCheckAWSAMICopyAttributes(&image, rName), testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), + resource.TestCheckResourceAttr(resourceName, "usage_operation", "RunInstances"), + resource.TestCheckResourceAttr(resourceName, "platform_details", "Linux/UNIX"), + resource.TestCheckResourceAttr(resourceName, "image_type", "machine"), + resource.TestCheckResourceAttr(resourceName, "hypervisor", "xen"), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), ), }, }, @@ -41,6 +47,7 @@ func TestAccAWSAMICopy_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMICopyDestroy, Steps: []resource.TestStep{ @@ -69,6 +76,7 @@ func TestAccAWSAMICopy_EnaSupport(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMICopyDestroy, Steps: []resource.TestStep{ @@ -83,6 +91,29 @@ func TestAccAWSAMICopy_EnaSupport(t *testing.T) { }) } +func TestAccAWSAMICopy_DestinationOutpost(t *testing.T) { + var image ec2.Image + rName := acctest.RandomWithPrefix("tf-acc-test") + outpostDataSourceName := "data.aws_outposts_outpost.test" + resourceName := "aws_ami_copy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAMICopyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAMICopyConfigDestOutpost(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAMICopyExists(resourceName, &image), + resource.TestCheckResourceAttrPair(resourceName, "destination_outpost_arn", outpostDataSourceName, "arn"), + ), + }, + }, + }) +} + func TestAccAWSAMICopy_tags(t *testing.T) { var ami ec2.Image resourceName := "aws_ami_copy.test" @@ -90,6 +121,7 @@ func TestAccAWSAMICopy_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMICopyDestroy, Steps: []resource.TestStep{ @@ -358,3 +390,32 @@ resource "aws_ami_copy" "test" { } `, rName, rName) } + +func testAccAWSAMICopyConfigDestOutpost(rName string) string { + return testAccAWSAMICopyConfigBase(rName) + fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_ami" "test" { + ena_support = true + name = "%s-source" + virtualization_type = "hvm" + root_device_name = "/dev/sda1" + + ebs_block_device { + device_name = "/dev/sda1" + snapshot_id = aws_ebs_snapshot.test.id + } +} + +resource "aws_ami_copy" "test" { + name = "%s-copy" + source_ami_id = aws_ami.test.id + source_ami_region = data.aws_region.current.name + destination_outpost_arn = data.aws_outposts_outpost.test.arn +} +`, rName, rName) +} diff --git a/aws/resource_aws_ami_from_instance.go b/aws/resource_aws_ami_from_instance.go index 9cdadc90815a..96fd68a2a156 100644 --- a/aws/resource_aws_ami_from_instance.go +++ b/aws/resource_aws_ami_from_instance.go @@ -26,6 +26,10 @@ func resourceAwsAmiFromInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, "description": { Type: schema.TypeString, Optional: true, @@ -121,10 +125,22 @@ func resourceAwsAmiFromInstance() *schema.Resource { return hashcode.String(buf.String()) }, }, + "hypervisor": { + Type: schema.TypeString, + Computed: true, + }, "image_location": { Type: schema.TypeString, Computed: true, }, + "image_owner_alias": { + Type: schema.TypeString, + Computed: true, + }, + "image_type": { + Type: schema.TypeString, + Computed: true, + }, "kernel_id": { Type: schema.TypeString, Computed: true, @@ -142,6 +158,22 @@ func resourceAwsAmiFromInstance() *schema.Resource { Required: true, ForceNew: true, }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "platform": { + Type: schema.TypeString, + Computed: true, + }, + "platform_details": { + Type: schema.TypeString, + Computed: true, + }, + "public": { + Type: schema.TypeBool, + Computed: true, + }, "ramdisk_id": { Type: schema.TypeString, Computed: true, @@ -168,17 +200,20 @@ func resourceAwsAmiFromInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), - "virtualization_type": { + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + "usage_operation": { Type: schema.TypeString, Computed: true, }, - "arn": { + "virtualization_type": { Type: schema.TypeString, Computed: true, }, }, + CustomizeDiff: SetTagsDiff, + // The remaining operations are shared with the generic aws_ami resource, // since the aws_ami_copy resource only differs in how it's created. Read: resourceAwsAmiRead, @@ -189,12 +224,15 @@ func resourceAwsAmiFromInstance() *schema.Resource { func resourceAwsAmiFromInstanceCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &ec2.CreateImageInput{ - Name: aws.String(d.Get("name").(string)), - Description: aws.String(d.Get("description").(string)), - InstanceId: aws.String(d.Get("source_instance_id").(string)), - NoReboot: aws.Bool(d.Get("snapshot_without_reboot").(bool)), + Description: aws.String(d.Get("description").(string)), + InstanceId: aws.String(d.Get("source_instance_id").(string)), + Name: aws.String(d.Get("name").(string)), + NoReboot: aws.Bool(d.Get("snapshot_without_reboot").(bool)), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeImage), } res, err := client.CreateImage(req) @@ -205,12 +243,6 @@ func resourceAwsAmiFromInstanceCreate(d *schema.ResourceData, meta interface{}) d.SetId(aws.StringValue(res.ImageId)) d.Set("manage_ebs_snapshots", true) - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - if err := keyvaluetags.Ec2CreateTags(client, d.Id(), v); err != nil { - return fmt.Errorf("error adding tags: %s", err) - } - } - _, err = resourceAwsAmiWaitForAvailable(d.Timeout(schema.TimeoutCreate), d.Id(), client) if err != nil { return err diff --git a/aws/resource_aws_ami_from_instance_test.go b/aws/resource_aws_ami_from_instance_test.go index 019d34fe7b0b..98cc9c5a9ed7 100644 --- a/aws/resource_aws_ami_from_instance_test.go +++ b/aws/resource_aws_ami_from_instance_test.go @@ -19,6 +19,7 @@ func TestAccAWSAMIFromInstance_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMIFromInstanceDestroy, Steps: []resource.TestStep{ @@ -28,6 +29,12 @@ func TestAccAWSAMIFromInstance_basic(t *testing.T) { testAccCheckAWSAMIFromInstanceExists(resourceName, &image), testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), resource.TestCheckResourceAttr(resourceName, "description", "Testing Terraform aws_ami_from_instance resource"), + resource.TestCheckResourceAttr(resourceName, "usage_operation", "RunInstances"), + resource.TestCheckResourceAttr(resourceName, "platform_details", "Linux/UNIX"), + resource.TestCheckResourceAttr(resourceName, "image_type", "machine"), + resource.TestCheckResourceAttr(resourceName, "hypervisor", "xen"), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, }, @@ -41,6 +48,7 @@ func TestAccAWSAMIFromInstance_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMIFromInstanceDestroy, Steps: []resource.TestStep{ @@ -73,6 +81,29 @@ func TestAccAWSAMIFromInstance_tags(t *testing.T) { }) } +func TestAccAWSAMIFromInstance_disappears(t *testing.T) { + var image ec2.Image + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ami_from_instance.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAMIFromInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAMIFromInstanceConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAMIFromInstanceExists(resourceName, &image), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmiFromInstance(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAWSAMIFromInstanceExists(resourceName string, image *ec2.Image) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/aws/resource_aws_ami_launch_permission.go b/aws/resource_aws_ami_launch_permission.go index 43bcd8f21f82..8d4d190d2812 100644 --- a/aws/resource_aws_ami_launch_permission.go +++ b/aws/resource_aws_ami_launch_permission.go @@ -77,6 +77,10 @@ func resourceAwsAmiLaunchPermissionRead(d *schema.ResourceData, meta interface{} return fmt.Errorf("error reading AMI launch permission (%s): %w", d.Id(), err) } if !exists { + if d.IsNewResource() { + return fmt.Errorf("error reading EC2 AMI Launch Permission (%s): not found", d.Id()) + } + log.Printf("[WARN] AMI launch permission (%s) not found, removing from state", d.Id()) d.SetId("") return nil diff --git a/aws/resource_aws_ami_launch_permission_test.go b/aws/resource_aws_ami_launch_permission_test.go index 417ba85c2cfa..65dc0e311d35 100644 --- a/aws/resource_aws_ami_launch_permission_test.go +++ b/aws/resource_aws_ami_launch_permission_test.go @@ -17,6 +17,7 @@ func TestAccAWSAMILaunchPermission_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMILaunchPermissionDestroy, Steps: []resource.TestStep{ @@ -42,6 +43,7 @@ func TestAccAWSAMILaunchPermission_Disappears_LaunchPermission(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMILaunchPermissionDestroy, Steps: []resource.TestStep{ @@ -65,6 +67,7 @@ func TestAccAWSAMILaunchPermission_Disappears_LaunchPermission_Public(t *testing resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMILaunchPermissionDestroy, Steps: []resource.TestStep{ @@ -88,6 +91,7 @@ func TestAccAWSAMILaunchPermission_Disappears_AMI(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAMILaunchPermissionDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_ami_test.go b/aws/resource_aws_ami_test.go index 730c052688d9..3d601b905521 100644 --- a/aws/resource_aws_ami_test.go +++ b/aws/resource_aws_ami_test.go @@ -22,6 +22,7 @@ func TestAccAWSAMI_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -47,12 +48,18 @@ func TestAccAWSAMI_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "usage_operation", "RunInstances"), + resource.TestCheckResourceAttr(resourceName, "platform_details", "Linux/UNIX"), + resource.TestCheckResourceAttr(resourceName, "image_type", "machine"), + resource.TestCheckResourceAttr(resourceName, "hypervisor", "xen"), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), ), }, { @@ -77,6 +84,7 @@ func TestAccAWSAMI_description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -102,6 +110,7 @@ func TestAccAWSAMI_description(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), @@ -140,6 +149,7 @@ func TestAccAWSAMI_description(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), @@ -159,6 +169,7 @@ func TestAccAWSAMI_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -182,6 +193,7 @@ func TestAccAWSAMI_EphemeralBlockDevices(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -214,6 +226,7 @@ func TestAccAWSAMI_EphemeralBlockDevices(t *testing.T) { }), resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), @@ -242,6 +255,7 @@ func TestAccAWSAMI_Gp3BlockDevice(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -276,6 +290,7 @@ func TestAccAWSAMI_Gp3BlockDevice(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner_id"), resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), @@ -303,6 +318,7 @@ func TestAccAWSAMI_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ @@ -367,7 +383,8 @@ func testAccCheckAmiDestroy(s *terraform.State) error { if len(resp.Images) > 0 { state := resp.Images[0].State - return fmt.Errorf("AMI %s still exists in the state: %s.", *resp.Images[0].ImageId, *state) + return fmt.Errorf("AMI %s still exists in the state: %s.", aws.StringValue(resp.Images[0].ImageId), + aws.StringValue(state)) } } return nil diff --git a/aws/resource_aws_amplify_app.go b/aws/resource_aws_amplify_app.go new file mode 100644 index 000000000000..b28d2ba01202 --- /dev/null +++ b/aws/resource_aws_amplify_app.go @@ -0,0 +1,818 @@ +package aws + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAmplifyApp() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyAppCreate, + Read: resourceAwsAmplifyAppRead, + Update: resourceAwsAmplifyAppUpdate, + Delete: resourceAwsAmplifyAppDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + CustomizeDiff: customdiff.Sequence( + SetTagsDiff, + customdiff.ForceNewIfChange("description", func(_ context.Context, old, new, meta interface{}) bool { + // Any existing value cannot be cleared. + return new.(string) == "" + }), + customdiff.ForceNewIfChange("iam_service_role_arn", func(_ context.Context, old, new, meta interface{}) bool { + // Any existing value cannot be cleared. + return new.(string) == "" + }), + ), + + Schema: map[string]*schema.Schema{ + "access_token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "auto_branch_creation_config": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "basic_auth_credentials": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 2000), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // These credentials are ignored if basic auth is not enabled. + if d.Get("auto_branch_creation_config.0.enable_basic_auth").(bool) { + return old == new + } + + return true + }, + }, + + "build_spec": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 25000), + }, + + "enable_auto_build": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_basic_auth": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_performance_mode": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "enable_pull_request_preview": { + Type: schema.TypeBool, + Optional: true, + }, + + "environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "framework": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "pull_request_environment_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "stage": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(amplify.Stage_Values(), false), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // API returns "NONE" by default. + if old == tfamplify.StageNone && new == "" { + return true + } + + return old == new + }, + }, + }, + }, + }, + + "auto_branch_creation_patterns": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // These patterns are ignored if branch auto-creation is not enabled. + if d.Get("enable_auto_branch_creation").(bool) { + return old == new + } + + return true + }, + }, + + "basic_auth_credentials": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 2000), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // These credentials are ignored if basic auth is not enabled. + if d.Get("enable_basic_auth").(bool) { + return old == new + } + + return true + }, + }, + + "build_spec": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(1, 25000), + }, + + "custom_rule": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "condition": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + + "source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + + "status": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "200", + "301", + "302", + "404", + "404-200", + }, false), + }, + + "target": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + }, + }, + }, + + "default_domain": { + Type: schema.TypeString, + Computed: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "enable_auto_branch_creation": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_basic_auth": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_branch_auto_build": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_branch_auto_deletion": { + Type: schema.TypeBool, + Optional: true, + }, + + "environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "iam_service_role_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "oauth_token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "platform": { + Type: schema.TypeString, + Optional: true, + Default: amplify.PlatformWeb, + ValidateFunc: validation.StringInSlice(amplify.Platform_Values(), false), + }, + + "production_branch": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch_name": { + Type: schema.TypeString, + Computed: true, + }, + + "last_deploy_time": { + Type: schema.TypeString, + Computed: true, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "thumbnail_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "repository": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +func resourceAwsAmplifyAppCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("name").(string) + + input := &lify.CreateAppInput{ + Name: aws.String(name), + } + + if v, ok := d.GetOk("access_token"); ok { + input.AccessToken = aws.String(v.(string)) + } + + if v, ok := d.GetOk("auto_branch_creation_config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.AutoBranchCreationConfig = expandAmplifyAutoBranchCreationConfig(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("auto_branch_creation_patterns"); ok && v.(*schema.Set).Len() > 0 { + input.AutoBranchCreationPatterns = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("basic_auth_credentials"); ok { + input.BasicAuthCredentials = aws.String(v.(string)) + } + + if v, ok := d.GetOk("build_spec"); ok { + input.BuildSpec = aws.String(v.(string)) + } + + if v, ok := d.GetOk("custom_rule"); ok && len(v.([]interface{})) > 0 { + input.CustomRules = expandAmplifyCustomRules(v.([]interface{})) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("enable_auto_branch_creation"); ok { + input.EnableAutoBranchCreation = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_basic_auth"); ok { + input.EnableBasicAuth = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_branch_auto_build"); ok { + input.EnableBranchAutoBuild = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_branch_auto_deletion"); ok { + input.EnableBranchAutoDeletion = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("environment_variables"); ok && len(v.(map[string]interface{})) > 0 { + input.EnvironmentVariables = expandStringMap(v.(map[string]interface{})) + } + + if v, ok := d.GetOk("iam_service_role_arn"); ok { + input.IamServiceRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("oauth_token"); ok { + input.OauthToken = aws.String(v.(string)) + } + + if v, ok := d.GetOk("platform"); ok { + input.Platform = aws.String(v.(string)) + } + + if v, ok := d.GetOk("repository"); ok { + input.Repository = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AmplifyTags() + } + + log.Printf("[DEBUG] Creating Amplify App: %s", input) + output, err := conn.CreateApp(input) + + if err != nil { + return fmt.Errorf("error creating Amplify App (%s): %w", name, err) + } + + d.SetId(aws.StringValue(output.App.AppId)) + + return resourceAwsAmplifyAppRead(d, meta) +} + +func resourceAwsAmplifyAppRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + app, err := finder.AppByID(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify App (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify App (%s): %w", d.Id(), err) + } + + d.Set("arn", app.AppArn) + if app.AutoBranchCreationConfig != nil { + if err := d.Set("auto_branch_creation_config", []interface{}{flattenAmplifyAutoBranchCreationConfig(app.AutoBranchCreationConfig)}); err != nil { + return fmt.Errorf("error setting auto_branch_creation_config: %w", err) + } + } else { + d.Set("auto_branch_creation_config", nil) + } + d.Set("auto_branch_creation_patterns", aws.StringValueSlice(app.AutoBranchCreationPatterns)) + d.Set("basic_auth_credentials", app.BasicAuthCredentials) + d.Set("build_spec", app.BuildSpec) + if err := d.Set("custom_rule", flattenAmplifyCustomRules(app.CustomRules)); err != nil { + return fmt.Errorf("error setting custom_rule: %w", err) + } + d.Set("default_domain", app.DefaultDomain) + d.Set("description", app.Description) + d.Set("enable_auto_branch_creation", app.EnableAutoBranchCreation) + d.Set("enable_basic_auth", app.EnableBasicAuth) + d.Set("enable_branch_auto_build", app.EnableBranchAutoBuild) + d.Set("enable_branch_auto_deletion", app.EnableBranchAutoDeletion) + d.Set("environment_variables", aws.StringValueMap(app.EnvironmentVariables)) + d.Set("iam_service_role_arn", app.IamServiceRoleArn) + d.Set("name", app.Name) + d.Set("platform", app.Platform) + if app.ProductionBranch != nil { + if err := d.Set("production_branch", []interface{}{flattenAmplifyProductionBranch(app.ProductionBranch)}); err != nil { + return fmt.Errorf("error setting production_branch: %w", err) + } + } else { + d.Set("production_branch", nil) + } + d.Set("repository", app.Repository) + + tags := keyvaluetags.AmplifyKeyValueTags(app.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceAwsAmplifyAppUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + if d.HasChangesExcept("tags", "tags_all") { + input := &lify.UpdateAppInput{ + AppId: aws.String(d.Id()), + } + + if d.HasChange("access_token") { + input.AccessToken = aws.String(d.Get("access_token").(string)) + } + + if d.HasChange("auto_branch_creation_config") { + input.AutoBranchCreationConfig = expandAmplifyAutoBranchCreationConfig(d.Get("auto_branch_creation_config").([]interface{})[0].(map[string]interface{})) + + if d.HasChange("auto_branch_creation_config.0.environment_variables") { + if v := d.Get("auto_branch_creation_config.0.environment_variables").(map[string]interface{}); len(v) == 0 { + input.AutoBranchCreationConfig.EnvironmentVariables = aws.StringMap(map[string]string{"": ""}) + } + } + } + + if d.HasChange("auto_branch_creation_patterns") { + input.AutoBranchCreationPatterns = expandStringSet(d.Get("auto_branch_creation_patterns").(*schema.Set)) + } + + if d.HasChange("basic_auth_credentials") { + input.BasicAuthCredentials = aws.String(d.Get("basic_auth_credentials").(string)) + } + + if d.HasChange("build_spec") { + input.BuildSpec = aws.String(d.Get("build_spec").(string)) + } + + if d.HasChange("custom_rule") { + if v := d.Get("custom_rule").([]interface{}); len(v) > 0 { + input.CustomRules = expandAmplifyCustomRules(v) + } else { + input.CustomRules = []*amplify.CustomRule{} + } + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("enable_auto_branch_creation") { + input.EnableAutoBranchCreation = aws.Bool(d.Get("enable_auto_branch_creation").(bool)) + } + + if d.HasChange("enable_basic_auth") { + input.EnableBasicAuth = aws.Bool(d.Get("enable_basic_auth").(bool)) + } + + if d.HasChange("enable_branch_auto_build") { + input.EnableBranchAutoBuild = aws.Bool(d.Get("enable_branch_auto_build").(bool)) + } + + if d.HasChange("enable_branch_auto_deletion") { + input.EnableBranchAutoDeletion = aws.Bool(d.Get("enable_branch_auto_deletion").(bool)) + } + + if d.HasChange("environment_variables") { + if v := d.Get("environment_variables").(map[string]interface{}); len(v) > 0 { + input.EnvironmentVariables = expandStringMap(v) + } else { + input.EnvironmentVariables = aws.StringMap(map[string]string{"": ""}) + } + } + + if d.HasChange("iam_service_role_arn") { + input.IamServiceRoleArn = aws.String(d.Get("iam_service_role_arn").(string)) + } + + if d.HasChange("name") { + input.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("oauth_token") { + input.OauthToken = aws.String(d.Get("oauth_token").(string)) + } + + if d.HasChange("platform") { + input.Platform = aws.String(d.Get("platform").(string)) + } + + if d.HasChange("repository") { + input.Repository = aws.String(d.Get("repository").(string)) + } + + _, err := conn.UpdateApp(input) + + if err != nil { + return fmt.Errorf("error updating Amplify App (%s): %w", d.Id(), err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.AmplifyUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + + return resourceAwsAmplifyAppRead(d, meta) +} + +func resourceAwsAmplifyAppDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + log.Printf("[DEBUG] Deleting Amplify App (%s)", d.Id()) + _, err := conn.DeleteApp(&lify.DeleteAppInput{ + AppId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify App (%s): %w", d.Id(), err) + } + + return nil +} + +func expandAmplifyAutoBranchCreationConfig(tfMap map[string]interface{}) *amplify.AutoBranchCreationConfig { + if tfMap == nil { + return nil + } + + apiObject := &lify.AutoBranchCreationConfig{} + + if v, ok := tfMap["basic_auth_credentials"].(string); ok && v != "" { + apiObject.BasicAuthCredentials = aws.String(v) + } + + if v, ok := tfMap["build_spec"].(string); ok && v != "" { + apiObject.BuildSpec = aws.String(v) + } + + if v, ok := tfMap["enable_auto_build"].(bool); ok { + apiObject.EnableAutoBuild = aws.Bool(v) + } + + if v, ok := tfMap["enable_basic_auth"].(bool); ok { + apiObject.EnableBasicAuth = aws.Bool(v) + } + + if v, ok := tfMap["enable_performance_mode"].(bool); ok { + apiObject.EnablePerformanceMode = aws.Bool(v) + } + + if v, ok := tfMap["enable_pull_request_preview"].(bool); ok { + apiObject.EnablePullRequestPreview = aws.Bool(v) + } + + if v, ok := tfMap["environment_variables"].(map[string]interface{}); ok && len(v) > 0 { + apiObject.EnvironmentVariables = expandStringMap(v) + } + + if v, ok := tfMap["framework"].(string); ok && v != "" { + apiObject.Framework = aws.String(v) + } + + if v, ok := tfMap["pull_request_environment_name"].(string); ok && v != "" { + apiObject.PullRequestEnvironmentName = aws.String(v) + } + + if v, ok := tfMap["stage"].(string); ok && v != "" && v != tfamplify.StageNone { + apiObject.Stage = aws.String(v) + } + + return apiObject +} + +func flattenAmplifyAutoBranchCreationConfig(apiObject *amplify.AutoBranchCreationConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.BasicAuthCredentials; v != nil { + tfMap["basic_auth_credentials"] = aws.StringValue(v) + } + + if v := apiObject.BuildSpec; v != nil { + tfMap["build_spec"] = aws.StringValue(v) + } + + if v := apiObject.EnableAutoBuild; v != nil { + tfMap["enable_auto_build"] = aws.BoolValue(v) + } + + if v := apiObject.EnableBasicAuth; v != nil { + tfMap["enable_basic_auth"] = aws.BoolValue(v) + } + + if v := apiObject.EnablePerformanceMode; v != nil { + tfMap["enable_performance_mode"] = aws.BoolValue(v) + } + + if v := apiObject.EnablePullRequestPreview; v != nil { + tfMap["enable_pull_request_preview"] = aws.BoolValue(v) + } + + if v := apiObject.EnvironmentVariables; v != nil { + tfMap["environment_variables"] = aws.StringValueMap(v) + } + + if v := apiObject.Framework; v != nil { + tfMap["framework"] = aws.StringValue(v) + } + + if v := apiObject.PullRequestEnvironmentName; v != nil { + tfMap["pull_request_environment_name"] = aws.StringValue(v) + } + + if v := apiObject.Stage; v != nil { + tfMap["stage"] = aws.StringValue(v) + } + + return tfMap +} + +func expandAmplifyCustomRule(tfMap map[string]interface{}) *amplify.CustomRule { + if tfMap == nil { + return nil + } + + apiObject := &lify.CustomRule{} + + if v, ok := tfMap["condition"].(string); ok && v != "" { + apiObject.Condition = aws.String(v) + } + + if v, ok := tfMap["source"].(string); ok && v != "" { + apiObject.Source = aws.String(v) + } + + if v, ok := tfMap["status"].(string); ok && v != "" { + apiObject.Status = aws.String(v) + } + + if v, ok := tfMap["target"].(string); ok && v != "" { + apiObject.Target = aws.String(v) + } + + return apiObject +} + +func expandAmplifyCustomRules(tfList []interface{}) []*amplify.CustomRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*amplify.CustomRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandAmplifyCustomRule(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenAmplifyCustomRule(apiObject *amplify.CustomRule) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Condition; v != nil { + tfMap["condition"] = aws.StringValue(v) + } + + if v := apiObject.Source; v != nil { + tfMap["source"] = aws.StringValue(v) + } + + if v := apiObject.Status; v != nil { + tfMap["status"] = aws.StringValue(v) + } + + if v := apiObject.Target; v != nil { + tfMap["target"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenAmplifyCustomRules(apiObjects []*amplify.CustomRule) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenAmplifyCustomRule(apiObject)) + } + + return tfList +} + +func flattenAmplifyProductionBranch(apiObject *amplify.ProductionBranch) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.BranchName; v != nil { + tfMap["branch_name"] = aws.StringValue(v) + } + + if v := apiObject.LastDeployTime; v != nil { + tfMap["last_deploy_time"] = aws.TimeValue(v).Format(time.RFC3339) + } + + if v := apiObject.Status; v != nil { + tfMap["status"] = aws.StringValue(v) + } + + if v := apiObject.ThumbnailUrl; v != nil { + tfMap["thumbnail_url"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/aws/resource_aws_amplify_app_test.go b/aws/resource_aws_amplify_app_test.go new file mode 100644 index 000000000000..94b49a37ee83 --- /dev/null +++ b/aws/resource_aws_amplify_app_test.go @@ -0,0 +1,970 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "log" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/go-multierror" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/lister" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + resource.AddTestSweepers("aws_amplify_app", &resource.Sweeper{ + Name: "aws_amplify_app", + F: testSweepAmplifyApps, + }) +} + +func testSweepAmplifyApps(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).amplifyconn + input := &lify.ListAppsInput{} + var sweeperErrs *multierror.Error + + err = lister.ListAppsPages(conn, input, func(page *amplify.ListAppsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, app := range page.Apps { + r := resourceAwsAmplifyApp() + d := r.Data(nil) + d.SetId(aws.StringValue(app.AppId)) + err = r.Delete(d, client) + + if err != nil { + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Amplify Apps sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Amplify Apps: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func testAccAWSAmplifyApp_basic(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckNoResourceAttr(resourceName, "access_token"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+`)), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "0"), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "build_spec", ""), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "0"), + resource.TestMatchResourceAttr(resourceName, "default_domain", regexp.MustCompile(`\.amplifyapp\.com$`)), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_branch_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_branch_auto_deletion", "false"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "iam_service_role_arn", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckNoResourceAttr(resourceName, "oauth_token"), + resource.TestCheckResourceAttr(resourceName, "platform", "WEB"), + resource.TestCheckResourceAttr(resourceName, "production_branch.#", "0"), + resource.TestCheckResourceAttr(resourceName, "repository", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyApp_disappears(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyApp(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyApp_Tags(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_AutoBranchCreationConfig(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + credentials := base64.StdEncoding.EncodeToString([]byte("username1:password1")) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigNoAutoBranchCreationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "NONE"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "*"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.1", "*/**"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfig(rName, credentials), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", credentials), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", "version: 0.1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.ENVVAR1", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", "React"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", "test1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "DEVELOPMENT"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "feature/*"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfigUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", "version: 0.2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", "React"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", "test2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "EXPERIMENTAL"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "feature/*"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // No change is reflected in API. + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "0"), + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "0"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_BasicAuthCredentials(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + credentials1 := base64.StdEncoding.EncodeToString([]byte("username1:password1")) + credentials2 := base64.StdEncoding.EncodeToString([]byte("username2:password2")) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, credentials1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials1), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, credentials2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials2), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_BuildSpec(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigBuildSpec(rName, "version: 0.1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigBuildSpec(rName, "version: 0.2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // build_spec is Computed. + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_CustomRules(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigCustomRules(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.source", "/<*>"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.status", "404"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.target", "/index.html"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigCustomRulesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.condition", ""), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.source", "/documents"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.status", "302"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.target", "/documents/us"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.source", "/<*>"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.status", "200"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.target", "/index.html"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Description(t *testing.T) { + var app1, app2, app3 amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigDescription(rName, "description 1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app1), + resource.TestCheckResourceAttr(resourceName, "description", "description 1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigDescription(rName, "description 2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app2), + testAccCheckAWSAmplifyAppNotRecreated(&app1, &app2), + resource.TestCheckResourceAttr(resourceName, "description", "description 2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app3), + testAccCheckAWSAmplifyAppRecreated(&app2, &app3), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_EnvironmentVariables(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigEnvironmentVariables(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigEnvironmentVariablesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR2", "2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_IamServiceRole(t *testing.T) { + var app1, app2, app3 amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + iamRole1ResourceName := "aws_iam_role.test1" + iamRole2ResourceName := "aws_iam_role.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigIAMServiceRoleArn(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app1), + resource.TestCheckResourceAttrPair(resourceName, "iam_service_role_arn", iamRole1ResourceName, "arn")), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigIAMServiceRoleArnUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app2), + testAccCheckAWSAmplifyAppNotRecreated(&app1, &app2), + resource.TestCheckResourceAttrPair(resourceName, "iam_service_role_arn", iamRole2ResourceName, "arn"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app3), + testAccCheckAWSAmplifyAppRecreated(&app2, &app3), + resource.TestCheckResourceAttr(resourceName, "iam_service_role_arn", ""), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Name(t *testing.T) { + var app amplify.App + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "name", rName1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigName(rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "name", rName2), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Repository(t *testing.T) { + key := "AMPLIFY_GITHUB_ACCESS_TOKEN" + accessToken := os.Getenv(key) + if accessToken == "" { + t.Skipf("Environment variable %s is not set", key) + } + + key = "AMPLIFY_GITHUB_REPOSITORY" + repository := os.Getenv(key) + if repository == "" { + t.Skipf("Environment variable %s is not set", key) + } + + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigRepository(rName, repository, accessToken), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "access_token", accessToken), + resource.TestCheckResourceAttr(resourceName, "repository", repository), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // access_token is ignored because AWS does not store access_token and oauth_token + // See https://docs.aws.amazon.com/sdk-for-go/api/service/amplify/#CreateAppInput + ImportStateVerifyIgnore: []string{"access_token"}, + }, + }, + }) +} + +func testAccCheckAWSAmplifyAppExists(n string, v *amplify.App) 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 Amplify App ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + output, err := finder.AppByID(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckAWSAmplifyAppDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_app" { + continue + } + + _, err := finder.AppByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify App %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccPreCheckAWSAmplify(t *testing.T) { + if testAccGetPartition() == "aws-us-gov" { + t.Skip("AWS Amplify is not supported in GovCloud partition") + } +} + +func testAccCheckAWSAmplifyAppNotRecreated(before, after *amplify.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.AppId), aws.StringValue(after.AppId); before != after { + return fmt.Errorf("Amplify App (%s/%s) recreated", before, after) + } + + return nil + } +} + +func testAccCheckAWSAmplifyAppRecreated(before, after *amplify.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.AppId), aws.StringValue(after.AppId); before == after { + return fmt.Errorf("Amplify App (%s) not recreated", before) + } + + return nil + } +} + +func testAccAWSAmplifyAppConfigName(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} +`, rName) +} + +func testAccAWSAmplifyAppConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSAmplifyAppConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigNoAutoBranchCreationConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "*", + "*/**", + ] +} +`, rName) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfig(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "feature/*", + ] + + auto_branch_creation_config { + build_spec = "version: 0.1" + framework = "React" + stage = "DEVELOPMENT" + + enable_basic_auth = true + basic_auth_credentials = %[2]q + + enable_auto_build = true + enable_pull_request_preview = true + pull_request_environment_name = "test1" + + environment_variables = { + ENVVAR1 = "1" + } + } +} + +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfigUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "feature/*", + ] + + auto_branch_creation_config { + build_spec = "version: 0.2" + framework = "React" + stage = "EXPERIMENTAL" + + enable_basic_auth = false + + enable_auto_build = false + enable_pull_request_preview = false + + pull_request_environment_name = "test2" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + basic_auth_credentials = %[2]q + enable_basic_auth = true +} +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyAppConfigBuildSpec(rName, buildSpec string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + build_spec = %[2]q +} +`, rName, buildSpec) +} + +func testAccAWSAmplifyAppConfigCustomRules(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + custom_rule { + source = "/<*>" + status = "404" + target = "/index.html" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigCustomRulesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + custom_rule { + condition = "" + source = "/documents" + status = "302" + target = "/documents/us" + } + + custom_rule { + source = "/<*>" + status = "200" + target = "/index.html" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigDescription(rName, description string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + description = %[2]q +} +`, rName, description) +} + +func testAccAWSAmplifyAppConfigEnvironmentVariables(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + environment_variables = { + ENVVAR1 = "1" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigEnvironmentVariablesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + environment_variables = { + ENVVAR1 = "2", + ENVVAR2 = "2" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigIAMServiceRoleBase(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_role" "test1" { + name = "%[1]s-1" + + assume_role_policy = < 0 { + input.EnvironmentVariables = expandStringMap(v.(map[string]interface{})) + } + + if v, ok := d.GetOk("framework"); ok { + input.Framework = aws.String(v.(string)) + } + + if v, ok := d.GetOk("pull_request_environment_name"); ok { + input.PullRequestEnvironmentName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("stage"); ok { + input.Stage = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ttl"); ok { + input.Ttl = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AmplifyTags() + } + + log.Printf("[DEBUG] Creating Amplify Branch: %s", input) + _, err := conn.CreateBranch(input) + + if err != nil { + return fmt.Errorf("error creating Amplify Branch (%s): %w", id, err) + } + + d.SetId(id) + + return resourceAwsAmplifyBranchRead(d, meta) +} + +func resourceAwsAmplifyBranchRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + branch, err := finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify Branch (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify Branch (%s): %w", d.Id(), err) + } + + d.Set("app_id", appID) + d.Set("arn", branch.BranchArn) + d.Set("associated_resources", aws.StringValueSlice(branch.AssociatedResources)) + d.Set("backend_environment_arn", branch.BackendEnvironmentArn) + d.Set("basic_auth_credentials", branch.BasicAuthCredentials) + d.Set("branch_name", branch.BranchName) + d.Set("custom_domains", aws.StringValueSlice(branch.CustomDomains)) + d.Set("description", branch.Description) + d.Set("destination_branch", branch.DestinationBranch) + d.Set("display_name", branch.DisplayName) + d.Set("enable_auto_build", branch.EnableAutoBuild) + d.Set("enable_basic_auth", branch.EnableBasicAuth) + d.Set("enable_notification", branch.EnableNotification) + d.Set("enable_performance_mode", branch.EnablePerformanceMode) + d.Set("enable_pull_request_preview", branch.EnablePullRequestPreview) + d.Set("environment_variables", aws.StringValueMap(branch.EnvironmentVariables)) + d.Set("framework", branch.Framework) + d.Set("pull_request_environment_name", branch.PullRequestEnvironmentName) + d.Set("source_branch", branch.SourceBranch) + d.Set("stage", branch.Stage) + d.Set("ttl", branch.Ttl) + + tags := keyvaluetags.AmplifyKeyValueTags(branch.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceAwsAmplifyBranchUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + if d.HasChangesExcept("tags", "tags_all") { + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + input := &lify.UpdateBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + } + + if d.HasChange("backend_environment_arn") { + input.BackendEnvironmentArn = aws.String(d.Get("backend_environment_arn").(string)) + } + + if d.HasChange("basic_auth_credentials") { + input.BasicAuthCredentials = aws.String(d.Get("basic_auth_credentials").(string)) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChange("enable_auto_build") { + input.EnableAutoBuild = aws.Bool(d.Get("enable_auto_build").(bool)) + } + + if d.HasChange("enable_basic_auth") { + input.EnableBasicAuth = aws.Bool(d.Get("enable_basic_auth").(bool)) + } + + if d.HasChange("enable_notification") { + input.EnableNotification = aws.Bool(d.Get("enable_notification").(bool)) + } + + if d.HasChange("enable_performance_mode") { + input.EnablePullRequestPreview = aws.Bool(d.Get("enable_performance_mode").(bool)) + } + + if d.HasChange("enable_pull_request_preview") { + input.EnablePullRequestPreview = aws.Bool(d.Get("enable_pull_request_preview").(bool)) + } + + if d.HasChange("environment_variables") { + if v := d.Get("environment_variables").(map[string]interface{}); len(v) > 0 { + input.EnvironmentVariables = expandStringMap(v) + } else { + input.EnvironmentVariables = aws.StringMap(map[string]string{"": ""}) + } + } + + if d.HasChange("framework") { + input.Framework = aws.String(d.Get("framework").(string)) + } + + if d.HasChange("pull_request_environment_name") { + input.PullRequestEnvironmentName = aws.String(d.Get("pull_request_environment_name").(string)) + } + + if d.HasChange("stage") { + input.Stage = aws.String(d.Get("stage").(string)) + } + + if d.HasChange("ttl") { + input.Ttl = aws.String(d.Get("ttl").(string)) + } + + _, err = conn.UpdateBranch(input) + + if err != nil { + return fmt.Errorf("error updating Amplify Branch (%s): %w", d.Id(), err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.AmplifyUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + + return resourceAwsAmplifyBranchRead(d, meta) +} + +func resourceAwsAmplifyBranchDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + log.Printf("[DEBUG] Deleting Amplify Branch: %s", d.Id()) + _, err = conn.DeleteBranch(&lify.DeleteBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify Branch (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_amplify_branch_test.go b/aws/resource_aws_amplify_branch_test.go new file mode 100644 index 000000000000..3ba7103ddfde --- /dev/null +++ b/aws/resource_aws_amplify_branch_test.go @@ -0,0 +1,510 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/amplify" + "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" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccAWSAmplifyBranch_basic(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/branches/.+`)), + resource.TestCheckResourceAttr(resourceName, "associated_resources.#", "0"), + resource.TestCheckResourceAttr(resourceName, "backend_environment_arn", ""), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "branch_name", rName), + resource.TestCheckResourceAttr(resourceName, "custom_domains.#", "0"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "destination_branch", ""), + resource.TestCheckResourceAttr(resourceName, "display_name", rName), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "framework", ""), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", ""), + resource.TestCheckResourceAttr(resourceName, "source_branch", ""), + resource.TestCheckResourceAttr(resourceName, "stage", "NONE"), + resource.TestCheckResourceAttr(resourceName, "ttl", "5"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyBranch_disappears(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyBranch(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyBranch_Tags(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_BasicAuthCredentials(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + credentials1 := base64.StdEncoding.EncodeToString([]byte("username1:password1")) + credentials2 := base64.StdEncoding.EncodeToString([]byte("username2:password2")) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, credentials1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials1), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, credentials2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials2), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_EnvironmentVariables(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigEnvironmentVariables(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigEnvironmentVariablesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR2", "2"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_OptionalArguments(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + environmentName := acctest.RandStringFromCharSet(9, acctest.CharSetAlpha) + resourceName := "aws_amplify_branch.test" + backendEnvironment1ResourceName := "aws_amplify_backend_environment.test1" + backendEnvironment2ResourceName := "aws_amplify_backend_environment.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigOptionalArguments(rName, environmentName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttrPair(resourceName, "backend_environment_arn", backendEnvironment1ResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription1"), + resource.TestCheckResourceAttr(resourceName, "display_name", "testdisplayname1"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "framework", "React"), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", "testpr1"), + resource.TestCheckResourceAttr(resourceName, "stage", "DEVELOPMENT"), + resource.TestCheckResourceAttr(resourceName, "ttl", "10"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigOptionalArgumentsUpdated(rName, environmentName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttrPair(resourceName, "backend_environment_arn", backendEnvironment2ResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription2"), + resource.TestCheckResourceAttr(resourceName, "display_name", "testdisplayname2"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "true"), + resource.TestCheckResourceAttr(resourceName, "framework", "Angular"), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", "testpr2"), + resource.TestCheckResourceAttr(resourceName, "stage", "EXPERIMENTAL"), + resource.TestCheckResourceAttr(resourceName, "ttl", "15"), + ), + }, + }, + }) +} + +func testAccCheckAWSAmplifyBranchExists(resourceName string, v *amplify.Branch) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Amplify Branch ID is set") + } + + appID, branchName, err := tfamplify.BranchParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + branch, err := finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if err != nil { + return err + } + + *v = *branch + + return nil + } +} + +func testAccCheckAWSAmplifyBranchDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_branch" { + continue + } + + appID, branchName, err := tfamplify.BranchParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify Branch %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSAmplifyBranchConfigName(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSAmplifyBranchConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + basic_auth_credentials = %[2]q + enable_basic_auth = true +} +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyBranchConfigEnvironmentVariables(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + environment_variables = { + ENVVAR1 = "1" + } +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigEnvironmentVariablesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + environment_variables = { + ENVVAR1 = "2", + ENVVAR2 = "2" + } +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigOptionalArguments(rName, environmentName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_backend_environment" "test1" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sa" +} + +resource "aws_amplify_backend_environment" "test2" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sb" +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + backend_environment_arn = aws_amplify_backend_environment.test1.arn + description = "testdescription1" + display_name = "testdisplayname1" + enable_auto_build = false + enable_notification = true + enable_performance_mode = true + enable_pull_request_preview = false + framework = "React" + pull_request_environment_name = "testpr1" + stage = "DEVELOPMENT" + ttl = "10" +} +`, rName, environmentName) +} + +func testAccAWSAmplifyBranchConfigOptionalArgumentsUpdated(rName, environmentName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_backend_environment" "test1" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sa" +} + +resource "aws_amplify_backend_environment" "test2" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sb" +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + backend_environment_arn = aws_amplify_backend_environment.test2.arn + description = "testdescription2" + display_name = "testdisplayname2" + enable_auto_build = true + enable_notification = false + enable_performance_mode = true + enable_pull_request_preview = true + framework = "Angular" + pull_request_environment_name = "testpr2" + stage = "EXPERIMENTAL" + ttl = "15" +} +`, rName, environmentName) +} diff --git a/aws/resource_aws_amplify_domain_association.go b/aws/resource_aws_amplify_domain_association.go new file mode 100644 index 000000000000..f1d7f2a86c65 --- /dev/null +++ b/aws/resource_aws_amplify_domain_association.go @@ -0,0 +1,305 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAmplifyDomainAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyDomainAssociationCreate, + Read: resourceAwsAmplifyDomainAssociationRead, + Update: resourceAwsAmplifyDomainAssociationUpdate, + Delete: resourceAwsAmplifyDomainAssociationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "app_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "certificate_verification_dns_record": { + Type: schema.TypeString, + Computed: true, + }, + + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "sub_domain": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "dns_record": { + Type: schema.TypeString, + Computed: true, + }, + "prefix": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(0, 255), + }, + "verified": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + + "wait_for_verification": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceAwsAmplifyDomainAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID := d.Get("app_id").(string) + domainName := d.Get("domain_name").(string) + id := tfamplify.DomainAssociationCreateResourceID(appID, domainName) + + input := &lify.CreateDomainAssociationInput{ + AppId: aws.String(appID), + DomainName: aws.String(domainName), + SubDomainSettings: expandAmplifySubDomainSettings(d.Get("sub_domain").(*schema.Set).List()), + } + + log.Printf("[DEBUG] Creating Amplify Domain Association: %s", input) + _, err := conn.CreateDomainAssociation(input) + + if err != nil { + return fmt.Errorf("error creating Amplify Domain Association (%s): %w", id, err) + } + + d.SetId(id) + + if _, err := waiter.DomainAssociationCreated(conn, appID, domainName); err != nil { + return fmt.Errorf("error waiting for Amplify Domain Association (%s) to create: %w", d.Id(), err) + } + + if d.Get("wait_for_verification").(bool) { + if _, err := waiter.DomainAssociationVerified(conn, appID, domainName); err != nil { + return fmt.Errorf("error waiting for Amplify Domain Association (%s) to verify: %w", d.Id(), err) + } + } + + return resourceAwsAmplifyDomainAssociationRead(d, meta) +} + +func resourceAwsAmplifyDomainAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID, domainName, err := tfamplify.DomainAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Domain Association ID: %w", err) + } + + domainAssociation, err := finder.DomainAssociationByAppIDAndDomainName(conn, appID, domainName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify Domain Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify Domain Association (%s): %w", d.Id(), err) + } + + d.Set("app_id", appID) + d.Set("arn", domainAssociation.DomainAssociationArn) + d.Set("certificate_verification_dns_record", domainAssociation.CertificateVerificationDNSRecord) + d.Set("domain_name", domainAssociation.DomainName) + if err := d.Set("sub_domain", flattenAmplifySubDomains(domainAssociation.SubDomains)); err != nil { + return fmt.Errorf("error setting sub_domain: %w", err) + } + + return nil +} + +func resourceAwsAmplifyDomainAssociationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID, domainName, err := tfamplify.DomainAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Domain Association ID: %w", err) + } + + if d.HasChange("sub_domain") { + input := &lify.UpdateDomainAssociationInput{ + AppId: aws.String(appID), + DomainName: aws.String(domainName), + SubDomainSettings: expandAmplifySubDomainSettings(d.Get("sub_domain").(*schema.Set).List()), + } + + log.Printf("[DEBUG] Creating Amplify Domain Association: %s", input) + _, err := conn.UpdateDomainAssociation(input) + + if err != nil { + return fmt.Errorf("error updating Amplify Domain Association (%s): %w", d.Id(), err) + } + } + + if d.Get("wait_for_verification").(bool) { + if _, err := waiter.DomainAssociationVerified(conn, appID, domainName); err != nil { + return fmt.Errorf("error waiting for Amplify Domain Association (%s) to verify: %w", d.Id(), err) + } + } + + return resourceAwsAmplifyDomainAssociationRead(d, meta) +} + +func resourceAwsAmplifyDomainAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID, domainName, err := tfamplify.DomainAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Domain Association ID: %w", err) + } + + log.Printf("[DEBUG] Deleting Amplify Domain Association: %s", d.Id()) + _, err = conn.DeleteDomainAssociation(&lify.DeleteDomainAssociationInput{ + AppId: aws.String(appID), + DomainName: aws.String(domainName), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify Domain Association (%s): %w", d.Id(), err) + } + + return nil +} + +func expandAmplifySubDomainSetting(tfMap map[string]interface{}) *amplify.SubDomainSetting { + if tfMap == nil { + return nil + } + + apiObject := &lify.SubDomainSetting{} + + if v, ok := tfMap["branch_name"].(string); ok && v != "" { + apiObject.BranchName = aws.String(v) + } + + // Empty prefix is allowed. + if v, ok := tfMap["prefix"].(string); ok { + apiObject.Prefix = aws.String(v) + } + + return apiObject +} + +func expandAmplifySubDomainSettings(tfList []interface{}) []*amplify.SubDomainSetting { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*amplify.SubDomainSetting + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandAmplifySubDomainSetting(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenAmplifySubDomain(apiObject *amplify.SubDomain) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.DnsRecord; v != nil { + tfMap["dns_record"] = aws.StringValue(v) + } + + if v := apiObject.SubDomainSetting; v != nil { + apiObject := v + + if v := apiObject.BranchName; v != nil { + tfMap["branch_name"] = aws.StringValue(v) + } + + if v := apiObject.Prefix; v != nil { + tfMap["prefix"] = aws.StringValue(v) + } + } + + if v := apiObject.Verified; v != nil { + tfMap["verified"] = aws.BoolValue(v) + } + + return tfMap +} + +func flattenAmplifySubDomains(apiObjects []*amplify.SubDomain) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenAmplifySubDomain(apiObject)) + } + + return tfList +} diff --git a/aws/resource_aws_amplify_domain_association_test.go b/aws/resource_aws_amplify_domain_association_test.go new file mode 100644 index 000000000000..2ffe7d1d0a23 --- /dev/null +++ b/aws/resource_aws_amplify_domain_association_test.go @@ -0,0 +1,266 @@ +package aws + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/amplify" + "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" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccAWSAmplifyDomainAssociation_basic(t *testing.T) { + key := "AMPLIFY_DOMAIN_NAME" + domainName := os.Getenv(key) + if domainName == "" { + t.Skipf("Environment variable %s is not set", key) + } + + var domain amplify.DomainAssociation + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_domain_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyDomainAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyDomainAssociationConfig(rName, domainName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyDomainAssociationExists(resourceName, &domain), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/domains/.+`)), + resource.TestCheckResourceAttr(resourceName, "domain_name", domainName), + resource.TestCheckResourceAttr(resourceName, "sub_domain.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "sub_domain.*", map[string]string{ + "branch_name": rName, + "prefix": "", + }), + resource.TestCheckResourceAttr(resourceName, "wait_for_verification", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"wait_for_verification"}, + }, + }, + }) +} + +func testAccAWSAmplifyDomainAssociation_disappears(t *testing.T) { + key := "AMPLIFY_DOMAIN_NAME" + domainName := os.Getenv(key) + if domainName == "" { + t.Skipf("Environment variable %s is not set", key) + } + + var domain amplify.DomainAssociation + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_domain_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyDomainAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyDomainAssociationConfig(rName, domainName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyDomainAssociationExists(resourceName, &domain), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyDomainAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyDomainAssociation_update(t *testing.T) { + key := "AMPLIFY_DOMAIN_NAME" + domainName := os.Getenv(key) + if domainName == "" { + t.Skipf("Environment variable %s is not set", key) + } + + var domain amplify.DomainAssociation + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_domain_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyDomainAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyDomainAssociationConfig(rName, domainName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyDomainAssociationExists(resourceName, &domain), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/domains/.+`)), + resource.TestCheckResourceAttr(resourceName, "domain_name", domainName), + resource.TestCheckResourceAttr(resourceName, "sub_domain.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "sub_domain.*", map[string]string{ + "branch_name": rName, + "prefix": "", + }), + resource.TestCheckResourceAttr(resourceName, "wait_for_verification", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"wait_for_verification"}, + }, + { + Config: testAccAWSAmplifyDomainAssociationConfigUpdated(rName, domainName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyDomainAssociationExists(resourceName, &domain), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/domains/.+`)), + resource.TestCheckResourceAttr(resourceName, "domain_name", domainName), + resource.TestCheckResourceAttr(resourceName, "sub_domain.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "sub_domain.*", map[string]string{ + "branch_name": rName, + "prefix": "", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "sub_domain.*", map[string]string{ + "branch_name": fmt.Sprintf("%s-2", rName), + "prefix": "www", + }), + resource.TestCheckResourceAttr(resourceName, "wait_for_verification", "true"), + ), + }, + }, + }) +} + +func testAccCheckAWSAmplifyDomainAssociationExists(resourceName string, v *amplify.DomainAssociation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Amplify Domain Association ID is set") + } + + appID, domainName, err := tfamplify.DomainAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + domainAssociation, err := finder.DomainAssociationByAppIDAndDomainName(conn, appID, domainName) + + if err != nil { + return err + } + + *v = *domainAssociation + + return nil + } +} + +func testAccCheckAWSAmplifyDomainAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_domain_association" { + continue + } + + appID, domainName, err := tfamplify.DomainAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.DomainAssociationByAppIDAndDomainName(conn, appID, domainName) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify Domain Association %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSAmplifyDomainAssociationConfig(rName, domainName string, waitForVerification bool) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} + +resource "aws_amplify_domain_association" "test" { + app_id = aws_amplify_app.test.id + domain_name = %[2]q + + sub_domain { + branch_name = aws_amplify_branch.test.branch_name + prefix = "" + } + + wait_for_verification = %[3]t +} +`, rName, domainName, waitForVerification) +} + +func testAccAWSAmplifyDomainAssociationConfigUpdated(rName, domainName string, waitForVerification bool) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} + +resource "aws_amplify_branch" "test2" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-2" +} + +resource "aws_amplify_domain_association" "test" { + app_id = aws_amplify_app.test.id + domain_name = %[2]q + + sub_domain { + branch_name = aws_amplify_branch.test.branch_name + prefix = "" + } + + sub_domain { + branch_name = aws_amplify_branch.test2.branch_name + prefix = "www" + } + + wait_for_verification = %[3]t +} +`, rName, domainName, waitForVerification) +} diff --git a/aws/resource_aws_amplify_test.go b/aws/resource_aws_amplify_test.go new file mode 100644 index 000000000000..d4b48560d042 --- /dev/null +++ b/aws/resource_aws_amplify_test.go @@ -0,0 +1,63 @@ +package aws + +import ( + "testing" + "time" +) + +// Serialize to limit API rate-limit exceeded errors. +func TestAccAWSAmplify_serial(t *testing.T) { + testCases := map[string]map[string]func(t *testing.T){ + "App": { + "basic": testAccAWSAmplifyApp_basic, + "disappears": testAccAWSAmplifyApp_disappears, + "Tags": testAccAWSAmplifyApp_Tags, + "AutoBranchCreationConfig": testAccAWSAmplifyApp_AutoBranchCreationConfig, + "BasicAuthCredentials": testAccAWSAmplifyApp_BasicAuthCredentials, + "BuildSpec": testAccAWSAmplifyApp_BuildSpec, + "CustomRules": testAccAWSAmplifyApp_CustomRules, + "Description": testAccAWSAmplifyApp_Description, + "EnvironmentVariables": testAccAWSAmplifyApp_EnvironmentVariables, + "IamServiceRole": testAccAWSAmplifyApp_IamServiceRole, + "Name": testAccAWSAmplifyApp_Name, + "Repository": testAccAWSAmplifyApp_Repository, + }, + "BackendEnvironment": { + "basic": testAccAWSAmplifyBackendEnvironment_basic, + "disappears": testAccAWSAmplifyBackendEnvironment_disappears, + "DeploymentArtifacts_StackName": testAccAWSAmplifyBackendEnvironment_DeploymentArtifacts_StackName, + }, + "Branch": { + "basic": testAccAWSAmplifyBranch_basic, + "disappears": testAccAWSAmplifyBranch_disappears, + "Tags": testAccAWSAmplifyBranch_Tags, + "BasicAuthCredentials": testAccAWSAmplifyBranch_BasicAuthCredentials, + "EnvironmentVariables": testAccAWSAmplifyBranch_EnvironmentVariables, + "OptionalArguments": testAccAWSAmplifyBranch_OptionalArguments, + }, + "DomainAssociation": { + "basic": testAccAWSAmplifyDomainAssociation_basic, + "disappears": testAccAWSAmplifyDomainAssociation_disappears, + "update": testAccAWSAmplifyDomainAssociation_update, + }, + "Webhook": { + "basic": testAccAWSAmplifyWebhook_basic, + "disappears": testAccAWSAmplifyWebhook_disappears, + "update": testAccAWSAmplifyWebhook_update, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + // Explicitly sleep between tests. + time.Sleep(5 * time.Second) + }) + } + }) + } +} diff --git a/aws/resource_aws_amplify_webhook.go b/aws/resource_aws_amplify_webhook.go new file mode 100644 index 000000000000..fe66597b64d4 --- /dev/null +++ b/aws/resource_aws_amplify_webhook.go @@ -0,0 +1,164 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAmplifyWebhook() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyWebhookCreate, + Read: resourceAwsAmplifyWebhookRead, + Update: resourceAwsAmplifyWebhookUpdate, + Delete: resourceAwsAmplifyWebhookDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "app_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "branch_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z/_.-]{1,255}$`), "should be not be more than 255 letters, numbers, and the symbols /_.-"), + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsAmplifyWebhookCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + input := &lify.CreateWebhookInput{ + AppId: aws.String(d.Get("app_id").(string)), + BranchName: aws.String(d.Get("branch_name").(string)), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Amplify Webhook: %s", input) + output, err := conn.CreateWebhook(input) + + if err != nil { + return fmt.Errorf("error creating Amplify Webhook: %w", err) + } + + d.SetId(aws.StringValue(output.Webhook.WebhookId)) + + return resourceAwsAmplifyWebhookRead(d, meta) +} + +func resourceAwsAmplifyWebhookRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + webhook, err := finder.WebhookByID(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify Webhook (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify Webhook (%s): %w", d.Id(), err) + } + + webhookArn := aws.StringValue(webhook.WebhookArn) + arn, err := arn.Parse(webhookArn) + + if err != nil { + return fmt.Errorf("error parsing %q: %w", webhookArn, err) + } + + // arn:${Partition}:amplify:${Region}:${Account}:apps/${AppId}/webhooks/${WebhookId} + parts := strings.Split(arn.Resource, "/") + + if len(parts) != 4 { + return fmt.Errorf("unexpected format for ARN resource (%s)", arn.Resource) + } + + d.Set("app_id", parts[1]) + d.Set("arn", webhookArn) + d.Set("branch_name", webhook.BranchName) + d.Set("description", webhook.Description) + d.Set("url", webhook.WebhookUrl) + + return nil +} + +func resourceAwsAmplifyWebhookUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + input := &lify.UpdateWebhookInput{ + WebhookId: aws.String(d.Id()), + } + + if d.HasChange("branch_name") { + input.BranchName = aws.String(d.Get("branch_name").(string)) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + log.Printf("[DEBUG] Updating Amplify Webhook: %s", input) + _, err := conn.UpdateWebhook(input) + + if err != nil { + return fmt.Errorf("error updating Amplify Webhook (%s): %w", d.Id(), err) + } + + return resourceAwsAmplifyWebhookRead(d, meta) +} + +func resourceAwsAmplifyWebhookDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + log.Printf("[DEBUG] Deleting Amplify Webhook: %s", d.Id()) + _, err := conn.DeleteWebhook(&lify.DeleteWebhookInput{ + WebhookId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify Webhook (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_amplify_webhook_test.go b/aws/resource_aws_amplify_webhook_test.go new file mode 100644 index 000000000000..18beddc6fb4a --- /dev/null +++ b/aws/resource_aws_amplify_webhook_test.go @@ -0,0 +1,218 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/amplify" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccAWSAmplifyWebhook_basic(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/webhooks/.+`)), + resource.TestCheckResourceAttr(resourceName, "branch_name", rName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestMatchResourceAttr(resourceName, "url", regexp.MustCompile(fmt.Sprintf(`^https://webhooks.amplify.%s.%s/.+$`, testAccGetRegion(), testAccGetPartitionDNSSuffix()))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyWebhook_disappears(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyWebhook(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyWebhook_update(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfigDescription(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + resource.TestCheckResourceAttr(resourceName, "branch_name", fmt.Sprintf("%s-1", rName)), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyWebhookConfigDescriptionUpdated(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + resource.TestCheckResourceAttr(resourceName, "branch_name", fmt.Sprintf("%s-2", rName)), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription2"), + ), + }, + }, + }) +} + +func testAccCheckAWSAmplifyWebhookExists(resourceName string, v *amplify.Webhook) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Amplify Webhook ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + webhook, err := finder.WebhookByID(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *webhook + + return nil + } +} + +func testAccCheckAWSAmplifyWebhookDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_webhook" { + continue + } + + _, err := finder.WebhookByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify Webhook %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSAmplifyWebhookConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test.branch_name +} +`, rName) +} + +func testAccAWSAmplifyWebhookConfigDescription(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test1" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-1" +} + +resource "aws_amplify_branch" "test2" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-2" +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test1.branch_name + description = "testdescription1" +} +`, rName) +} + +func testAccAWSAmplifyWebhookConfigDescriptionUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test1" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-1" +} + +resource "aws_amplify_branch" "test2" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-2" +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test2.branch_name + description = "testdescription2" +} +`, rName) +} diff --git a/aws/resource_aws_api_gateway_account.go b/aws/resource_aws_api_gateway_account.go index d37b831bf938..ffa32fb3a460 100644 --- a/aws/resource_aws_api_gateway_account.go +++ b/aws/resource_aws_api_gateway_account.go @@ -3,12 +3,12 @@ package aws import ( "fmt" "log" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) func resourceAwsApiGatewayAccount() *schema.Resource { @@ -96,7 +96,7 @@ func resourceAwsApiGatewayAccountUpdate(d *schema.ResourceData, meta interface{} otherErrMsg := "API Gateway could not successfully write to CloudWatch Logs using the ARN specified" var out *apigateway.Account var err error - err = resource.Retry(2*time.Minute, func() *resource.RetryError { + err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { out, err = conn.UpdateAccount(&input) if err != nil { diff --git a/aws/resource_aws_api_gateway_account_test.go b/aws/resource_aws_api_gateway_account_test.go index 2c47a9b7c066..a44e1ef2fa4f 100644 --- a/aws/resource_aws_api_gateway_account_test.go +++ b/aws/resource_aws_api_gateway_account_test.go @@ -24,6 +24,7 @@ func TestAccAWSAPIGatewayAccount_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAccountDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_api_key.go b/aws/resource_aws_api_gateway_api_key.go index 2f7108571157..d1a4bed30a5d 100644 --- a/aws/resource_aws_api_gateway_api_key.go +++ b/aws/resource_aws_api_gateway_api_key.go @@ -64,13 +64,18 @@ func resourceAwsApiGatewayApiKey() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayApiKeyCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) log.Printf("[DEBUG] Creating API Gateway API Key") apiKey, err := conn.CreateApiKey(&apigateway.CreateApiKeyInput{ @@ -78,7 +83,7 @@ func resourceAwsApiGatewayApiKeyCreate(d *schema.ResourceData, meta interface{}) Description: aws.String(d.Get("description").(string)), Enabled: aws.Bool(d.Get("enabled").(bool)), Value: aws.String(d.Get("value").(string)), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().ApigatewayTags(), + Tags: tags.IgnoreAws().ApigatewayTags(), }) if err != nil { return fmt.Errorf("Error creating API Gateway API Key: %s", err) @@ -91,6 +96,7 @@ func resourceAwsApiGatewayApiKeyCreate(d *schema.ResourceData, meta interface{}) func resourceAwsApiGatewayApiKeyRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[DEBUG] Reading API Gateway API Key: %s", d.Id()) @@ -109,8 +115,15 @@ func resourceAwsApiGatewayApiKeyRead(d *schema.ResourceData, meta interface{}) e return err } - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(apiKey.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(apiKey.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } arn := arn.ARN{ @@ -167,8 +180,8 @@ func resourceAwsApiGatewayApiKeyUpdate(d *schema.ResourceData, meta interface{}) log.Printf("[DEBUG] Updating API Gateway API Key: %s", d.Id()) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } diff --git a/aws/resource_aws_api_gateway_api_key_test.go b/aws/resource_aws_api_gateway_api_key_test.go index 73e8139a771f..d97ec60f7d47 100644 --- a/aws/resource_aws_api_gateway_api_key_test.go +++ b/aws/resource_aws_api_gateway_api_key_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayApiKey_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -52,6 +53,7 @@ func TestAccAWSAPIGatewayApiKey_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -96,6 +98,7 @@ func TestAccAWSAPIGatewayApiKey_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -130,6 +133,7 @@ func TestAccAWSAPIGatewayApiKey_Enabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -164,6 +168,7 @@ func TestAccAWSAPIGatewayApiKey_Value(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -190,6 +195,7 @@ func TestAccAWSAPIGatewayApiKey_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayApiKeyDestroy, Steps: []resource.TestStep{ @@ -269,7 +275,7 @@ func testAccCheckAWSAPIGatewayApiKeyDestroy(s *terraform.State) error { func testAccCheckAWSAPIGatewayApiKeyNotRecreated(i, j *apigateway.ApiKey) resource.TestCheckFunc { return func(s *terraform.State) error { - if aws.TimeValue(i.CreatedDate) != aws.TimeValue(j.CreatedDate) { + if !aws.TimeValue(i.CreatedDate).Equal(aws.TimeValue(j.CreatedDate)) { return fmt.Errorf("API Gateway API Key recreated") } diff --git a/aws/resource_aws_api_gateway_authorizer_test.go b/aws/resource_aws_api_gateway_authorizer_test.go index aaf8293087f1..9d11a29236da 100644 --- a/aws/resource_aws_api_gateway_authorizer_test.go +++ b/aws/resource_aws_api_gateway_authorizer_test.go @@ -23,6 +23,7 @@ func TestAccAWSAPIGatewayAuthorizer_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -68,6 +69,7 @@ func TestAccAWSAPIGatewayAuthorizer_cognito(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -105,6 +107,7 @@ func TestAccAWSAPIGatewayAuthorizer_cognito_authorizerCredentials(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -135,6 +138,7 @@ func TestAccAWSAPIGatewayAuthorizer_switchAuthType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -181,6 +185,7 @@ func TestAccAWSAPIGatewayAuthorizer_switchAuthorizerTTL(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -227,6 +232,7 @@ func TestAccAWSAPIGatewayAuthorizer_authTypeValidation(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -253,6 +259,7 @@ func TestAccAWSAPIGatewayAuthorizer_zero_ttl(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ @@ -280,6 +287,7 @@ func TestAccAWSAPIGatewayAuthorizer_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayAuthorizerDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_base_path_mapping.go b/aws/resource_aws_api_gateway_base_path_mapping.go index 30e1385174c5..f4b0205ad3de 100644 --- a/aws/resource_aws_api_gateway_base_path_mapping.go +++ b/aws/resource_aws_api_gateway_base_path_mapping.go @@ -167,7 +167,7 @@ func resourceAwsApiGatewayBasePathMappingRead(d *schema.ResourceData, meta inter return fmt.Errorf("Error reading Gateway base path mapping: %s", err) } - mappingBasePath := *mapping.BasePath + mappingBasePath := aws.StringValue(mapping.BasePath) if mappingBasePath == emptyBasePathMappingValue { mappingBasePath = "" diff --git a/aws/resource_aws_api_gateway_base_path_mapping_test.go b/aws/resource_aws_api_gateway_base_path_mapping_test.go index 0686ef72da0b..3adfd1eebfa2 100644 --- a/aws/resource_aws_api_gateway_base_path_mapping_test.go +++ b/aws/resource_aws_api_gateway_base_path_mapping_test.go @@ -6,7 +6,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigateway" - "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" ) @@ -66,13 +65,14 @@ func TestDecodeApiGatewayBasePathMappingId(t *testing.T) { func TestAccAWSAPIGatewayBasePathMapping_basic(t *testing.T) { var conf apigateway.BasePathMapping - name := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + name := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, name) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayBasePathDestroy(name), Steps: []resource.TestStep{ @@ -95,13 +95,14 @@ func TestAccAWSAPIGatewayBasePathMapping_basic(t *testing.T) { func TestAccAWSAPIGatewayBasePathMapping_BasePath_Empty(t *testing.T) { var conf apigateway.BasePathMapping - name := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + name := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, name) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayBasePathDestroy(name), Steps: []resource.TestStep{ @@ -123,13 +124,14 @@ func TestAccAWSAPIGatewayBasePathMapping_BasePath_Empty(t *testing.T) { func TestAccAWSAPIGatewayBasePathMapping_updates(t *testing.T) { var confFirst, conf apigateway.BasePathMapping resourceName := "aws_api_gateway_base_path_mapping.test" - name := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + name := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, name) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayBasePathDestroy(name), Steps: []resource.TestStep{ @@ -172,7 +174,7 @@ func TestAccAWSAPIGatewayBasePathMapping_updates(t *testing.T) { func TestAccAWSAPIGatewayBasePathMapping_disappears(t *testing.T) { var conf apigateway.BasePathMapping - name := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + name := testAccRandomSubdomain() resourceName := "aws_api_gateway_base_path_mapping.test" key := tlsRsaPrivateKeyPem(2048) @@ -180,6 +182,7 @@ func TestAccAWSAPIGatewayBasePathMapping_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayBasePathDestroy(name), Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_client_certificate.go b/aws/resource_aws_api_gateway_client_certificate.go index 527fd80743f5..73d1e93c007f 100644 --- a/aws/resource_aws_api_gateway_client_certificate.go +++ b/aws/resource_aws_api_gateway_client_certificate.go @@ -42,20 +42,25 @@ func resourceAwsApiGatewayClientCertificate() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayClientCertificateCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := apigateway.GenerateClientCertificateInput{} if v, ok := d.GetOk("description"); ok { input.Description = aws.String(v.(string)) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ApigatewayTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().ApigatewayTags() } log.Printf("[DEBUG] Generating API Gateway Client Certificate: %s", input) out, err := conn.GenerateClientCertificate(&input) @@ -70,6 +75,7 @@ func resourceAwsApiGatewayClientCertificateCreate(d *schema.ResourceData, meta i func resourceAwsApiGatewayClientCertificateRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := apigateway.GetClientCertificateInput{ @@ -86,8 +92,15 @@ func resourceAwsApiGatewayClientCertificateRead(d *schema.ResourceData, meta int } log.Printf("[DEBUG] Received API Gateway Client Certificate: %s", out) - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(out.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(out.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } arn := arn.ARN{ @@ -129,8 +142,8 @@ func resourceAwsApiGatewayClientCertificateUpdate(d *schema.ResourceData, meta i return fmt.Errorf("Updating API Gateway Client Certificate failed: %s", err) } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } diff --git a/aws/resource_aws_api_gateway_client_certificate_test.go b/aws/resource_aws_api_gateway_client_certificate_test.go index d1a5c20c334f..875e5f904bb2 100644 --- a/aws/resource_aws_api_gateway_client_certificate_test.go +++ b/aws/resource_aws_api_gateway_client_certificate_test.go @@ -18,6 +18,7 @@ func TestAccAWSAPIGatewayClientCertificate_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayClientCertificateDestroy, Steps: []resource.TestStep{ @@ -52,6 +53,7 @@ func TestAccAWSAPIGatewayClientCertificate_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayClientCertificateDestroy, Steps: []resource.TestStep{ @@ -95,6 +97,7 @@ func TestAccAWSAPIGatewayClientCertificate_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayClientCertificateDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_deployment_test.go b/aws/resource_aws_api_gateway_deployment_test.go index 885cdfa28910..dfb0121c787b 100644 --- a/aws/resource_aws_api_gateway_deployment_test.go +++ b/aws/resource_aws_api_gateway_deployment_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayDeployment_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -50,6 +51,7 @@ func TestAccAWSAPIGatewayDeployment_disappears_RestApi(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -73,6 +75,7 @@ func TestAccAWSAPIGatewayDeployment_Triggers(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -128,6 +131,7 @@ func TestAccAWSAPIGatewayDeployment_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -156,6 +160,7 @@ func TestAccAWSAPIGatewayDeployment_StageDescription(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -178,6 +183,7 @@ func TestAccAWSAPIGatewayDeployment_StageName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -205,6 +211,7 @@ func TestAccAWSAPIGatewayDeployment_StageName_EmptyString(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -225,6 +232,7 @@ func TestAccAWSAPIGatewayDeployment_Variables(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDeploymentDestroy, Steps: []resource.TestStep{ @@ -331,7 +339,7 @@ func testAccCheckAWSAPIGatewayDeploymentDestroy(s *terraform.State) error { func testAccCheckAWSAPIGatewayDeploymentNotRecreated(i, j *apigateway.Deployment) resource.TestCheckFunc { return func(s *terraform.State) error { - if aws.TimeValue(i.CreatedDate) != aws.TimeValue(j.CreatedDate) { + if !aws.TimeValue(i.CreatedDate).Equal(aws.TimeValue(j.CreatedDate)) { return fmt.Errorf("API Gateway Deployment recreated") } @@ -341,7 +349,7 @@ func testAccCheckAWSAPIGatewayDeploymentNotRecreated(i, j *apigateway.Deployment func testAccCheckAWSAPIGatewayDeploymentRecreated(i, j *apigateway.Deployment) resource.TestCheckFunc { return func(s *terraform.State) error { - if aws.TimeValue(i.CreatedDate) == aws.TimeValue(j.CreatedDate) { + if aws.TimeValue(i.CreatedDate).Equal(aws.TimeValue(j.CreatedDate)) { return fmt.Errorf("API Gateway Deployment not recreated") } diff --git a/aws/resource_aws_api_gateway_documentation_part.go b/aws/resource_aws_api_gateway_documentation_part.go index a3b5df3c2c19..9131db188403 100644 --- a/aws/resource_aws_api_gateway_documentation_part.go +++ b/aws/resource_aws_api_gateway_documentation_part.go @@ -200,18 +200,25 @@ func flattenApiGatewayDocumentationPartLocation(l *apigateway.DocumentationPartL } m := make(map[string]interface{}) - m["type"] = *l.Type - if l.Method != nil { - m["method"] = *l.Method + + if v := l.Method; v != nil { + m["method"] = aws.StringValue(v) + } + + if v := l.Name; v != nil { + m["name"] = aws.StringValue(v) } - if l.Name != nil { - m["name"] = *l.Name + + if v := l.Path; v != nil { + m["path"] = aws.StringValue(v) } - if l.Path != nil { - m["path"] = *l.Path + + if v := l.StatusCode; v != nil { + m["status_code"] = aws.StringValue(v) } - if l.StatusCode != nil { - m["status_code"] = *l.StatusCode + + if v := l.Type; v != nil { + m["type"] = aws.StringValue(v) } return []interface{}{m} diff --git a/aws/resource_aws_api_gateway_documentation_part_test.go b/aws/resource_aws_api_gateway_documentation_part_test.go index 90ac8f6fb6d5..67d589355081 100644 --- a/aws/resource_aws_api_gateway_documentation_part_test.go +++ b/aws/resource_aws_api_gateway_documentation_part_test.go @@ -24,6 +24,7 @@ func TestAccAWSAPIGatewayDocumentationPart_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationPartDestroy, Steps: []resource.TestStep{ @@ -68,6 +69,7 @@ func TestAccAWSAPIGatewayDocumentationPart_method(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationPartDestroy, Steps: []resource.TestStep{ @@ -116,6 +118,7 @@ func TestAccAWSAPIGatewayDocumentationPart_responseHeader(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationPartDestroy, Steps: []resource.TestStep{ @@ -167,6 +170,7 @@ func TestAccAWSAPIGatewayDocumentationPart_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationPartDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_documentation_version_test.go b/aws/resource_aws_api_gateway_documentation_version_test.go index 9508ee861616..2c95d55395f5 100644 --- a/aws/resource_aws_api_gateway_documentation_version_test.go +++ b/aws/resource_aws_api_gateway_documentation_version_test.go @@ -22,6 +22,7 @@ func TestAccAWSAPIGatewayDocumentationVersion_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationVersionDestroy, Steps: []resource.TestStep{ @@ -56,6 +57,7 @@ func TestAccAWSAPIGatewayDocumentationVersion_allFields(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationVersionDestroy, Steps: []resource.TestStep{ @@ -97,6 +99,7 @@ func TestAccAWSAPIGatewayDocumentationVersion_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDocumentationVersionDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_domain_name.go b/aws/resource_aws_api_gateway_domain_name.go index e48563eaadcc..73e9914d3c4a 100644 --- a/aws/resource_aws_api_gateway_domain_name.go +++ b/aws/resource_aws_api_gateway_domain_name.go @@ -163,13 +163,18 @@ func resourceAwsApiGatewayDomainName() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayDomainNameCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) log.Printf("[DEBUG] Creating API Gateway Domain Name") params := &apigateway.CreateDomainNameInput{ @@ -213,8 +218,8 @@ func resourceAwsApiGatewayDomainNameCreate(d *schema.ResourceData, meta interfac params.SecurityPolicy = aws.String(v.(string)) } - if v, ok := d.GetOk("tags"); ok { - params.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ApigatewayTags() + if len(tags) > 0 { + params.Tags = tags.IgnoreAws().ApigatewayTags() } domainName, err := conn.CreateDomainName(params) @@ -229,6 +234,7 @@ func resourceAwsApiGatewayDomainNameCreate(d *schema.ResourceData, meta interfac func resourceAwsApiGatewayDomainNameRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[DEBUG] Reading API Gateway Domain Name %s", d.Id()) @@ -246,8 +252,15 @@ func resourceAwsApiGatewayDomainNameRead(d *schema.ResourceData, meta interface{ return err } - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(domainName.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(domainName.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } arn := arn.ARN{ @@ -366,8 +379,8 @@ func resourceAwsApiGatewayDomainNameUpdate(d *schema.ResourceData, meta interfac conn := meta.(*AWSClient).apigatewayconn log.Printf("[DEBUG] Updating API Gateway Domain Name %s", d.Id()) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } diff --git a/aws/resource_aws_api_gateway_domain_name_test.go b/aws/resource_aws_api_gateway_domain_name_test.go index 1a2b47407679..3e74b4925f29 100644 --- a/aws/resource_aws_api_gateway_domain_name_test.go +++ b/aws/resource_aws_api_gateway_domain_name_test.go @@ -24,6 +24,7 @@ func TestAccAWSAPIGatewayDomainName_CertificateArn(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckApigatewayEdgeDomainName(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), ProviderFactories: testAccProviderFactories, CheckDestroy: testAccCheckAWSAPIGatewayEdgeDomainNameDestroy, Steps: []resource.TestStep{ @@ -85,6 +86,7 @@ func TestAccAWSAPIGatewayDomainName_CertificateName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -113,13 +115,14 @@ func TestAccAWSAPIGatewayDomainName_CertificateName(t *testing.T) { func TestAccAWSAPIGatewayDomainName_RegionalCertificateArn(t *testing.T) { var domainName apigateway.DomainName resourceName := "aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + rName := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, rName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -154,15 +157,18 @@ func TestAccAWSAPIGatewayDomainName_RegionalCertificateName(t *testing.T) { var domainName apigateway.DomainName resourceName := "aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + domain := testAccRandomDomainName() + domainWildcard := fmt.Sprintf("*.%s", domain) + rName := fmt.Sprintf("%s.%s", acctest.RandString(8), domain) caKey := tlsRsaPrivateKeyPem(2048) caCertificate := tlsRsaX509SelfSignedCaCertificatePem(caKey) key := tlsRsaPrivateKeyPem(2048) - certificate := tlsRsaX509LocallySignedCertificatePem(caKey, caCertificate, key, "*.terraformtest.com") + certificate := tlsRsaX509LocallySignedCertificatePem(caKey, caCertificate, key, domainWildcard) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -188,13 +194,14 @@ func TestAccAWSAPIGatewayDomainName_RegionalCertificateName(t *testing.T) { func TestAccAWSAPIGatewayDomainName_SecurityPolicy(t *testing.T) { var domainName apigateway.DomainName resourceName := "aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + rName := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, rName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -217,13 +224,14 @@ func TestAccAWSAPIGatewayDomainName_SecurityPolicy(t *testing.T) { func TestAccAWSAPIGatewayDomainName_Tags(t *testing.T) { var domainName apigateway.DomainName resourceName := "aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + rName := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, rName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -264,13 +272,14 @@ func TestAccAWSAPIGatewayDomainName_Tags(t *testing.T) { func TestAccAWSAPIGatewayDomainName_disappears(t *testing.T) { var domainName apigateway.DomainName resourceName := "aws_api_gateway_domain_name.test" - rName := fmt.Sprintf("tf-acc-%s.terraformtest.com", acctest.RandString(8)) + rName := testAccRandomSubdomain() key := tlsRsaPrivateKeyPem(2048) certificate := tlsRsaX509SelfSignedCertificatePem(key, rName) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ @@ -298,6 +307,7 @@ func TestAccAWSAPIGatewayDomainName_MutualTlsAuthentication(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayDomainNameDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_gateway_response_test.go b/aws/resource_aws_api_gateway_gateway_response_test.go index 91f16f273bcf..5374d21e92ff 100644 --- a/aws/resource_aws_api_gateway_gateway_response_test.go +++ b/aws/resource_aws_api_gateway_gateway_response_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayGatewayResponse_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayGatewayResponseDestroy, Steps: []resource.TestStep{ @@ -62,6 +63,7 @@ func TestAccAWSAPIGatewayGatewayResponse_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayGatewayResponseDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_integration.go b/aws/resource_aws_api_gateway_integration.go index f2c3813375b4..a2bf85db98b4 100644 --- a/aws/resource_aws_api_gateway_integration.go +++ b/aws/resource_aws_api_gateway_integration.go @@ -174,95 +174,69 @@ func resourceAwsApiGatewayIntegrationCreate(d *schema.ResourceData, meta interfa log.Print("[DEBUG] Creating API Gateway Integration") - connectionType := aws.String(d.Get("connection_type").(string)) - var connectionId *string - if *connectionType == apigateway.ConnectionTypeVpcLink { - if _, ok := d.GetOk("connection_id"); !ok { - return fmt.Errorf("connection_id required when connection_type set to VPC_LINK") - } - connectionId = aws.String(d.Get("connection_id").(string)) + input := &apigateway.PutIntegrationInput{ + HttpMethod: aws.String(d.Get("http_method").(string)), + ResourceId: aws.String(d.Get("resource_id").(string)), + RestApiId: aws.String(d.Get("rest_api_id").(string)), + Type: aws.String(d.Get("type").(string)), } - var integrationHttpMethod *string - if v, ok := d.GetOk("integration_http_method"); ok { - integrationHttpMethod = aws.String(v.(string)) + if v, ok := d.GetOk("cache_key_parameters"); ok && v.(*schema.Set).Len() > 0 { + input.CacheKeyParameters = expandStringSet(v.(*schema.Set)) } - var uri *string - if v, ok := d.GetOk("uri"); ok { - uri = aws.String(v.(string)) + if v, ok := d.GetOk("cache_namespace"); ok { + input.CacheNamespace = aws.String(v.(string)) + } else if input.CacheKeyParameters != nil { + input.CacheNamespace = aws.String(d.Get("resource_id").(string)) } - templates := make(map[string]string) - for k, v := range d.Get("request_templates").(map[string]interface{}) { - templates[k] = v.(string) + if v, ok := d.GetOk("connection_id"); ok { + input.ConnectionId = aws.String(v.(string)) } - parameters := make(map[string]string) - if kv, ok := d.GetOk("request_parameters"); ok { - for k, v := range kv.(map[string]interface{}) { - parameters[k] = v.(string) - } + if v, ok := d.GetOk("connection_type"); ok { + input.ConnectionType = aws.String(v.(string)) } - var passthroughBehavior *string - if v, ok := d.GetOk("passthrough_behavior"); ok { - passthroughBehavior = aws.String(v.(string)) + if v, ok := d.GetOk("content_handling"); ok { + input.ContentHandling = aws.String(v.(string)) } - var credentials *string - if val, ok := d.GetOk("credentials"); ok { - credentials = aws.String(val.(string)) + if v, ok := d.GetOk("credentials"); ok { + input.Credentials = aws.String(v.(string)) } - var contentHandling *string - if val, ok := d.GetOk("content_handling"); ok { - contentHandling = aws.String(val.(string)) + if v, ok := d.GetOk("integration_http_method"); ok { + input.IntegrationHttpMethod = aws.String(v.(string)) } - var cacheKeyParameters []*string - if v, ok := d.GetOk("cache_key_parameters"); ok { - cacheKeyParameters = expandStringSet(v.(*schema.Set)) + if v, ok := d.GetOk("passthrough_behavior"); ok { + input.PassthroughBehavior = aws.String(v.(string)) } - var cacheNamespace *string - if cacheKeyParameters != nil { - // Use resource_id unless user provides a custom name - cacheNamespace = aws.String(d.Get("resource_id").(string)) + if v, ok := d.GetOk("request_parameters"); ok && len(v.(map[string]interface{})) > 0 { + input.RequestParameters = expandStringMap(v.(map[string]interface{})) } - if v, ok := d.GetOk("cache_namespace"); ok { - cacheNamespace = aws.String(v.(string)) + + if v, ok := d.GetOk("request_templates"); ok && len(v.(map[string]interface{})) > 0 { + input.RequestTemplates = expandStringMap(v.(map[string]interface{})) } - var timeoutInMillis *int64 if v, ok := d.GetOk("timeout_milliseconds"); ok { - timeoutInMillis = aws.Int64(int64(v.(int))) - } - - var tlsConfig *apigateway.TlsConfig - if v, ok := d.GetOk("tls_config"); ok { - tlsConfig = expandApiGatewayTlsConfig(v.([]interface{})) - } - - _, err := conn.PutIntegration(&apigateway.PutIntegrationInput{ - HttpMethod: aws.String(d.Get("http_method").(string)), - ResourceId: aws.String(d.Get("resource_id").(string)), - RestApiId: aws.String(d.Get("rest_api_id").(string)), - Type: aws.String(d.Get("type").(string)), - IntegrationHttpMethod: integrationHttpMethod, - Uri: uri, - RequestParameters: aws.StringMap(parameters), - RequestTemplates: aws.StringMap(templates), - Credentials: credentials, - CacheNamespace: cacheNamespace, - CacheKeyParameters: cacheKeyParameters, - PassthroughBehavior: passthroughBehavior, - ContentHandling: contentHandling, - ConnectionType: connectionType, - ConnectionId: connectionId, - TimeoutInMillis: timeoutInMillis, - TlsConfig: tlsConfig, - }) + input.TimeoutInMillis = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("tls_config"); ok && len(v.([]interface{})) > 0 { + input.TlsConfig = expandApiGatewayTlsConfig(v.([]interface{})) + } + + if v, ok := d.GetOk("uri"); ok { + input.Uri = aws.String(v.(string)) + } + + _, err := conn.PutIntegration(input) + if err != nil { return fmt.Errorf("Error creating API Gateway Integration: %s", err) } diff --git a/aws/resource_aws_api_gateway_integration_response_test.go b/aws/resource_aws_api_gateway_integration_response_test.go index c08533b75981..d0ebb075eb65 100644 --- a/aws/resource_aws_api_gateway_integration_response_test.go +++ b/aws/resource_aws_api_gateway_integration_response_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayIntegrationResponse_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationResponseDestroy, Steps: []resource.TestStep{ @@ -66,6 +67,7 @@ func TestAccAWSAPIGatewayIntegrationResponse_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationResponseDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_integration_test.go b/aws/resource_aws_api_gateway_integration_test.go index 4f07224dbf0f..959f4bd8cbcf 100644 --- a/aws/resource_aws_api_gateway_integration_test.go +++ b/aws/resource_aws_api_gateway_integration_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayIntegration_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ @@ -135,6 +136,7 @@ func TestAccAWSAPIGatewayIntegration_contentHandling(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ @@ -210,6 +212,7 @@ func TestAccAWSAPIGatewayIntegration_cache_key_parameters(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ @@ -252,6 +255,7 @@ func TestAccAWSAPIGatewayIntegration_integrationType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ @@ -296,6 +300,7 @@ func TestAccAWSAPIGatewayIntegration_TlsConfig_InsecureSkipVerification(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ @@ -332,6 +337,7 @@ func TestAccAWSAPIGatewayIntegration_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayIntegrationDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_method_response_test.go b/aws/resource_aws_api_gateway_method_response_test.go index 3c8e31741f09..0f453085818c 100644 --- a/aws/resource_aws_api_gateway_method_response_test.go +++ b/aws/resource_aws_api_gateway_method_response_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayMethodResponse_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodResponseDestroy, Steps: []resource.TestStep{ @@ -62,6 +63,7 @@ func TestAccAWSAPIGatewayMethodResponse_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodResponseDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_method_settings_test.go b/aws/resource_aws_api_gateway_method_settings_test.go index 1fe37b10ba71..4454e0116574 100644 --- a/aws/resource_aws_api_gateway_method_settings_test.go +++ b/aws/resource_aws_api_gateway_method_settings_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayMethodSettings_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -47,6 +48,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_CacheDataEncrypted(t *testing.T resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -83,6 +85,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_CacheTtlInSeconds(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -127,6 +130,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_CachingEnabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -163,6 +167,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_DataTraceEnabled(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -199,6 +204,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_LoggingLevel(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -237,6 +243,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_MetricsEnabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -275,6 +282,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_Multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -317,6 +325,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_RequireAuthorizationForCacheCon resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -353,6 +362,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_ThrottlingBurstLimit(t *testing resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -390,6 +400,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_ThrottlingBurstLimitDisabledByD resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -426,6 +437,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_ThrottlingRateLimit(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -463,6 +475,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_ThrottlingRateLimitDisabledByDe resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -499,6 +512,7 @@ func TestAccAWSAPIGatewayMethodSettings_Settings_UnauthorizedCacheControlHeaderS resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ @@ -568,6 +582,7 @@ func TestAccAWSAPIGatewayMethodSettings_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodSettingsDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_method_test.go b/aws/resource_aws_api_gateway_method_test.go index 723946410323..414d1369a1fe 100644 --- a/aws/resource_aws_api_gateway_method_test.go +++ b/aws/resource_aws_api_gateway_method_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayMethod_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ @@ -58,6 +59,7 @@ func TestAccAWSAPIGatewayMethod_customauthorizer(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ @@ -99,6 +101,7 @@ func TestAccAWSAPIGatewayMethod_cognitoauthorizer(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ @@ -143,6 +146,7 @@ func TestAccAWSAPIGatewayMethod_customrequestvalidator(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ @@ -183,6 +187,7 @@ func TestAccAWSAPIGatewayMethod_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ @@ -205,6 +210,7 @@ func TestAccAWSAPIGatewayMethod_OperationName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayMethodDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_model_test.go b/aws/resource_aws_api_gateway_model_test.go index f0d812f9d54b..08c5097704b4 100644 --- a/aws/resource_aws_api_gateway_model_test.go +++ b/aws/resource_aws_api_gateway_model_test.go @@ -21,6 +21,7 @@ func TestAccAWSAPIGatewayModel_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayModelDestroy, Steps: []resource.TestStep{ @@ -56,6 +57,7 @@ func TestAccAWSAPIGatewayModel_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayModelDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_request_validator_test.go b/aws/resource_aws_api_gateway_request_validator_test.go index 3dbf820e19d2..77877dc92dd2 100644 --- a/aws/resource_aws_api_gateway_request_validator_test.go +++ b/aws/resource_aws_api_gateway_request_validator_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayRequestValidator_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRequestValidatorDestroy, Steps: []resource.TestStep{ @@ -63,6 +64,7 @@ func TestAccAWSAPIGatewayRequestValidator_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRequestValidatorDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_resource_test.go b/aws/resource_aws_api_gateway_resource_test.go index d18f6932b981..ac33c4336dab 100644 --- a/aws/resource_aws_api_gateway_resource_test.go +++ b/aws/resource_aws_api_gateway_resource_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayResource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayResourceDestroy, Steps: []resource.TestStep{ @@ -50,6 +51,7 @@ func TestAccAWSAPIGatewayResource_update(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayResourceDestroy, Steps: []resource.TestStep{ @@ -93,6 +95,7 @@ func TestAccAWSAPIGatewayResource_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayResourceDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_rest_api.go b/aws/resource_aws_api_gateway_rest_api.go index f48ef8551b90..6c656aa49589 100644 --- a/aws/resource_aws_api_gateway_rest_api.go +++ b/aws/resource_aws_api_gateway_rest_api.go @@ -135,13 +135,18 @@ func resourceAwsApiGatewayRestApi() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayRestApiCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) log.Printf("[DEBUG] Creating API Gateway") var description *string @@ -170,8 +175,8 @@ func resourceAwsApiGatewayRestApiCreate(d *schema.ResourceData, meta interface{} params.Policy = aws.String(v.(string)) } - if v, ok := d.GetOk("tags"); ok { - params.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ApigatewayTags() + if len(tags) > 0 { + params.Tags = tags.IgnoreAws().ApigatewayTags() } binaryMediaTypes, binaryMediaTypesOk := d.GetOk("binary_media_types") @@ -201,7 +206,7 @@ func resourceAwsApiGatewayRestApiCreate(d *schema.ResourceData, meta interface{} } if v, ok := d.GetOk("parameters"); ok && len(v.(map[string]interface{})) > 0 { - input.Parameters = stringMapToPointers(v.(map[string]interface{})) + input.Parameters = expandStringMap(v.(map[string]interface{})) } output, err := conn.PutRestApi(input) @@ -321,6 +326,7 @@ func resourceAwsApiGatewayRestApiCreate(d *schema.ResourceData, meta interface{} func resourceAwsApiGatewayRestApiRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[DEBUG] Reading API Gateway %s", d.Id()) @@ -366,7 +372,7 @@ func resourceAwsApiGatewayRestApiRead(d *schema.ResourceData, meta interface{}) // I'm not sure why it needs to be wrapped with double quotes first, but it does normalized_policy, err := structure.NormalizeJsonString(`"` + aws.StringValue(api.Policy) + `"`) if err != nil { - fmt.Printf("error normalizing policy JSON: %s\n", err) + return fmt.Errorf("error normalizing policy JSON: %w", err) } policy, err := strconv.Unquote(normalized_policy) if err != nil { @@ -398,8 +404,15 @@ func resourceAwsApiGatewayRestApiRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error setting endpoint_configuration: %s", err) } - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(api.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(api.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } rest_api_arn := arn.ARN{ @@ -542,8 +555,8 @@ func resourceAwsApiGatewayRestApiUpdate(d *schema.ResourceData, meta interface{} conn := meta.(*AWSClient).apigatewayconn log.Printf("[DEBUG] Updating API Gateway %s", d.Id()) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } @@ -560,7 +573,7 @@ func resourceAwsApiGatewayRestApiUpdate(d *schema.ResourceData, meta interface{} } if v, ok := d.GetOk("parameters"); ok && len(v.(map[string]interface{})) > 0 { - input.Parameters = stringMapToPointers(v.(map[string]interface{})) + input.Parameters = expandStringMap(v.(map[string]interface{})) } output, err := conn.PutRestApi(input) diff --git a/aws/resource_aws_api_gateway_rest_api_policy_test.go b/aws/resource_aws_api_gateway_rest_api_policy_test.go index 1e667919e858..a308e84af856 100644 --- a/aws/resource_aws_api_gateway_rest_api_policy_test.go +++ b/aws/resource_aws_api_gateway_rest_api_policy_test.go @@ -21,6 +21,7 @@ func TestAccAWSAPIGatewayRestApiPolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestApiPolicyDestroy, Steps: []resource.TestStep{ @@ -53,6 +54,7 @@ func TestAccAWSAPIGatewayRestApiPolicy_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestApiPolicyDestroy, Steps: []resource.TestStep{ @@ -75,6 +77,7 @@ func TestAccAWSAPIGatewayRestApiPolicy_disappears_restApi(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestApiPolicyDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_rest_api_test.go b/aws/resource_aws_api_gateway_rest_api_test.go index 2efa40cd6216..fba3ed46a5c6 100644 --- a/aws/resource_aws_api_gateway_rest_api_test.go +++ b/aws/resource_aws_api_gateway_rest_api_test.go @@ -71,6 +71,7 @@ func TestAccAWSAPIGatewayRestApi_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -110,6 +111,7 @@ func TestAccAWSAPIGatewayRestApi_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -159,6 +161,7 @@ func TestAccAWSAPIGatewayRestApi_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -181,6 +184,7 @@ func TestAccAWSAPIGatewayRestApi_EndpointConfiguration(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -256,6 +260,7 @@ func TestAccAWSAPIGatewayRestApi_EndpointConfiguration_Private(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -308,6 +313,7 @@ func TestAccAWSAPIGatewayRestApi_ApiKeySource(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -345,6 +351,7 @@ func TestAccAWSAPIGatewayRestApi_ApiKeySource_OverrideBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -388,6 +395,7 @@ func TestAccAWSAPIGatewayRestApi_ApiKeySource_SetByBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -415,6 +423,7 @@ func TestAccAWSAPIGatewayRestApi_BinaryMediaTypes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -451,6 +460,7 @@ func TestAccAWSAPIGatewayRestApi_BinaryMediaTypes_OverrideBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -497,6 +507,7 @@ func TestAccAWSAPIGatewayRestApi_BinaryMediaTypes_SetByBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -525,6 +536,7 @@ func TestAccAWSAPIGatewayRestApi_Body(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -568,6 +580,7 @@ func TestAccAWSAPIGatewayRestApi_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -602,6 +615,7 @@ func TestAccAWSAPIGatewayRestApi_Description_OverrideBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -645,6 +659,7 @@ func TestAccAWSAPIGatewayRestApi_Description_SetByBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -671,6 +686,7 @@ func TestAccAWSAPIGatewayRestApi_DisableExecuteApiEndpoint(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -708,6 +724,7 @@ func TestAccAWSAPIGatewayRestApi_DisableExecuteApiEndpoint_OverrideBody(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -751,6 +768,7 @@ func TestAccAWSAPIGatewayRestApi_DisableExecuteApiEndpoint_SetByBody(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -780,6 +798,7 @@ func TestAccAWSAPIGatewayRestApi_EndpointConfiguration_VpcEndpointIds(t *testing resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -837,6 +856,7 @@ func TestAccAWSAPIGatewayRestApi_EndpointConfiguration_VpcEndpointIds_OverrideBo resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -887,6 +907,7 @@ func TestAccAWSAPIGatewayRestApi_EndpointConfiguration_VpcEndpointIds_SetByBody( resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -916,6 +937,7 @@ func TestAccAWSAPIGatewayRestApi_MinimumCompressionSize(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -957,6 +979,7 @@ func TestAccAWSAPIGatewayRestApi_MinimumCompressionSize_OverrideBody(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1000,6 +1023,7 @@ func TestAccAWSAPIGatewayRestApi_MinimumCompressionSize_SetByBody(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1030,6 +1054,7 @@ func TestAccAWSAPIGatewayRestApi_Name_OverrideBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1073,6 +1098,7 @@ func TestAccAWSAPIGatewayRestApi_Parameters(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1108,6 +1134,7 @@ func TestAccAWSAPIGatewayRestApi_Policy(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1139,6 +1166,7 @@ func TestAccAWSAPIGatewayRestApi_Policy_OverrideBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1185,6 +1213,7 @@ func TestAccAWSAPIGatewayRestApi_Policy_SetByBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayRestAPIDestroy, Steps: []resource.TestStep{ @@ -1348,7 +1377,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1389,7 +1418,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1591,7 +1620,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1673,7 +1702,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1800,7 +1829,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1839,7 +1868,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1888,7 +1917,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1927,7 +1956,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -1966,7 +1995,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2015,7 +2044,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2054,7 +2083,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2102,7 +2131,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2141,7 +2170,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2188,7 +2217,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2227,7 +2256,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2269,7 +2298,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } @@ -2326,7 +2355,7 @@ resource "aws_api_gateway_rest_api" "test" { statusCode = 200 } } - uri = "https://aws.amazon.com/" + uri = "https://api.example.com/" } } } diff --git a/aws/resource_aws_api_gateway_stage.go b/aws/resource_aws_api_gateway_stage.go index a7d4b39f6a5c..f5b794d8843b 100644 --- a/aws/resource_aws_api_gateway_stage.go +++ b/aws/resource_aws_api_gateway_stage.go @@ -111,7 +111,8 @@ func resourceAwsApiGatewayStage() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "xray_tracing_enabled": { Type: schema.TypeBool, Optional: true, @@ -121,11 +122,15 @@ func resourceAwsApiGatewayStage() *schema.Resource { Computed: true, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayStageCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := apigateway.CreateStageInput{ RestApiId: aws.String(d.Get("rest_api_id").(string)), @@ -158,8 +163,8 @@ func resourceAwsApiGatewayStageCreate(d *schema.ResourceData, meta interface{}) } input.Variables = aws.StringMap(variables) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ApigatewayTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().ApigatewayTags() } out, err := conn.CreateStage(&input) @@ -169,7 +174,7 @@ func resourceAwsApiGatewayStageCreate(d *schema.ResourceData, meta interface{}) d.SetId(fmt.Sprintf("ags-%s-%s", d.Get("rest_api_id").(string), d.Get("stage_name").(string))) - if waitForCache && *out.CacheClusterStatus != apigateway.CacheClusterStatusNotAvailable { + if waitForCache && out != nil && aws.StringValue(out.CacheClusterStatus) != apigateway.CacheClusterStatusNotAvailable { stateConf := &resource.StateChangeConf{ Pending: []string{ apigateway.CacheClusterStatusCreateInProgress, @@ -200,6 +205,7 @@ func resourceAwsApiGatewayStageCreate(d *schema.ResourceData, meta interface{}) func resourceAwsApiGatewayStageRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[DEBUG] Reading API Gateway Stage %s", d.Id()) @@ -229,7 +235,7 @@ func resourceAwsApiGatewayStageRead(d *schema.ResourceData, meta interface{}) er d.Set("client_certificate_id", stage.ClientCertificateId) - if stage.CacheClusterStatus != nil && *stage.CacheClusterStatus == apigateway.CacheClusterStatusDeleteInProgress { + if aws.StringValue(stage.CacheClusterStatus) == apigateway.CacheClusterStatusDeleteInProgress { d.Set("cache_cluster_enabled", false) d.Set("cache_cluster_size", nil) } else { @@ -242,8 +248,15 @@ func resourceAwsApiGatewayStageRead(d *schema.ResourceData, meta interface{}) er d.Set("documentation_version", stage.DocumentationVersion) d.Set("xray_tracing_enabled", stage.TracingEnabled) - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(stage.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(stage.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } stageArn := arn.ARN{ @@ -281,8 +294,8 @@ func resourceAwsApiGatewayStageUpdate(d *schema.ResourceData, meta interface{}) Service: "apigateway", Resource: fmt.Sprintf("/restapis/%s/stages/%s", d.Get("rest_api_id").(string), d.Get("stage_name").(string)), }.String() - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, stageArn, o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } @@ -379,7 +392,7 @@ func resourceAwsApiGatewayStageUpdate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Updating API Gateway Stage failed: %s", err) } - if waitForCache && *out.CacheClusterStatus != apigateway.CacheClusterStatusNotAvailable { + if waitForCache && out != nil && aws.StringValue(out.CacheClusterStatus) != apigateway.CacheClusterStatusNotAvailable { stateConf := &resource.StateChangeConf{ Pending: []string{ apigateway.CacheClusterStatusCreateInProgress, diff --git a/aws/resource_aws_api_gateway_stage_test.go b/aws/resource_aws_api_gateway_stage_test.go index 40e497378b54..fa99189e9663 100644 --- a/aws/resource_aws_api_gateway_stage_test.go +++ b/aws/resource_aws_api_gateway_stage_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayStage_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, Steps: []resource.TestStep{ @@ -85,6 +86,7 @@ func TestAccAWSAPIGatewayStage_disappears_ReferencingDeployment(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, Steps: []resource.TestStep{ @@ -111,6 +113,7 @@ func TestAccAWSAPIGatewayStage_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, Steps: []resource.TestStep{ @@ -138,6 +141,7 @@ func TestAccAWSAPIGatewayStage_accessLogSettings(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, Steps: []resource.TestStep{ @@ -205,6 +209,7 @@ func TestAccAWSAPIGatewayStage_accessLogSettings_kinesis(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayStageDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_usage_plan.go b/aws/resource_aws_api_gateway_usage_plan.go index 0de0d6924071..3063fb29d60e 100644 --- a/aws/resource_aws_api_gateway_usage_plan.go +++ b/aws/resource_aws_api_gateway_usage_plan.go @@ -106,17 +106,22 @@ func resourceAwsApiGatewayUsagePlan() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "arn": { Type: schema.TypeString, Computed: true, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayUsagePlanCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) log.Print("[DEBUG] Creating API Gateway Usage Plan") params := &apigateway.CreateUsagePlanInput{ @@ -150,8 +155,8 @@ func resourceAwsApiGatewayUsagePlanCreate(d *schema.ResourceData, meta interface params.Throttle = expandApiGatewayUsageThrottleSettings(v.([]interface{})) } - if v, ok := d.GetOk("tags"); ok { - params.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ApigatewayTags() + if len(tags) > 0 { + params.Tags = tags.IgnoreAws().ApigatewayTags() } up, err := conn.CreateUsagePlan(params) @@ -186,6 +191,7 @@ func resourceAwsApiGatewayUsagePlanCreate(d *schema.ResourceData, meta interface func resourceAwsApiGatewayUsagePlanRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[DEBUG] Reading API Gateway Usage Plan: %s", d.Id()) @@ -202,10 +208,17 @@ func resourceAwsApiGatewayUsagePlanRead(d *schema.ResourceData, meta interface{} return err } - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(up.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags := keyvaluetags.ApigatewayKeyValueTags(up.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + arn := arn.ARN{ Partition: meta.(*AWSClient).partition, Service: "apigateway", @@ -411,8 +424,8 @@ func resourceAwsApiGatewayUsagePlanUpdate(d *schema.ResourceData, meta interface } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %w", err) } diff --git a/aws/resource_aws_api_gateway_usage_plan_key_test.go b/aws/resource_aws_api_gateway_usage_plan_key_test.go index a76d39fa33a6..11bfca8be4c7 100644 --- a/aws/resource_aws_api_gateway_usage_plan_key_test.go +++ b/aws/resource_aws_api_gateway_usage_plan_key_test.go @@ -22,6 +22,7 @@ func TestAccAWSAPIGatewayUsagePlanKey_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanKeyDestroy, Steps: []resource.TestStep{ @@ -53,6 +54,7 @@ func TestAccAWSAPIGatewayUsagePlanKey_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanKeyDestroy, Steps: []resource.TestStep{ @@ -74,6 +76,7 @@ func TestAccAWSAPIGatewayUsagePlanKey_KeyId_Concurrency(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanKeyDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_usage_plan_test.go b/aws/resource_aws_api_gateway_usage_plan_test.go index 400eca313919..bfa231af4977 100644 --- a/aws/resource_aws_api_gateway_usage_plan_test.go +++ b/aws/resource_aws_api_gateway_usage_plan_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayUsagePlan_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -59,6 +60,7 @@ func TestAccAWSAPIGatewayUsagePlan_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -105,6 +107,7 @@ func TestAccAWSAPIGatewayUsagePlan_description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -158,6 +161,7 @@ func TestAccAWSAPIGatewayUsagePlan_productCode(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -205,6 +209,7 @@ func TestAccAWSAPIGatewayUsagePlan_throttling(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -259,6 +264,7 @@ func TestAccAWSAPIGatewayUsagePlan_throttlingInitialRateLimit(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -285,6 +291,7 @@ func TestAccAWSAPIGatewayUsagePlan_quota(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -340,6 +347,7 @@ func TestAccAWSAPIGatewayUsagePlan_apiStages(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -423,6 +431,7 @@ func TestAccAWSAPIGatewayUsagePlan_apiStages_multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ @@ -455,6 +464,7 @@ func TestAccAWSAPIGatewayUsagePlan_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayUsagePlanDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_api_gateway_vpc_link.go b/aws/resource_aws_api_gateway_vpc_link.go index 9bc2ae15e985..e686fb46f33f 100644 --- a/aws/resource_aws_api_gateway_vpc_link.go +++ b/aws/resource_aws_api_gateway_vpc_link.go @@ -3,21 +3,13 @@ package aws import ( "fmt" "log" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/apigateway" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" -) - -const ( - // Maximum amount of time for VpcLink to become available - apigatewayVpcLinkAvailableTimeout = 20 * time.Minute - // Maximum amount of time for VpcLink to delete - apigatewayVpcLinkDeleteTimeout = 20 * time.Minute + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apigateway/waiter" ) func resourceAwsApiGatewayVpcLink() *schema.Resource { @@ -51,19 +43,23 @@ func resourceAwsApiGatewayVpcLink() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayVpcLinkCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn - tags := keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().ApigatewayTags() + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &apigateway.CreateVpcLinkInput{ Name: aws.String(d.Get("name").(string)), TargetArns: expandStringList(d.Get("target_arns").([]interface{})), - Tags: tags, + Tags: tags.IgnoreAws().ApigatewayTags(), } if v, ok := d.GetOk("description"); ok { input.Description = aws.String(v.(string)) @@ -76,18 +72,8 @@ func resourceAwsApiGatewayVpcLinkCreate(d *schema.ResourceData, meta interface{} d.SetId(aws.StringValue(resp.Id)) - stateConf := &resource.StateChangeConf{ - Pending: []string{apigateway.VpcLinkStatusPending}, - Target: []string{apigateway.VpcLinkStatusAvailable}, - Refresh: apigatewayVpcLinkRefreshStatusFunc(conn, *resp.Id), - Timeout: apigatewayVpcLinkAvailableTimeout, - MinTimeout: 3 * time.Second, - } - - _, err = stateConf.WaitForState() - if err != nil { - d.SetId("") - return fmt.Errorf("Error waiting for APIGateway Vpc Link status to be \"%s\": %s", apigateway.VpcLinkStatusAvailable, err) + if err := waiter.ApiGatewayVpcLinkAvailable(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for API Gateway VPC Link (%s) availability after creation: %w", d.Id(), err) } return resourceAwsApiGatewayVpcLinkRead(d, meta) @@ -95,6 +81,7 @@ func resourceAwsApiGatewayVpcLinkCreate(d *schema.ResourceData, meta interface{} func resourceAwsApiGatewayVpcLinkRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &apigateway.GetVpcLinkInput{ @@ -111,8 +98,15 @@ func resourceAwsApiGatewayVpcLinkRead(d *schema.ResourceData, meta interface{}) return err } - if err := d.Set("tags", keyvaluetags.ApigatewayKeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.ApigatewayKeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } arn := arn.ARN{ @@ -150,8 +144,8 @@ func resourceAwsApiGatewayVpcLinkUpdate(d *schema.ResourceData, meta interface{} }) } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.ApigatewayUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } @@ -172,17 +166,8 @@ func resourceAwsApiGatewayVpcLinkUpdate(d *schema.ResourceData, meta interface{} return err } - stateConf := &resource.StateChangeConf{ - Pending: []string{apigateway.VpcLinkStatusPending}, - Target: []string{apigateway.VpcLinkStatusAvailable}, - Refresh: apigatewayVpcLinkRefreshStatusFunc(conn, d.Id()), - Timeout: apigatewayVpcLinkAvailableTimeout, - MinTimeout: 3 * time.Second, - } - - _, err = stateConf.WaitForState() - if err != nil { - return fmt.Errorf("Error waiting for APIGateway Vpc Link status to be \"%s\": %s", apigateway.VpcLinkStatusAvailable, err) + if err := waiter.ApiGatewayVpcLinkAvailable(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for API Gateway VPC Link (%s) availability after update: %w", d.Id(), err) } return resourceAwsApiGatewayVpcLinkRead(d, meta) @@ -202,55 +187,12 @@ func resourceAwsApiGatewayVpcLinkDelete(d *schema.ResourceData, meta interface{} } if err != nil { - return fmt.Errorf("error deleting API Gateway VPC Link (%s): %s", d.Id(), err) + return fmt.Errorf("error deleting API Gateway VPC Link (%s): %w", d.Id(), err) } - if err := waitForApiGatewayVpcLinkDeletion(conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for API Gateway VPC Link (%s) deletion: %s", d.Id(), err) + if err := waiter.ApiGatewayVpcLinkDeleted(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for API Gateway VPC Link (%s) deletion: %w", d.Id(), err) } return nil } - -func apigatewayVpcLinkRefreshStatusFunc(conn *apigateway.APIGateway, vl string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - input := &apigateway.GetVpcLinkInput{ - VpcLinkId: aws.String(vl), - } - resp, err := conn.GetVpcLink(input) - if err != nil { - return nil, "failed", err - } - return resp, *resp.Status, nil - } -} - -func waitForApiGatewayVpcLinkDeletion(conn *apigateway.APIGateway, vpcLinkID string) error { - stateConf := resource.StateChangeConf{ - Pending: []string{apigateway.VpcLinkStatusPending, - apigateway.VpcLinkStatusAvailable, - apigateway.VpcLinkStatusDeleting}, - Target: []string{""}, - Timeout: apigatewayVpcLinkDeleteTimeout, - MinTimeout: 1 * time.Second, - Refresh: func() (interface{}, string, error) { - resp, err := conn.GetVpcLink(&apigateway.GetVpcLinkInput{ - VpcLinkId: aws.String(vpcLinkID), - }) - - if isAWSErr(err, apigateway.ErrCodeNotFoundException, "") { - return 1, "", nil - } - - if err != nil { - return nil, apigateway.VpcLinkStatusFailed, err - } - - return resp, aws.StringValue(resp.Status), nil - }, - } - - _, err := stateConf.WaitForState() - - return err -} diff --git a/aws/resource_aws_api_gateway_vpc_link_test.go b/aws/resource_aws_api_gateway_vpc_link_test.go index 0cb2d3bcebf8..db5377730588 100644 --- a/aws/resource_aws_api_gateway_vpc_link_test.go +++ b/aws/resource_aws_api_gateway_vpc_link_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigateway" + multierror "github.com/hashicorp/go-multierror" "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" @@ -23,40 +24,39 @@ func init() { func testSweepAPIGatewayVpcLinks(region string) error { client, err := sharedClientForRegion(region) if err != nil { - return fmt.Errorf("error getting client: %s", err) + return fmt.Errorf("error getting client: %w", err) } conn := client.(*AWSClient).apigatewayconn + sweepResources := make([]*testSweepResource, 0) + var sweeperErrs *multierror.Error + err = conn.GetVpcLinksPages(&apigateway.GetVpcLinksInput{}, func(page *apigateway.GetVpcLinksOutput, lastPage bool) bool { for _, item := range page.Items { - input := &apigateway.DeleteVpcLinkInput{ - VpcLinkId: item.Id, - } id := aws.StringValue(item.Id) - log.Printf("[INFO] Deleting API Gateway VPC Link: %s", id) - _, err := conn.DeleteVpcLink(input) + log.Printf("[INFO] Deleting API Gateway VPC Link (%s)", id) + r := resourceAwsApiGatewayVpcLink() + d := r.Data(nil) + d.SetId(id) - if err != nil { - log.Printf("[ERROR] Failed to delete API Gateway VPC Link %s: %s", id, err) - continue - } - - if err := waitForApiGatewayVpcLinkDeletion(conn, id); err != nil { - log.Printf("[ERROR] Error waiting for API Gateway VPC Link (%s) deletion: %s", id, err) - } + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) } return !lastPage }) + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping API Gateway VPC Link sweep for %s: %s", region, err) + return nil + } if err != nil { - if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping API Gateway VPC Link sweep for %s: %s", region, err) - return nil - } - return fmt.Errorf("Error retrieving API Gateway VPC Links: %s", err) + return fmt.Errorf("error retrieving API Gateway VPC Links: %w", err) } - return nil + if err := testSweepResourceOrchestrator(sweepResources); err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping API Gateway VPC Links: %w", err)) + } + + return sweeperErrs.ErrorOrNil() } func TestAccAWSAPIGatewayVpcLink_basic(t *testing.T) { @@ -67,6 +67,7 @@ func TestAccAWSAPIGatewayVpcLink_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAPIGatewayVpcLinkDestroy, Steps: []resource.TestStep{ @@ -107,6 +108,7 @@ func TestAccAWSAPIGatewayVpcLink_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAPIGatewayVpcLinkDestroy, Steps: []resource.TestStep{ @@ -159,6 +161,7 @@ func TestAccAWSAPIGatewayVpcLink_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigateway.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAPIGatewayVpcLinkDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_api.go b/aws/resource_aws_apigatewayv2_api.go index 69f868c0f7be..63131d79992e 100644 --- a/aws/resource_aws_apigatewayv2_api.go +++ b/aws/resource_aws_apigatewayv2_api.go @@ -97,6 +97,10 @@ func resourceAwsApiGatewayV2Api() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + "fail_on_warnings": { + Type: schema.TypeBool, + Optional: true, + }, "execution_arn": { Type: schema.TypeString, Computed: true, @@ -128,7 +132,8 @@ func resourceAwsApiGatewayV2Api() *schema.Resource { Optional: true, Default: "$request.method $request.path", }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "target": { Type: schema.TypeString, Optional: true, @@ -140,11 +145,14 @@ func resourceAwsApiGatewayV2Api() *schema.Resource { ValidateFunc: validation.StringLenBetween(1, 64), }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAPIGatewayV2ImportOpenAPI(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig if body, ok := d.GetOk("body"); ok { revertReq := &apigatewayv2.UpdateApiInput{ @@ -160,13 +168,18 @@ func resourceAwsAPIGatewayV2ImportOpenAPI(d *schema.ResourceData, meta interface Body: aws.String(body.(string)), } + if value, ok := d.GetOk("fail_on_warnings"); ok { + importReq.FailOnWarnings = aws.Bool(value.(bool)) + } + _, err := conn.ReimportApi(importReq) if err != nil { return fmt.Errorf("error importing API Gateway v2 API (%s) OpenAPI specification: %s", d.Id(), err) } - tags := d.Get("tags") + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + corsConfiguration := d.Get("cors_configuration") if err := resourceAwsApiGatewayV2ApiRead(d, meta); err != nil { @@ -187,7 +200,7 @@ func resourceAwsAPIGatewayV2ImportOpenAPI(d *schema.ResourceData, meta interface } } - if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), d.Get("tags"), tags); err != nil { + if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), d.Get("tags_all"), tags); err != nil { return fmt.Errorf("error updating API Gateway v2 API (%s) tags: %s", d.Id(), err) } @@ -203,12 +216,14 @@ func resourceAwsAPIGatewayV2ImportOpenAPI(d *schema.ResourceData, meta interface func resourceAwsApiGatewayV2ApiCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) protocolType := d.Get("protocol_type").(string) req := &apigatewayv2.CreateApiInput{ Name: aws.String(d.Get("name").(string)), ProtocolType: aws.String(protocolType), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().Apigatewayv2Tags(), + Tags: tags.IgnoreAws().Apigatewayv2Tags(), } if v, ok := d.GetOk("api_key_selection_expression"); ok { req.ApiKeySelectionExpression = aws.String(v.(string)) @@ -256,6 +271,7 @@ func resourceAwsApiGatewayV2ApiCreate(d *schema.ResourceData, meta interface{}) func resourceAwsApiGatewayV2ApiRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig resp, err := conn.GetApi(&apigatewayv2.GetApiInput{ @@ -295,8 +311,16 @@ func resourceAwsApiGatewayV2ApiRead(d *schema.ResourceData, meta interface{}) er d.Set("name", resp.Name) d.Set("protocol_type", resp.ProtocolType) d.Set("route_selection_expression", resp.RouteSelectionExpression) - if err := d.Set("tags", keyvaluetags.Apigatewayv2KeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + + tags := keyvaluetags.Apigatewayv2KeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } d.Set("version", resp.Version) @@ -357,8 +381,8 @@ func resourceAwsApiGatewayV2ApiUpdate(d *schema.ResourceData, meta interface{}) } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating API Gateway v2 API (%s) tags: %s", d.Id(), err) } diff --git a/aws/resource_aws_apigatewayv2_api_mapping_test.go b/aws/resource_aws_apigatewayv2_api_mapping_test.go index ae77f9ea59af..0ca812940f04 100644 --- a/aws/resource_aws_apigatewayv2_api_mapping_test.go +++ b/aws/resource_aws_apigatewayv2_api_mapping_test.go @@ -43,6 +43,7 @@ func TestAccAWSAPIGatewayV2ApiMapping_basic(t *testing.T) { func testAccAWSAPIGatewayV2ApiMapping_createCertificate(t *testing.T, rName string, certificateArn *string) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ @@ -67,6 +68,7 @@ func testAccAWSAPIGatewayV2ApiMapping_basic(t *testing.T, rName string, certific resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiMappingDestroy, Steps: []resource.TestStep{ @@ -94,6 +96,7 @@ func testAccAWSAPIGatewayV2ApiMapping_disappears(t *testing.T, rName string, cer resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiMappingDestroy, Steps: []resource.TestStep{ @@ -118,6 +121,7 @@ func testAccAWSAPIGatewayV2ApiMapping_ApiMappingKey(t *testing.T, rName string, resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiMappingDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_api_test.go b/aws/resource_aws_apigatewayv2_api_test.go index c1f6d7da02aa..a0a84afad1b9 100644 --- a/aws/resource_aws_apigatewayv2_api_test.go +++ b/aws/resource_aws_apigatewayv2_api_test.go @@ -75,6 +75,7 @@ func TestAccAWSAPIGatewayV2Api_basicWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -112,6 +113,7 @@ func TestAccAWSAPIGatewayV2Api_basicHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -149,6 +151,7 @@ func TestAccAWSAPIGatewayV2Api_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -172,6 +175,7 @@ func TestAccAWSAPIGatewayV2Api_AllAttributesWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -253,6 +257,7 @@ func TestAccAWSAPIGatewayV2Api_AllAttributesHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -333,6 +338,7 @@ func TestAccAWSAPIGatewayV2Api_Openapi(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -379,6 +385,7 @@ func TestAccAWSAPIGatewayV2Api_Openapi_WithTags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -423,6 +430,7 @@ func TestAccAWSAPIGatewayV2Api_Openapi_WithCorsConfiguration(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -479,6 +487,7 @@ func TestAccAWSAPIGatewayV2Api_OpenapiWithMoreFields(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -524,6 +533,59 @@ func TestAccAWSAPIGatewayV2Api_OpenapiWithMoreFields(t *testing.T) { }) } +func TestAccAWSAPIGatewayV2Api_Openapi_FailOnWarnings(t *testing.T) { + var v apigatewayv2.GetApiOutput + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, + Steps: []resource.TestStep{ + // Invalid body should not be accepted when fail_on_warnings is enabled + { + Config: testAccAWSAPIGatewayV2ApiConfig_FailOnWarnings(rName, "fail_on_warnings = true"), + ExpectError: regexp.MustCompile(`BadRequestException: Warnings found during import`), + }, + // Warnings do not break the deployment when fail_on_warnings is disabled + { + Config: testAccAWSAPIGatewayV2ApiConfig_FailOnWarnings(rName, "fail_on_warnings = false"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + resource.TestCheckResourceAttr(resourceName, "fail_on_warnings", "false"), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body", "fail_on_warnings"}, + }, + // fail_on_warnings should be optional and false by default + { + Config: testAccAWSAPIGatewayV2ApiConfig_FailOnWarnings(rName, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body", "fail_on_warnings"}, + }, + }, + }) +} + func testAccCheckAWSAPIGatewayV2ApiRoutes(v *apigatewayv2.GetApiOutput, routes []string) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).apigatewayv2conn @@ -562,6 +624,7 @@ func TestAccAWSAPIGatewayV2Api_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -614,6 +677,7 @@ func TestAccAWSAPIGatewayV2Api_CorsConfiguration(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -704,6 +768,7 @@ func TestAccAWSAPIGatewayV2Api_QuickCreate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, Steps: []resource.TestStep{ @@ -1263,3 +1328,47 @@ EOF } `, rName, rName) } + +func testAccAWSAPIGatewayV2ApiConfig_FailOnWarnings(rName string, failOnWarnings string) string { + return fmt.Sprintf(` +resource "aws_apigatewayv2_api" "test" { + name = %[1]q + protocol_type = "HTTP" + body = < { - # name = dvo.resource_record_name - # record = dvo.resource_record_value - # type = dvo.resource_record_type - # } - # } - # allow_overwrite = true - # name = each.value.name - # records = [each.value.record] - # ttl = 60 - # type = each.value.type - # zone_id = data.aws_route53_zone.test.zone_id - # } +# +# for_each acceptance testing requires: +# https://github.com/hashicorp/terraform-plugin-sdk/issues/536 +# +# resource "aws_route53_record" "test" { +# for_each = { +# for dvo in aws_acm_certificate.test.domain_validation_options: dvo.domain_name => { +# name = dvo.resource_record_name +# record = dvo.resource_record_value +# type = dvo.resource_record_type +# } +# } +# allow_overwrite = true +# name = each.value.name +# records = [each.value.record] +# ttl = 60 +# type = each.value.type +# zone_id = data.aws_route53_zone.test.zone_id +# } resource "aws_route53_record" "test" { allow_overwrite = true @@ -487,7 +510,7 @@ resource "aws_apigatewayv2_domain_name" "test" { `, rName, index)) } -func testAccAWSAPIGatewayV2DomainNameConfigMututalTlsAuthentication(rootDomain, domain, rName string) string { +func testAccAWSAPIGatewayV2DomainNameConfigMututalTlsAuthenticationNoObjectVersion(rootDomain, domain, rName, pemFileName string) string { return composeConfig( testAccAWSAPIGatewayV2DomainNameConfigPublicCert(rootDomain, domain), fmt.Sprintf(` @@ -499,8 +522,8 @@ resource "aws_s3_bucket" "test" { resource "aws_s3_bucket_object" "test" { bucket = aws_s3_bucket.test.id - key = "%[1]s.1" - source = "test-fixtures/apigateway-domain-name-truststore-1.pem" + key = %[1]q + source = "test-fixtures/%[2]s" } resource "aws_apigatewayv2_domain_name" "test" { @@ -516,10 +539,10 @@ resource "aws_apigatewayv2_domain_name" "test" { truststore_uri = "s3://${aws_s3_bucket_object.test.bucket}/${aws_s3_bucket_object.test.key}" } } -`, rName)) +`, rName, pemFileName)) } -func testAccAWSAPIGatewayV2DomainNameConfigMututalTlsAuthenticationUpdated(rootDomain, domain, rName string) string { +func testAccAWSAPIGatewayV2DomainNameConfigMututalTlsAuthenticationObjectVersion(rootDomain, domain, rName, pemFileName string) string { return composeConfig( testAccAWSAPIGatewayV2DomainNameConfigPublicCert(rootDomain, domain), fmt.Sprintf(` @@ -535,8 +558,8 @@ resource "aws_s3_bucket" "test" { resource "aws_s3_bucket_object" "test" { bucket = aws_s3_bucket.test.id - key = "%[1]s.2" - source = "test-fixtures/apigateway-domain-name-truststore-2.pem" + key = %[1]q + source = "test-fixtures/%[2]s" } resource "aws_apigatewayv2_domain_name" "test" { @@ -553,7 +576,7 @@ resource "aws_apigatewayv2_domain_name" "test" { truststore_version = aws_s3_bucket_object.test.version_id } } -`, rName)) +`, rName, pemFileName)) } func testAccAWSAPIGatewayV2DomainNameConfigMututalTlsAuthenticationMissing(rootDomain, domain string) string { diff --git a/aws/resource_aws_apigatewayv2_integration.go b/aws/resource_aws_apigatewayv2_integration.go index 5c7a517bd658..2974eb3cf6e3 100644 --- a/aws/resource_aws_apigatewayv2_integration.go +++ b/aws/resource_aws_apigatewayv2_integration.go @@ -203,10 +203,10 @@ func resourceAwsApiGatewayV2IntegrationCreate(d *schema.ResourceData, meta inter req.PayloadFormatVersion = aws.String(v.(string)) } if v, ok := d.GetOk("request_parameters"); ok { - req.RequestParameters = stringMapToPointers(v.(map[string]interface{})) + req.RequestParameters = expandStringMap(v.(map[string]interface{})) } if v, ok := d.GetOk("request_templates"); ok { - req.RequestTemplates = stringMapToPointers(v.(map[string]interface{})) + req.RequestTemplates = expandStringMap(v.(map[string]interface{})) } if v, ok := d.GetOk("response_parameters"); ok && v.(*schema.Set).Len() > 0 { req.ResponseParameters = expandApiGateway2IntegrationResponseParameters(v.(*schema.Set).List()) @@ -342,7 +342,7 @@ func resourceAwsApiGatewayV2IntegrationUpdate(d *schema.ResourceData, meta inter req.RequestParameters = variables } if d.HasChange("request_templates") { - req.RequestTemplates = stringMapToPointers(d.Get("request_templates").(map[string]interface{})) + req.RequestTemplates = expandStringMap(d.Get("request_templates").(map[string]interface{})) } if d.HasChange("response_parameters") { o, n := d.GetChange("response_parameters") @@ -475,7 +475,7 @@ func expandApiGateway2IntegrationResponseParameters(tfList []interface{}) map[st if vStatusCode, ok := tfMap["status_code"].(string); ok && vStatusCode != "" { if v, ok := tfMap["mappings"].(map[string]interface{}); ok && len(v) > 0 { - responseParameters[vStatusCode] = stringMapToPointers(v) + responseParameters[vStatusCode] = expandStringMap(v) } } } diff --git a/aws/resource_aws_apigatewayv2_integration_response.go b/aws/resource_aws_apigatewayv2_integration_response.go index 26a7de405c01..9cc934698040 100644 --- a/aws/resource_aws_apigatewayv2_integration_response.go +++ b/aws/resource_aws_apigatewayv2_integration_response.go @@ -69,7 +69,7 @@ func resourceAwsApiGatewayV2IntegrationResponseCreate(d *schema.ResourceData, me req.ContentHandlingStrategy = aws.String(v.(string)) } if v, ok := d.GetOk("response_templates"); ok { - req.ResponseTemplates = stringMapToPointers(v.(map[string]interface{})) + req.ResponseTemplates = expandStringMap(v.(map[string]interface{})) } if v, ok := d.GetOk("template_selection_expression"); ok { req.TemplateSelectionExpression = aws.String(v.(string)) @@ -129,7 +129,7 @@ func resourceAwsApiGatewayV2IntegrationResponseUpdate(d *schema.ResourceData, me req.IntegrationResponseKey = aws.String(d.Get("integration_response_key").(string)) } if d.HasChange("response_templates") { - req.ResponseTemplates = stringMapToPointers(d.Get("response_templates").(map[string]interface{})) + req.ResponseTemplates = expandStringMap(d.Get("response_templates").(map[string]interface{})) } if d.HasChange("template_selection_expression") { req.TemplateSelectionExpression = aws.String(d.Get("template_selection_expression").(string)) diff --git a/aws/resource_aws_apigatewayv2_integration_response_test.go b/aws/resource_aws_apigatewayv2_integration_response_test.go index ce62dc29e718..cb30a074f0f3 100644 --- a/aws/resource_aws_apigatewayv2_integration_response_test.go +++ b/aws/resource_aws_apigatewayv2_integration_response_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayV2IntegrationResponse_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationResponseDestroy, Steps: []resource.TestStep{ @@ -52,6 +53,7 @@ func TestAccAWSAPIGatewayV2IntegrationResponse_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationResponseDestroy, Steps: []resource.TestStep{ @@ -76,6 +78,7 @@ func TestAccAWSAPIGatewayV2IntegrationResponse_AllAttributes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationResponseDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_integration_test.go b/aws/resource_aws_apigatewayv2_integration_test.go index 2b0248cc96dc..f2fe1425aabb 100644 --- a/aws/resource_aws_apigatewayv2_integration_test.go +++ b/aws/resource_aws_apigatewayv2_integration_test.go @@ -19,6 +19,7 @@ func TestAccAWSAPIGatewayV2Integration_basicWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -64,6 +65,7 @@ func TestAccAWSAPIGatewayV2Integration_basicHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -109,6 +111,7 @@ func TestAccAWSAPIGatewayV2Integration_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -132,6 +135,7 @@ func TestAccAWSAPIGatewayV2Integration_DataMappingHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -222,6 +226,7 @@ func TestAccAWSAPIGatewayV2Integration_IntegrationTypeHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -298,6 +303,7 @@ func TestAccAWSAPIGatewayV2Integration_LambdaWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -343,6 +349,7 @@ func TestAccAWSAPIGatewayV2Integration_LambdaHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -388,6 +395,7 @@ func TestAccAWSAPIGatewayV2Integration_VpcLinkWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -435,6 +443,7 @@ func TestAccAWSAPIGatewayV2Integration_VpcLinkHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ @@ -515,6 +524,7 @@ func TestAccAWSAPIGatewayV2Integration_AwsServiceIntegration(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2IntegrationDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_model_test.go b/aws/resource_aws_apigatewayv2_model_test.go index 9b8a7ab48bba..2b8bf791adab 100644 --- a/aws/resource_aws_apigatewayv2_model_test.go +++ b/aws/resource_aws_apigatewayv2_model_test.go @@ -33,6 +33,7 @@ func TestAccAWSAPIGatewayV2Model_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ModelDestroy, Steps: []resource.TestStep{ @@ -77,6 +78,7 @@ func TestAccAWSAPIGatewayV2Model_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ModelDestroy, Steps: []resource.TestStep{ @@ -128,6 +130,7 @@ func TestAccAWSAPIGatewayV2Model_AllAttributes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2ModelDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_route.go b/aws/resource_aws_apigatewayv2_route.go index 4ddb0537b923..3fa7e2948a78 100644 --- a/aws/resource_aws_apigatewayv2_route.go +++ b/aws/resource_aws_apigatewayv2_route.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigatewayv2" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -38,15 +39,10 @@ func resourceAwsApiGatewayV2Route() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, "authorization_type": { - Type: schema.TypeString, - Optional: true, - Default: apigatewayv2.AuthorizationTypeNone, - ValidateFunc: validation.StringInSlice([]string{ - apigatewayv2.AuthorizationTypeNone, - apigatewayv2.AuthorizationTypeAwsIam, - apigatewayv2.AuthorizationTypeCustom, - apigatewayv2.AuthorizationTypeJwt, - }, false), + Type: schema.TypeString, + Optional: true, + Default: apigatewayv2.AuthorizationTypeNone, + ValidateFunc: validation.StringInSlice(apigatewayv2.AuthorizationType_Values(), false), }, "authorizer_id": { Type: schema.TypeString, @@ -66,6 +62,23 @@ func resourceAwsApiGatewayV2Route() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "request_parameter": { + Type: schema.TypeSet, + Optional: true, + MinItems: 0, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "request_parameter_key": { + Type: schema.TypeString, + Required: true, + }, + "required": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, "route_key": { Type: schema.TypeString, Required: true, @@ -105,7 +118,10 @@ func resourceAwsApiGatewayV2RouteCreate(d *schema.ResourceData, meta interface{} req.OperationName = aws.String(v.(string)) } if v, ok := d.GetOk("request_models"); ok { - req.RequestModels = stringMapToPointers(v.(map[string]interface{})) + req.RequestModels = expandStringMap(v.(map[string]interface{})) + } + if v, ok := d.GetOk("request_parameter"); ok && v.(*schema.Set).Len() > 0 { + req.RequestParameters = expandApiGatewayV2RouteRequestParameters(v.(*schema.Set).List()) } if v, ok := d.GetOk("route_response_selection_expression"); ok { req.RouteResponseSelectionExpression = aws.String(v.(string)) @@ -117,7 +133,7 @@ func resourceAwsApiGatewayV2RouteCreate(d *schema.ResourceData, meta interface{} log.Printf("[DEBUG] Creating API Gateway v2 route: %s", req) resp, err := conn.CreateRoute(req) if err != nil { - return fmt.Errorf("error creating API Gateway v2 route: %s", err) + return fmt.Errorf("error creating API Gateway v2 route: %w", err) } d.SetId(aws.StringValue(resp.RouteId)) @@ -132,25 +148,30 @@ func resourceAwsApiGatewayV2RouteRead(d *schema.ResourceData, meta interface{}) ApiId: aws.String(d.Get("api_id").(string)), RouteId: aws.String(d.Id()), }) - if isAWSErr(err, apigatewayv2.ErrCodeNotFoundException, "") { + + if tfawserr.ErrCodeEquals(err, apigatewayv2.ErrCodeNotFoundException) { log.Printf("[WARN] API Gateway v2 route (%s) not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { - return fmt.Errorf("error reading API Gateway v2 route: %s", err) + return fmt.Errorf("error reading API Gateway v2 route (%s): %w", d.Id(), err) } d.Set("api_key_required", resp.ApiKeyRequired) if err := d.Set("authorization_scopes", flattenStringSet(resp.AuthorizationScopes)); err != nil { - return fmt.Errorf("error setting authorization_scopes: %s", err) + return fmt.Errorf("error setting authorization_scopes: %w", err) } d.Set("authorization_type", resp.AuthorizationType) d.Set("authorizer_id", resp.AuthorizerId) d.Set("model_selection_expression", resp.ModelSelectionExpression) d.Set("operation_name", resp.OperationName) if err := d.Set("request_models", pointersMapToStringList(resp.RequestModels)); err != nil { - return fmt.Errorf("error setting request_models: %s", err) + return fmt.Errorf("error setting request_models: %w", err) + } + if err := d.Set("request_parameter", flattenApiGatewayV2RouteRequestParameters(resp.RequestParameters)); err != nil { + return fmt.Errorf("error setting request_parameter: %w", err) } d.Set("route_key", resp.RouteKey) d.Set("route_response_selection_expression", resp.RouteResponseSelectionExpression) @@ -162,45 +183,86 @@ func resourceAwsApiGatewayV2RouteRead(d *schema.ResourceData, meta interface{}) func resourceAwsApiGatewayV2RouteUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn - req := &apigatewayv2.UpdateRouteInput{ - ApiId: aws.String(d.Get("api_id").(string)), - RouteId: aws.String(d.Id()), - } - if d.HasChange("api_key_required") { - req.ApiKeyRequired = aws.Bool(d.Get("api_key_required").(bool)) - } - if d.HasChange("authorization_scopes") { - req.AuthorizationScopes = expandStringSet(d.Get("authorization_scopes").(*schema.Set)) - } - if d.HasChange("authorization_type") { - req.AuthorizationType = aws.String(d.Get("authorization_type").(string)) - } - if d.HasChange("authorizer_id") { - req.AuthorizerId = aws.String(d.Get("authorizer_id").(string)) - } - if d.HasChange("model_selection_expression") { - req.ModelSelectionExpression = aws.String(d.Get("model_selection_expression").(string)) - } - if d.HasChange("operation_name") { - req.OperationName = aws.String(d.Get("operation_name").(string)) - } - if d.HasChange("request_models") { - req.RequestModels = stringMapToPointers(d.Get("request_models").(map[string]interface{})) - } - if d.HasChange("route_key") { - req.RouteKey = aws.String(d.Get("route_key").(string)) - } - if d.HasChange("route_response_selection_expression") { - req.RouteResponseSelectionExpression = aws.String(d.Get("route_response_selection_expression").(string)) - } - if d.HasChange("target") { - req.Target = aws.String(d.Get("target").(string)) + var requestParameters map[string]*apigatewayv2.ParameterConstraints + + if d.HasChange("request_parameter") { + o, n := d.GetChange("request_parameter") + os := o.(*schema.Set) + ns := n.(*schema.Set) + + for _, tfMapRaw := range os.Difference(ns).List() { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + if v, ok := tfMap["request_parameter_key"].(string); ok && v != "" { + log.Printf("[DEBUG] Deleting API Gateway v2 route (%s) request parameter (%s)", d.Id(), v) + _, err := conn.DeleteRouteRequestParameter(&apigatewayv2.DeleteRouteRequestParameterInput{ + ApiId: aws.String(d.Get("api_id").(string)), + RequestParameterKey: aws.String(v), + RouteId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, apigatewayv2.ErrCodeNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("error deleting API Gateway v2 route (%s) request parameter (%s): %w", d.Id(), v, err) + } + } + } + + requestParameters = expandApiGatewayV2RouteRequestParameters(ns.List()) } - log.Printf("[DEBUG] Updating API Gateway v2 route: %s", req) - _, err := conn.UpdateRoute(req) - if err != nil { - return fmt.Errorf("error updating API Gateway v2 route: %s", err) + if d.HasChangesExcept("request_parameter") || len(requestParameters) > 0 { + req := &apigatewayv2.UpdateRouteInput{ + ApiId: aws.String(d.Get("api_id").(string)), + RouteId: aws.String(d.Id()), + } + if d.HasChange("api_key_required") { + req.ApiKeyRequired = aws.Bool(d.Get("api_key_required").(bool)) + } + if d.HasChange("authorization_scopes") { + req.AuthorizationScopes = expandStringSet(d.Get("authorization_scopes").(*schema.Set)) + } + if d.HasChange("authorization_type") { + req.AuthorizationType = aws.String(d.Get("authorization_type").(string)) + } + if d.HasChange("authorizer_id") { + req.AuthorizerId = aws.String(d.Get("authorizer_id").(string)) + } + if d.HasChange("model_selection_expression") { + req.ModelSelectionExpression = aws.String(d.Get("model_selection_expression").(string)) + } + if d.HasChange("operation_name") { + req.OperationName = aws.String(d.Get("operation_name").(string)) + } + if d.HasChange("request_models") { + req.RequestModels = expandStringMap(d.Get("request_models").(map[string]interface{})) + } + if d.HasChange("request_parameter") { + req.RequestParameters = requestParameters + } + if d.HasChange("route_key") { + req.RouteKey = aws.String(d.Get("route_key").(string)) + } + if d.HasChange("route_response_selection_expression") { + req.RouteResponseSelectionExpression = aws.String(d.Get("route_response_selection_expression").(string)) + } + if d.HasChange("target") { + req.Target = aws.String(d.Get("target").(string)) + } + + log.Printf("[DEBUG] Updating API Gateway v2 route: %s", req) + _, err := conn.UpdateRoute(req) + + if err != nil { + return fmt.Errorf("error updating API Gateway v2 route (%s): %w", d.Id(), err) + } } return resourceAwsApiGatewayV2RouteRead(d, meta) @@ -214,11 +276,13 @@ func resourceAwsApiGatewayV2RouteDelete(d *schema.ResourceData, meta interface{} ApiId: aws.String(d.Get("api_id").(string)), RouteId: aws.String(d.Id()), }) - if isAWSErr(err, apigatewayv2.ErrCodeNotFoundException, "") { + + if tfawserr.ErrCodeEquals(err, apigatewayv2.ErrCodeNotFoundException) { return nil } + if err != nil { - return fmt.Errorf("error deleting API Gateway v2 route: %s", err) + return fmt.Errorf("error deleting API Gateway v2 route (%s): %w", d.Id(), err) } return nil @@ -252,3 +316,52 @@ func resourceAwsApiGatewayV2RouteImport(d *schema.ResourceData, meta interface{} return []*schema.ResourceData{d}, nil } + +func expandApiGatewayV2RouteRequestParameters(tfList []interface{}) map[string]*apigatewayv2.ParameterConstraints { + if len(tfList) == 0 { + return nil + } + + apiObjects := map[string]*apigatewayv2.ParameterConstraints{} + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := &apigatewayv2.ParameterConstraints{} + + if v, ok := tfMap["required"].(bool); ok { + apiObject.Required = aws.Bool(v) + } + + if v, ok := tfMap["request_parameter_key"].(string); ok && v != "" { + apiObjects[v] = apiObject + } + } + + return apiObjects +} + +func flattenApiGatewayV2RouteRequestParameters(apiObjects map[string]*apigatewayv2.ParameterConstraints) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for k, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, map[string]interface{}{ + "request_parameter_key": k, + "required": aws.BoolValue(apiObject.Required), + }) + } + + return tfList +} diff --git a/aws/resource_aws_apigatewayv2_route_response.go b/aws/resource_aws_apigatewayv2_route_response.go index 703d765db832..e1e65c23d841 100644 --- a/aws/resource_aws_apigatewayv2_route_response.go +++ b/aws/resource_aws_apigatewayv2_route_response.go @@ -60,7 +60,7 @@ func resourceAwsApiGatewayV2RouteResponseCreate(d *schema.ResourceData, meta int req.ModelSelectionExpression = aws.String(v.(string)) } if v, ok := d.GetOk("response_models"); ok { - req.ResponseModels = stringMapToPointers(v.(map[string]interface{})) + req.ResponseModels = expandStringMap(v.(map[string]interface{})) } log.Printf("[DEBUG] Creating API Gateway v2 route response: %s", req) @@ -112,7 +112,7 @@ func resourceAwsApiGatewayV2RouteResponseUpdate(d *schema.ResourceData, meta int req.ModelSelectionExpression = aws.String(d.Get("model_selection_expression").(string)) } if d.HasChange("response_models") { - req.ResponseModels = stringMapToPointers(d.Get("response_models").(map[string]interface{})) + req.ResponseModels = expandStringMap(d.Get("response_models").(map[string]interface{})) } if d.HasChange("route_response_key") { req.RouteResponseKey = aws.String(d.Get("route_response_key").(string)) diff --git a/aws/resource_aws_apigatewayv2_route_response_test.go b/aws/resource_aws_apigatewayv2_route_response_test.go index 2f35d9debc03..6a843c1b205d 100644 --- a/aws/resource_aws_apigatewayv2_route_response_test.go +++ b/aws/resource_aws_apigatewayv2_route_response_test.go @@ -21,11 +21,12 @@ func TestAccAWSAPIGatewayV2RouteResponse_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteResponseDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAPIGatewayV2RouteResponseConfig_basic(rName), + Config: testAccAWSAPIGatewayV2RouteResponseConfig_basicWebSocket(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAPIGatewayV2RouteResponseExists(resourceName, &apiId, &routeId, &v), resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), @@ -52,11 +53,12 @@ func TestAccAWSAPIGatewayV2RouteResponse_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteResponseDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAPIGatewayV2RouteResponseConfig_basic(rName), + Config: testAccAWSAPIGatewayV2RouteResponseConfig_basicWebSocket(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAPIGatewayV2RouteResponseExists(resourceName, &apiId, &routeId, &v), testAccCheckAWSAPIGatewayV2RouteResponseDisappears(&apiId, &routeId, &v), @@ -78,6 +80,7 @@ func TestAccAWSAPIGatewayV2RouteResponse_Model(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteResponseDestroy, Steps: []resource.TestStep{ @@ -185,18 +188,22 @@ func testAccAWSAPIGatewayV2RouteResponseImportStateIdFunc(resourceName string) r } } -func testAccAWSAPIGatewayV2RouteResponseConfig_basic(rName string) string { - return testAccAWSAPIGatewayV2RouteConfig_basic(rName) + ` +func testAccAWSAPIGatewayV2RouteResponseConfig_basicWebSocket(rName string) string { + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_basicWebSocket(rName), + ` resource "aws_apigatewayv2_route_response" "test" { api_id = aws_apigatewayv2_api.test.id route_id = aws_apigatewayv2_route.test.id route_response_key = "$default" } -` +`) } func testAccAWSAPIGatewayV2RouteResponseConfig_model(rName string) string { - return testAccAWSAPIGatewayV2RouteConfig_model(rName) + ` + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_model(rName), + ` resource "aws_apigatewayv2_route_response" "test" { api_id = aws_apigatewayv2_api.test.id route_id = aws_apigatewayv2_route.test.id @@ -208,5 +215,5 @@ resource "aws_apigatewayv2_route_response" "test" { "test" = aws_apigatewayv2_model.test.name } } -` +`) } diff --git a/aws/resource_aws_apigatewayv2_route_test.go b/aws/resource_aws_apigatewayv2_route_test.go index e1a136c535c2..4bea408bc1cf 100644 --- a/aws/resource_aws_apigatewayv2_route_test.go +++ b/aws/resource_aws_apigatewayv2_route_test.go @@ -20,11 +20,12 @@ func TestAccAWSAPIGatewayV2Route_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAPIGatewayV2RouteConfig_basic(rName), + Config: testAccAWSAPIGatewayV2RouteConfig_basicWebSocket(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), resource.TestCheckResourceAttr(resourceName, "api_key_required", "false"), @@ -33,6 +34,7 @@ func TestAccAWSAPIGatewayV2Route_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -56,11 +58,12 @@ func TestAccAWSAPIGatewayV2Route_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAPIGatewayV2RouteConfig_basic(rName), + Config: testAccAWSAPIGatewayV2RouteConfig_basicWebSocket(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), testAccCheckResourceDisappears(testAccProvider, resourceAwsApiGatewayV2Route(), resourceName), @@ -80,6 +83,7 @@ func TestAccAWSAPIGatewayV2Route_Authorizer(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -94,6 +98,7 @@ func TestAccAWSAPIGatewayV2Route_Authorizer(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$connect"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -116,6 +121,7 @@ func TestAccAWSAPIGatewayV2Route_Authorizer(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$connect"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -134,6 +140,7 @@ func TestAccAWSAPIGatewayV2Route_JwtAuthorization(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -148,6 +155,7 @@ func TestAccAWSAPIGatewayV2Route_JwtAuthorization(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "GET /test"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -170,6 +178,7 @@ func TestAccAWSAPIGatewayV2Route_JwtAuthorization(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "GET /test"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -189,6 +198,7 @@ func TestAccAWSAPIGatewayV2Route_Model(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -203,6 +213,7 @@ func TestAccAWSAPIGatewayV2Route_Model(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "1"), resource.TestCheckResourceAttrPair(resourceName, "request_models.test", modelResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -218,6 +229,88 @@ func TestAccAWSAPIGatewayV2Route_Model(t *testing.T) { }) } +func TestAccAWSAPIGatewayV2Route_RequestParameters(t *testing.T) { + var apiId string + var v apigatewayv2.GetRouteOutput + resourceName := "aws_apigatewayv2_route.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2RouteConfig_requestParameters(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), + resource.TestCheckResourceAttr(resourceName, "api_key_required", "false"), + resource.TestCheckResourceAttr(resourceName, "authorization_type", apigatewayv2.AuthorizationTypeNone), + resource.TestCheckResourceAttr(resourceName, "authorizer_id", ""), + resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "operation_name", ""), + resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "request_parameter.*", map[string]string{ + "request_parameter_key": "route.request.header.authorization", + "required": "true", + }), + resource.TestCheckResourceAttr(resourceName, "route_key", "$connect"), + resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "target", ""), + ), + }, + { + Config: testAccAWSAPIGatewayV2RouteConfig_requestParametersUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), + resource.TestCheckResourceAttr(resourceName, "api_key_required", "false"), + resource.TestCheckResourceAttr(resourceName, "authorization_type", apigatewayv2.AuthorizationTypeNone), + resource.TestCheckResourceAttr(resourceName, "authorizer_id", ""), + resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "operation_name", ""), + resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "request_parameter.*", map[string]string{ + "request_parameter_key": "route.request.header.authorization", + "required": "false", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "request_parameter.*", map[string]string{ + "request_parameter_key": "route.request.querystring.authToken", + "required": "true", + }), + resource.TestCheckResourceAttr(resourceName, "route_key", "$connect"), + resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "target", ""), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: testAccAWSAPIGatewayV2RouteImportStateIdFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAPIGatewayV2RouteConfig_noRequestParameters(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), + resource.TestCheckResourceAttr(resourceName, "api_key_required", "false"), + resource.TestCheckResourceAttr(resourceName, "authorization_type", apigatewayv2.AuthorizationTypeNone), + resource.TestCheckResourceAttr(resourceName, "authorizer_id", ""), + resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "operation_name", ""), + resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), + resource.TestCheckResourceAttr(resourceName, "route_key", "$connect"), + resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), + resource.TestCheckResourceAttr(resourceName, "target", ""), + ), + }, + }, + }) +} + func TestAccAWSAPIGatewayV2Route_SimpleAttributes(t *testing.T) { var apiId string var v apigatewayv2.GetRouteOutput @@ -226,6 +319,7 @@ func TestAccAWSAPIGatewayV2Route_SimpleAttributes(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -239,13 +333,14 @@ func TestAccAWSAPIGatewayV2Route_SimpleAttributes(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", "GET"), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", "$default"), resource.TestCheckResourceAttr(resourceName, "target", ""), ), }, { - Config: testAccAWSAPIGatewayV2RouteConfig_basic(rName), + Config: testAccAWSAPIGatewayV2RouteConfig_basicWebSocket(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSAPIGatewayV2RouteExists(resourceName, &apiId, &v), resource.TestCheckResourceAttr(resourceName, "api_key_required", "false"), @@ -254,7 +349,7 @@ func TestAccAWSAPIGatewayV2Route_SimpleAttributes(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), - resource.TestCheckResourceAttr(resourceName, "request_parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -270,6 +365,7 @@ func TestAccAWSAPIGatewayV2Route_SimpleAttributes(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", "GET"), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", "$default"), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -294,6 +390,7 @@ func TestAccAWSAPIGatewayV2Route_Target(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -307,6 +404,7 @@ func TestAccAWSAPIGatewayV2Route_Target(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "$default"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), testAccCheckAWSAPIGatewayV2RouteTarget(resourceName, integrationResourceName), @@ -330,6 +428,7 @@ func TestAccAWSAPIGatewayV2Route_UpdateRouteKey(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2RouteDestroy, Steps: []resource.TestStep{ @@ -343,6 +442,7 @@ func TestAccAWSAPIGatewayV2Route_UpdateRouteKey(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "GET /path"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -358,6 +458,7 @@ func TestAccAWSAPIGatewayV2Route_UpdateRouteKey(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "model_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "operation_name", ""), resource.TestCheckResourceAttr(resourceName, "request_models.%", "0"), + resource.TestCheckResourceAttr(resourceName, "request_parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "route_key", "POST /new/path"), resource.TestCheckResourceAttr(resourceName, "route_response_selection_expression", ""), resource.TestCheckResourceAttr(resourceName, "target", ""), @@ -468,17 +569,21 @@ resource "aws_apigatewayv2_api" "test" { `, rName) } -func testAccAWSAPIGatewayV2RouteConfig_basic(rName string) string { - return testAccAWSAPIGatewayV2RouteConfig_apiWebSocket(rName) + ` +func testAccAWSAPIGatewayV2RouteConfig_basicWebSocket(rName string) string { + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_apiWebSocket(rName), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "$default" } -` +`) } func testAccAWSAPIGatewayV2RouteConfig_authorizer(rName string) string { - return testAccAWSAPIGatewayV2AuthorizerConfig_basic(rName) + ` + return composeConfig( + testAccAWSAPIGatewayV2AuthorizerConfig_basic(rName), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "$connect" @@ -486,22 +591,26 @@ resource "aws_apigatewayv2_route" "test" { authorization_type = "CUSTOM" authorizer_id = aws_apigatewayv2_authorizer.test.id } -` +`) } func testAccAWSAPIGatewayV2RouteConfig_authorizerUpdated(rName string) string { - return testAccAWSAPIGatewayV2AuthorizerConfig_basic(rName) + ` + return composeConfig( + testAccAWSAPIGatewayV2AuthorizerConfig_basic(rName), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "$connect" authorization_type = "AWS_IAM" } -` +`) } func testAccAWSAPIGatewayV2RouteConfig_jwtAuthorization(rName string) string { - return testAccAWSAPIGatewayV2AuthorizerConfig_jwt(rName) + ` + return composeConfig( + testAccAWSAPIGatewayV2AuthorizerConfig_jwt(rName), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "GET /test" @@ -511,11 +620,13 @@ resource "aws_apigatewayv2_route" "test" { authorization_scopes = ["user.id", "user.email"] } -` +`) } func testAccAWSAPIGatewayV2RouteConfig_jwtAuthorizationUpdated(rName string) string { - return testAccAWSAPIGatewayV2AuthorizerConfig_jwt(rName) + ` + return composeConfig( + testAccAWSAPIGatewayV2AuthorizerConfig_jwt(rName), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "GET /test" @@ -525,7 +636,7 @@ resource "aws_apigatewayv2_route" "test" { authorization_scopes = ["user.email"] } -` +`) } func testAccAWSAPIGatewayV2RouteConfig_model(rName string) string { @@ -542,7 +653,9 @@ func testAccAWSAPIGatewayV2RouteConfig_model(rName string) string { } ` - return testAccAWSAPIGatewayV2ModelConfig_basic(rName, schema) + ` + return composeConfig( + testAccAWSAPIGatewayV2ModelConfig_basic(rName, schema), + ` resource "aws_apigatewayv2_route" "test" { api_id = aws_apigatewayv2_api.test.id route_key = "$default" @@ -553,7 +666,55 @@ resource "aws_apigatewayv2_route" "test" { "test" = aws_apigatewayv2_model.test.name } } -` +`) +} + +func testAccAWSAPIGatewayV2RouteConfig_noRequestParameters(rName string) string { + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_apiWebSocket(rName), + ` +resource "aws_apigatewayv2_route" "test" { + api_id = aws_apigatewayv2_api.test.id + route_key = "$connect" +} +`) +} + +func testAccAWSAPIGatewayV2RouteConfig_requestParameters(rName string) string { + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_apiWebSocket(rName), + ` +resource "aws_apigatewayv2_route" "test" { + api_id = aws_apigatewayv2_api.test.id + route_key = "$connect" + + request_parameter { + request_parameter_key = "route.request.header.authorization" + required = true + } +} +`) +} + +func testAccAWSAPIGatewayV2RouteConfig_requestParametersUpdated(rName string) string { + return composeConfig( + testAccAWSAPIGatewayV2RouteConfig_apiWebSocket(rName), + ` +resource "aws_apigatewayv2_route" "test" { + api_id = aws_apigatewayv2_api.test.id + route_key = "$connect" + + request_parameter { + request_parameter_key = "route.request.header.authorization" + required = false + } + + request_parameter { + request_parameter_key = "route.request.querystring.authToken" + required = true + } +} +`) } func testAccAWSAPIGatewayV2RouteConfig_routeKey(rName, routeKey string) string { diff --git a/aws/resource_aws_apigatewayv2_stage.go b/aws/resource_aws_apigatewayv2_stage.go index bb3ecd221129..38f4d5cc9255 100644 --- a/aws/resource_aws_apigatewayv2_stage.go +++ b/aws/resource_aws_apigatewayv2_stage.go @@ -174,13 +174,18 @@ func resourceAwsApiGatewayV2Stage() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayV2StageCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) apiId := d.Get("api_id").(string) @@ -197,7 +202,7 @@ func resourceAwsApiGatewayV2StageCreate(d *schema.ResourceData, meta interface{} ApiId: aws.String(apiId), AutoDeploy: aws.Bool(d.Get("auto_deploy").(bool)), StageName: aws.String(d.Get("name").(string)), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().Apigatewayv2Tags(), + Tags: tags.IgnoreAws().Apigatewayv2Tags(), } if v, ok := d.GetOk("access_log_settings"); ok { req.AccessLogSettings = expandApiGatewayV2AccessLogSettings(v.([]interface{})) @@ -218,7 +223,7 @@ func resourceAwsApiGatewayV2StageCreate(d *schema.ResourceData, meta interface{} req.RouteSettings = expandApiGatewayV2RouteSettings(v.(*schema.Set).List(), protocolType) } if v, ok := d.GetOk("stage_variables"); ok { - req.StageVariables = stringMapToPointers(v.(map[string]interface{})) + req.StageVariables = expandStringMap(v.(map[string]interface{})) } log.Printf("[DEBUG] Creating API Gateway v2 stage: %s", req) @@ -234,6 +239,7 @@ func resourceAwsApiGatewayV2StageCreate(d *schema.ResourceData, meta interface{} func resourceAwsApiGatewayV2StageRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig apiId := d.Get("api_id").(string) @@ -288,8 +294,16 @@ func resourceAwsApiGatewayV2StageRead(d *schema.ResourceData, meta interface{}) if err != nil { return fmt.Errorf("error setting stage_variables: %s", err) } - if err := d.Set("tags", keyvaluetags.Apigatewayv2KeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + + tags := keyvaluetags.Apigatewayv2KeyValueTags(resp.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } apiOutput, err := conn.GetApi(&apigatewayv2.GetApiInput{ @@ -397,8 +411,8 @@ func resourceAwsApiGatewayV2StageUpdate(d *schema.ResourceData, meta interface{} } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating API Gateway v2 stage (%s) tags: %s", d.Id(), err) } diff --git a/aws/resource_aws_apigatewayv2_stage_test.go b/aws/resource_aws_apigatewayv2_stage_test.go index 5e725e00e6b9..a8fe7785224e 100644 --- a/aws/resource_aws_apigatewayv2_stage_test.go +++ b/aws/resource_aws_apigatewayv2_stage_test.go @@ -20,6 +20,7 @@ func TestAccAWSAPIGatewayV2Stage_basicWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -65,6 +66,7 @@ func TestAccAWSAPIGatewayV2Stage_basicHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -110,6 +112,7 @@ func TestAccAWSAPIGatewayV2Stage_defaultHttpStage(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -155,6 +158,7 @@ func TestAccAWSAPIGatewayV2Stage_autoDeployHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -226,6 +230,7 @@ func TestAccAWSAPIGatewayV2Stage_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -250,6 +255,7 @@ func TestAccAWSAPIGatewayV2Stage_AccessLogSettings(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAPIGatewayAccountCloudWatchRoleArn(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -321,6 +327,7 @@ func TestAccAWSAPIGatewayV2Stage_ClientCertificateIdAndDescription(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -390,6 +397,7 @@ func TestAccAWSAPIGatewayV2Stage_DefaultRouteSettingsWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAPIGatewayAccountCloudWatchRoleArn(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -480,6 +488,7 @@ func TestAccAWSAPIGatewayV2Stage_DefaultRouteSettingsHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -571,6 +580,7 @@ func TestAccAWSAPIGatewayV2Stage_Deployment(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -616,6 +626,7 @@ func TestAccAWSAPIGatewayV2Stage_RouteSettingsWebSocket(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAPIGatewayAccountCloudWatchRoleArn(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -743,6 +754,7 @@ func TestAccAWSAPIGatewayV2Stage_RouteSettingsHttp(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -847,6 +859,7 @@ func TestAccAWSAPIGatewayV2Stage_RouteSettingsHttp_WithRoute(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -930,6 +943,7 @@ func TestAccAWSAPIGatewayV2Stage_StageVariables(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ @@ -998,6 +1012,7 @@ func TestAccAWSAPIGatewayV2Stage_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2StageDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apigatewayv2_vpc_link.go b/aws/resource_aws_apigatewayv2_vpc_link.go index 3bafee3ab64a..455524ba5c49 100644 --- a/aws/resource_aws_apigatewayv2_vpc_link.go +++ b/aws/resource_aws_apigatewayv2_vpc_link.go @@ -47,19 +47,24 @@ func resourceAwsApiGatewayV2VpcLink() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsApiGatewayV2VpcLinkCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &apigatewayv2.CreateVpcLinkInput{ Name: aws.String(d.Get("name").(string)), SecurityGroupIds: expandStringSet(d.Get("security_group_ids").(*schema.Set)), SubnetIds: expandStringSet(d.Get("subnet_ids").(*schema.Set)), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().Apigatewayv2Tags(), + Tags: tags.IgnoreAws().Apigatewayv2Tags(), } log.Printf("[DEBUG] Creating API Gateway v2 VPC Link: %s", req) @@ -79,6 +84,7 @@ func resourceAwsApiGatewayV2VpcLinkCreate(d *schema.ResourceData, meta interface func resourceAwsApiGatewayV2VpcLinkRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig outputRaw, _, err := waiter.VpcLinkStatus(conn, d.Id())() @@ -106,8 +112,16 @@ func resourceAwsApiGatewayV2VpcLinkRead(d *schema.ResourceData, meta interface{} if err := d.Set("subnet_ids", flattenStringSet(output.SubnetIds)); err != nil { return fmt.Errorf("error setting subnet_ids: %s", err) } - if err := d.Set("tags", keyvaluetags.Apigatewayv2KeyValueTags(output.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + + tags := keyvaluetags.Apigatewayv2KeyValueTags(output.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -129,8 +143,8 @@ func resourceAwsApiGatewayV2VpcLinkUpdate(d *schema.ResourceData, meta interface } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating API Gateway v2 VPC Link (%s) tags: %s", d.Id(), err) } diff --git a/aws/resource_aws_apigatewayv2_vpc_link_test.go b/aws/resource_aws_apigatewayv2_vpc_link_test.go index 2aee38728d03..dc36f90dcc5b 100644 --- a/aws/resource_aws_apigatewayv2_vpc_link_test.go +++ b/aws/resource_aws_apigatewayv2_vpc_link_test.go @@ -85,6 +85,7 @@ func TestAccAWSAPIGatewayV2VpcLink_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2VpcLinkDestroy, Steps: []resource.TestStep{ @@ -126,6 +127,7 @@ func TestAccAWSAPIGatewayV2VpcLink_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2VpcLinkDestroy, Steps: []resource.TestStep{ @@ -148,6 +150,7 @@ func TestAccAWSAPIGatewayV2VpcLink_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, apigatewayv2.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAPIGatewayV2VpcLinkDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_app_cookie_stickiness_policy.go b/aws/resource_aws_app_cookie_stickiness_policy.go index 71be5ef28785..846e83040425 100644 --- a/aws/resource_aws_app_cookie_stickiness_policy.go +++ b/aws/resource_aws_app_cookie_stickiness_policy.go @@ -132,7 +132,7 @@ func resourceAwsAppCookieStickinessPolicyRead(d *schema.ResourceData, meta inter // cookie expiration, in these descriptions. policyDesc := getResp.PolicyDescriptions[0] cookieAttr := policyDesc.PolicyAttributeDescriptions[0] - if *cookieAttr.AttributeName != "CookieName" { + if aws.StringValue(cookieAttr.AttributeName) != "CookieName" { return fmt.Errorf("Unable to find cookie Name.") } @@ -168,12 +168,12 @@ func resourceAwsELBSticknessPolicyAssigned(policyName, lbName, lbPort string, el lb := describeResp.LoadBalancerDescriptions[0] assigned := false for _, listener := range lb.ListenerDescriptions { - if lbPort != strconv.Itoa(int(*listener.Listener.LoadBalancerPort)) { + if listener == nil || listener.Listener == nil || lbPort != strconv.Itoa(int(aws.Int64Value(listener.Listener.LoadBalancerPort))) { continue } for _, name := range listener.PolicyNames { - if policyName == *name { + if policyName == aws.StringValue(name) { assigned = true break } diff --git a/aws/resource_aws_app_cookie_stickiness_policy_test.go b/aws/resource_aws_app_cookie_stickiness_policy_test.go index 146d55858cae..38460d4b50fc 100644 --- a/aws/resource_aws_app_cookie_stickiness_policy_test.go +++ b/aws/resource_aws_app_cookie_stickiness_policy_test.go @@ -17,6 +17,7 @@ func TestAccAWSAppCookieStickinessPolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppCookieStickinessPolicyDestroy, Steps: []resource.TestStep{ @@ -54,6 +55,7 @@ func TestAccAWSAppCookieStickinessPolicy_disappears_ELB(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppCookieStickinessPolicyDestroy, Steps: []resource.TestStep{ @@ -132,6 +134,7 @@ func TestAccAWSAppCookieStickinessPolicy_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elb.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppCookieStickinessPolicyDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appautoscaling_policy.go b/aws/resource_aws_appautoscaling_policy.go index fdcb9d5e7006..4142fcd6e578 100644 --- a/aws/resource_aws_appautoscaling_policy.go +++ b/aws/resource_aws_appautoscaling_policy.go @@ -12,6 +12,7 @@ import ( "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" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) func resourceAwsAppautoscalingPolicy() *schema.Resource { @@ -209,7 +210,7 @@ func resourceAwsAppautoscalingPolicyCreate(d *schema.ResourceData, meta interfac log.Printf("[DEBUG] ApplicationAutoScaling PutScalingPolicy: %#v", params) var resp *applicationautoscaling.PutScalingPolicyOutput - err = resource.Retry(2*time.Minute, func() *resource.RetryError { + err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { var err error resp, err = conn.PutScalingPolicy(¶ms) if err != nil { @@ -301,7 +302,7 @@ func resourceAwsAppautoscalingPolicyUpdate(d *schema.ResourceData, meta interfac } log.Printf("[DEBUG] Application Autoscaling Update Scaling Policy: %#v", params) - err := resource.Retry(2*time.Minute, func() *resource.RetryError { + err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { _, err := conn.PutScalingPolicy(¶ms) if err != nil { if isAWSErr(err, applicationautoscaling.ErrCodeFailedResourceAccessException, "") { @@ -341,7 +342,7 @@ func resourceAwsAppautoscalingPolicyDelete(d *schema.ResourceData, meta interfac ServiceNamespace: aws.String(d.Get("service_namespace").(string)), } log.Printf("[DEBUG] Deleting Application AutoScaling Policy opts: %#v", params) - err = resource.Retry(2*time.Minute, func() *resource.RetryError { + err = resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { _, err = conn.DeleteScalingPolicy(¶ms) if isAWSErr(err, applicationautoscaling.ErrCodeFailedResourceAccessException, "") { @@ -705,22 +706,29 @@ func flattenTargetTrackingScalingPolicyConfiguration(cfg *applicationautoscaling } m := make(map[string]interface{}) - m["target_value"] = *cfg.TargetValue - if cfg.DisableScaleIn != nil { - m["disable_scale_in"] = *cfg.DisableScaleIn + if v := cfg.CustomizedMetricSpecification; v != nil { + m["customized_metric_specification"] = flattenCustomizedMetricSpecification(v) } - if cfg.ScaleInCooldown != nil { - m["scale_in_cooldown"] = *cfg.ScaleInCooldown + + if v := cfg.DisableScaleIn; v != nil { + m["disable_scale_in"] = aws.BoolValue(v) } - if cfg.ScaleOutCooldown != nil { - m["scale_out_cooldown"] = *cfg.ScaleOutCooldown + + if v := cfg.PredefinedMetricSpecification; v != nil { + m["predefined_metric_specification"] = flattenPredefinedMetricSpecification(v) } - if cfg.CustomizedMetricSpecification != nil { - m["customized_metric_specification"] = flattenCustomizedMetricSpecification(cfg.CustomizedMetricSpecification) + + if v := cfg.ScaleInCooldown; v != nil { + m["scale_in_cooldown"] = aws.Int64Value(v) } - if cfg.PredefinedMetricSpecification != nil { - m["predefined_metric_specification"] = flattenPredefinedMetricSpecification(cfg.PredefinedMetricSpecification) + + if v := cfg.ScaleOutCooldown; v != nil { + m["scale_out_cooldown"] = aws.Int64Value(v) + } + + if v := cfg.TargetValue; v != nil { + m["target_value"] = aws.Float64Value(v) } return []interface{}{m} @@ -731,29 +739,49 @@ func flattenCustomizedMetricSpecification(cfg *applicationautoscaling.Customized return []interface{}{} } - m := map[string]interface{}{ - "metric_name": *cfg.MetricName, - "namespace": *cfg.Namespace, - "statistic": *cfg.Statistic, - } + m := map[string]interface{}{} - if len(cfg.Dimensions) > 0 { + if v := cfg.Dimensions; len(v) > 0 { m["dimensions"] = flattenMetricDimensions(cfg.Dimensions) } - if cfg.Unit != nil { - m["unit"] = *cfg.Unit + if v := cfg.MetricName; v != nil { + m["metric_name"] = aws.StringValue(v) + } + + if v := cfg.Namespace; v != nil { + m["namespace"] = aws.StringValue(v) } + + if v := cfg.Statistic; v != nil { + m["statistic"] = aws.StringValue(v) + } + + if v := cfg.Unit; v != nil { + m["unit"] = aws.StringValue(v) + } + return []interface{}{m} } func flattenMetricDimensions(ds []*applicationautoscaling.MetricDimension) []interface{} { l := make([]interface{}, len(ds)) for i, d := range ds { - l[i] = map[string]interface{}{ - "name": *d.Name, - "value": *d.Value, + if ds == nil { + continue + } + + m := map[string]interface{}{} + + if v := d.Name; v != nil { + m["name"] = aws.StringValue(v) + } + + if v := d.Value; v != nil { + m["value"] = aws.StringValue(v) } + + l[i] = m } return l } @@ -762,11 +790,16 @@ func flattenPredefinedMetricSpecification(cfg *applicationautoscaling.Predefined if cfg == nil { return []interface{}{} } - m := map[string]interface{}{ - "predefined_metric_type": *cfg.PredefinedMetricType, + + m := map[string]interface{}{} + + if v := cfg.PredefinedMetricType; v != nil { + m["predefined_metric_type"] = aws.StringValue(v) } - if cfg.ResourceLabel != nil { - m["resource_label"] = *cfg.ResourceLabel + + if v := cfg.ResourceLabel; v != nil { + m["resource_label"] = aws.StringValue(v) } + return []interface{}{m} } diff --git a/aws/resource_aws_appautoscaling_policy_test.go b/aws/resource_aws_appautoscaling_policy_test.go index 98c76ee2f656..6d038ab12631 100644 --- a/aws/resource_aws_appautoscaling_policy_test.go +++ b/aws/resource_aws_appautoscaling_policy_test.go @@ -85,6 +85,7 @@ func TestAccAWSAppautoScalingPolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -124,6 +125,7 @@ func TestAccAWSAppautoScalingPolicy_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -147,6 +149,7 @@ func TestAccAWSAppautoScalingPolicy_scaleOutAndIn(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -227,6 +230,7 @@ func TestAccAWSAppautoScalingPolicy_spotFleetRequest(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -258,6 +262,7 @@ func TestAccAWSAppautoScalingPolicy_dynamodb_table(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -289,6 +294,7 @@ func TestAccAWSAppautoScalingPolicy_dynamodb_index(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -322,6 +328,7 @@ func TestAccAWSAppautoScalingPolicy_multiplePoliciesSameName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -354,6 +361,7 @@ func TestAccAWSAppautoScalingPolicy_multiplePoliciesSameResource(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -398,6 +406,7 @@ func TestAccAWSAppautoScalingPolicy_ResourceId_ForceNew(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAppautoscalingPolicyDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appautoscaling_scheduled_action.go b/aws/resource_aws_appautoscaling_scheduled_action.go index cc1f79d5a8c2..33ff3f8ffdeb 100644 --- a/aws/resource_aws_appautoscaling_scheduled_action.go +++ b/aws/resource_aws_appautoscaling_scheduled_action.go @@ -3,20 +3,25 @@ package aws import ( "fmt" "log" + "strconv" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/applicationautoscaling" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/applicationautoscaling/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) -const awsAppautoscalingScheduleTimeLayout = "2006-01-02T15:04:05Z" - func resourceAwsAppautoscalingScheduledAction() *schema.Resource { return &schema.Resource{ Create: resourceAwsAppautoscalingScheduledActionPut, Read: resourceAwsAppautoscalingScheduledActionRead, + Update: resourceAwsAppautoscalingScheduledActionPut, Delete: resourceAwsAppautoscalingScheduledActionDelete, Schema: map[string]*schema.Schema{ @@ -37,43 +42,58 @@ func resourceAwsAppautoscalingScheduledAction() *schema.Resource { }, "scalable_dimension": { Type: schema.TypeString, - Optional: true, + Required: true, ForceNew: true, }, "scalable_target_action": { Type: schema.TypeList, - Optional: true, - ForceNew: true, + Required: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "max_capacity": { - Type: schema.TypeInt, - Optional: true, - ForceNew: true, + Type: nullable.TypeNullableInt, + Optional: true, + ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0), + AtLeastOneOf: []string{ + "scalable_target_action.0.max_capacity", + "scalable_target_action.0.min_capacity", + }, }, "min_capacity": { - Type: schema.TypeInt, - Optional: true, - ForceNew: true, + Type: nullable.TypeNullableInt, + Optional: true, + ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0), + AtLeastOneOf: []string{ + "scalable_target_action.0.max_capacity", + "scalable_target_action.0.min_capacity", + }, }, }, }, }, "schedule": { Type: schema.TypeString, - Optional: true, - ForceNew: true, + Required: true, }, + // The AWS API normalizes start_time and end_time to UTC. Uses + // suppressEquivalentTime to allow any timezone to be used. "start_time": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + DiffSuppressFunc: suppressEquivalentTime, }, "end_time": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + DiffSuppressFunc: suppressEquivalentTime, + }, + "timezone": { Type: schema.TypeString, Optional: true, - ForceNew: true, + Default: "UTC", }, "arn": { Type: schema.TypeString, @@ -90,98 +110,118 @@ func resourceAwsAppautoscalingScheduledActionPut(d *schema.ResourceData, meta in ScheduledActionName: aws.String(d.Get("name").(string)), ServiceNamespace: aws.String(d.Get("service_namespace").(string)), ResourceId: aws.String(d.Get("resource_id").(string)), + ScalableDimension: aws.String(d.Get("scalable_dimension").(string)), } - if v, ok := d.GetOk("scalable_dimension"); ok { - input.ScalableDimension = aws.String(v.(string)) - } - if v, ok := d.GetOk("schedule"); ok { - input.Schedule = aws.String(v.(string)) + + needsPut := true + if d.IsNewResource() { + appautoscalingScheduledActionPopulateInputForCreate(input, d) + } else { + needsPut = appautoscalingScheduledActionPopulateInputForUpdate(input, d) } - if v, ok := d.GetOk("scalable_target_action"); ok { - sta := &applicationautoscaling.ScalableTargetAction{} - raw := v.([]interface{})[0].(map[string]interface{}) - if max, ok := raw["max_capacity"]; ok { - sta.MaxCapacity = aws.Int64(int64(max.(int))) + + if needsPut { + err := resource.Retry(5*time.Minute, func() *resource.RetryError { + _, err := conn.PutScheduledAction(input) + if err != nil { + if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if isResourceTimeoutError(err) { + _, err = conn.PutScheduledAction(input) } - if min, ok := raw["min_capacity"]; ok { - sta.MinCapacity = aws.Int64(int64(min.(int))) + if err != nil { + return fmt.Errorf("error putting Application Auto Scaling scheduled action: %w", err) + } + + if d.IsNewResource() { + d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string)) } - input.ScalableTargetAction = sta } + + return resourceAwsAppautoscalingScheduledActionRead(d, meta) +} + +func appautoscalingScheduledActionPopulateInputForCreate(input *applicationautoscaling.PutScheduledActionInput, d *schema.ResourceData) { + input.Schedule = aws.String(d.Get("schedule").(string)) + input.ScalableTargetAction = expandScalableTargetAction(d.Get("scalable_target_action").([]interface{})) + input.Timezone = aws.String(d.Get("timezone").(string)) + if v, ok := d.GetOk("start_time"); ok { - t, err := time.Parse(awsAppautoscalingScheduleTimeLayout, v.(string)) - if err != nil { - return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action Start Time: %s", err.Error()) - } + t, _ := time.Parse(time.RFC3339, v.(string)) input.StartTime = aws.Time(t) } if v, ok := d.GetOk("end_time"); ok { - t, err := time.Parse(awsAppautoscalingScheduleTimeLayout, v.(string)) - if err != nil { - return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action End Time: %s", err.Error()) - } + t, _ := time.Parse(time.RFC3339, v.(string)) input.EndTime = aws.Time(t) } +} - err := resource.Retry(5*time.Minute, func() *resource.RetryError { - _, err := conn.PutScheduledAction(input) - if err != nil { - if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") { - return resource.RetryableError(err) - } - return resource.NonRetryableError(err) - } - return nil - }) - if isResourceTimeoutError(err) { - _, err = conn.PutScheduledAction(input) +func appautoscalingScheduledActionPopulateInputForUpdate(input *applicationautoscaling.PutScheduledActionInput, d *schema.ResourceData) bool { + hasChange := false + + if d.HasChange("schedule") { + input.Schedule = aws.String(d.Get("schedule").(string)) + hasChange = true } - if err != nil { - return fmt.Errorf("Error putting scheduled action: %s", err) + if d.HasChange("scalable_target_action") { + input.ScalableTargetAction = expandScalableTargetAction(d.Get("scalable_target_action").([]interface{})) + hasChange = true } - d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string)) - return resourceAwsAppautoscalingScheduledActionRead(d, meta) + if d.HasChange("timezone") { + input.Timezone = aws.String(d.Get("timezone").(string)) + hasChange = true + } + + if d.HasChange("start_time") { + if v, ok := d.GetOk("start_time"); ok { + t, _ := time.Parse(time.RFC3339, v.(string)) + input.StartTime = aws.Time(t) + hasChange = true + } + } + if d.HasChange("end_time") { + if v, ok := d.GetOk("end_time"); ok { + t, _ := time.Parse(time.RFC3339, v.(string)) + input.EndTime = aws.Time(t) + hasChange = true + } + } + + return hasChange } func resourceAwsAppautoscalingScheduledActionRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appautoscalingconn - saName := d.Get("name").(string) - input := &applicationautoscaling.DescribeScheduledActionsInput{ - ResourceId: aws.String(d.Get("resource_id").(string)), - ScheduledActionNames: []*string{aws.String(saName)}, - ServiceNamespace: aws.String(d.Get("service_namespace").(string)), + scheduledAction, err := finder.ScheduledAction(conn, d.Get("name").(string), d.Get("service_namespace").(string), d.Get("resource_id").(string)) + if tfresource.NotFound(err) { + log.Printf("[WARN] Application Auto Scaling Scheduled Action (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil } - resp, err := conn.DescribeScheduledActions(input) if err != nil { return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): %w", d.Id(), err) } - var scheduledAction *applicationautoscaling.ScheduledAction - - if resp == nil { - return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): empty response", d.Id()) + if err := d.Set("scalable_target_action", flattenScalableTargetAction(scheduledAction.ScalableTargetAction)); err != nil { + return fmt.Errorf("error setting scalable_target_action: %w", err) } - for _, sa := range resp.ScheduledActions { - if sa == nil { - continue - } - - if aws.StringValue(sa.ScheduledActionName) == saName { - scheduledAction = sa - break - } + d.Set("schedule", scheduledAction.Schedule) + if scheduledAction.StartTime != nil { + d.Set("start_time", scheduledAction.StartTime.Format(time.RFC3339)) } - - if scheduledAction == nil { - log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil + if scheduledAction.EndTime != nil { + d.Set("end_time", scheduledAction.EndTime.Format(time.RFC3339)) } - + d.Set("timezone", scheduledAction.Timezone) d.Set("arn", scheduledAction.ScheduledActionARN) return nil @@ -200,8 +240,8 @@ func resourceAwsAppautoscalingScheduledActionDelete(d *schema.ResourceData, meta } _, err := conn.DeleteScheduledAction(input) if err != nil { - if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") { - log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) already gone, removing from state", d.Id()) + if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) { + log.Printf("[WARN] Application Auto Scaling scheduled action (%s) not found, removing from state", d.Id()) return nil } return err @@ -209,3 +249,42 @@ func resourceAwsAppautoscalingScheduledActionDelete(d *schema.ResourceData, meta return nil } + +func expandScalableTargetAction(l []interface{}) *applicationautoscaling.ScalableTargetAction { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + result := &applicationautoscaling.ScalableTargetAction{} + + if v, ok := m["max_capacity"]; ok { + if v, null, _ := nullable.Int(v.(string)).Value(); !null { + result.MaxCapacity = aws.Int64(v) + } + } + if v, ok := m["min_capacity"]; ok { + if v, null, _ := nullable.Int(v.(string)).Value(); !null { + result.MinCapacity = aws.Int64(v) + } + } + + return result +} + +func flattenScalableTargetAction(cfg *applicationautoscaling.ScalableTargetAction) []interface{} { + if cfg == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + if cfg.MaxCapacity != nil { + m["max_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MaxCapacity), 10) + } + if cfg.MinCapacity != nil { + m["min_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MinCapacity), 10) + } + + return []interface{}{m} +} diff --git a/aws/resource_aws_appautoscaling_scheduled_action_test.go b/aws/resource_aws_appautoscaling_scheduled_action_test.go index d7acf82f1119..dc3750652277 100644 --- a/aws/resource_aws_appautoscaling_scheduled_action_test.go +++ b/aws/resource_aws_appautoscaling_scheduled_action_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "regexp" "testing" "time" @@ -10,19 +11,59 @@ import ( "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/terraform-providers/terraform-provider-aws/aws/internal/service/applicationautoscaling/finder" ) -func TestAccAWSAppautoscalingScheduledAction_dynamo(t *testing.T) { - ts := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") +func TestAccAWSAppautoscalingScheduledAction_DynamoDB(t *testing.T) { + var sa1, sa2 applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + schedule1 := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + schedule2 := time.Now().AddDate(0, 0, 2).Format("2006-01-02T15:04:05") + updatedTimezone := "Pacific/Tahiti" + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, Steps: []resource.TestStep{ { - Config: testAccAppautoscalingScheduledActionConfig_DynamoDB(acctest.RandString(5), ts), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsAppautoscalingScheduledActionExists("aws_appautoscaling_scheduled_action.hoge"), + Config: testAccAppautoscalingScheduledActionConfig_DynamoDB(rName, schedule1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa1), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", schedule1)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_DynamoDB_Updated(rName, schedule2, updatedTimezone), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa2), + testAccCheckAppautoscalingScheduledActionNotRecreated(&sa1, &sa2), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", schedule2)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "9"), + resource.TestCheckResourceAttr(resourceName, "timezone", updatedTimezone), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), ), }, }, @@ -30,16 +71,32 @@ func TestAccAWSAppautoscalingScheduledAction_dynamo(t *testing.T) { } func TestAccAWSAppautoscalingScheduledAction_ECS(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") ts := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, Steps: []resource.TestStep{ { - Config: testAccAppautoscalingScheduledActionConfig_ECS(acctest.RandString(5), ts), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsAppautoscalingScheduledActionExists("aws_appautoscaling_scheduled_action.hoge"), + Config: testAccAppautoscalingScheduledActionConfig_ECS(rName, ts), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", ts)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), ), }, }, @@ -47,16 +104,32 @@ func TestAccAWSAppautoscalingScheduledAction_ECS(t *testing.T) { } func TestAccAWSAppautoscalingScheduledAction_EMR(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") ts := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, Steps: []resource.TestStep{ { - Config: testAccAppautoscalingScheduledActionConfig_EMR(acctest.RandString(5), ts), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsAppautoscalingScheduledActionExists("aws_appautoscaling_scheduled_action.hoge"), + Config: testAccAppautoscalingScheduledActionConfig_EMR(rName, ts), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", ts)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), ), }, }, @@ -64,20 +137,22 @@ func TestAccAWSAppautoscalingScheduledAction_EMR(t *testing.T) { } func TestAccAWSAppautoscalingScheduledAction_Name_Duplicate(t *testing.T) { + var sa1, sa2 applicationautoscaling.ScheduledAction resourceName := "aws_appautoscaling_scheduled_action.test" resourceName2 := "aws_appautoscaling_scheduled_action.test2" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, Steps: []resource.TestStep{ { Config: testAccAppautoscalingScheduledActionConfig_Name_Duplicate(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsAppautoscalingScheduledActionExists(resourceName), - testAccCheckAwsAppautoscalingScheduledActionExists(resourceName2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa1), + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName2, &sa2), ), }, }, @@ -85,18 +160,388 @@ func TestAccAWSAppautoscalingScheduledAction_Name_Duplicate(t *testing.T) { } func TestAccAWSAppautoscalingScheduledAction_SpotFleet(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") ts := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_SpotFleet(rName, ts, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", ts)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "3"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_AtExpression_Timezone(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + ts := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + at := fmt.Sprintf("at(%s)", ts) + timezone := "Pacific/Tahiti" + startTime := time.Now().AddDate(0, 0, 2).Format("2006-01-02T15:04:05Z") + endTime := time.Now().AddDate(0, 0, 8).Format("2006-01-02T15:04:05Z") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, Steps: []resource.TestStep{ { - Config: testAccAppautoscalingScheduledActionConfig_SpotFleet(acctest.RandString(5), ts, validUntil), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsAppautoscalingScheduledActionExists("aws_appautoscaling_scheduled_action.hoge"), + Config: testAccAppautoscalingScheduledActionConfig_ScheduleWithTimezone(rName, at, timezone, startTime, endTime), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", at), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", timezone), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckResourceAttr(resourceName, "start_time", startTime), + resource.TestCheckResourceAttr(resourceName, "end_time", endTime), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_CronExpression_basic(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + cron := "cron(0 17 * * ? *)" + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_Schedule(rName, cron), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", cron), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_CronExpression_Timezone(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + cron := "cron(0 17 * * ? *)" + timezone := "Pacific/Tahiti" + startTime := time.Now().AddDate(0, 0, 2).Format("2006-01-02T15:04:05Z") + endTime := time.Now().AddDate(0, 0, 8).Format("2006-01-02T15:04:05Z") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_ScheduleWithTimezone(rName, cron, timezone, startTime, endTime), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", cron), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", timezone), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckResourceAttr(resourceName, "start_time", startTime), + resource.TestCheckResourceAttr(resourceName, "end_time", endTime), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_CronExpression_StartEndTimeTimezone(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + cron := "cron(0 17 * * ? *)" + scheduleTimezone := "Etc/GMT+9" // Z-09:00 (IANA and RFC3339 have inverted signs) + startTimezone, _ := time.LoadLocation("Antarctica/DumontDUrville") // Z+10:00 + endTimezone, _ := time.LoadLocation("America/Vancouver") // Z-08:00 + startTime := time.Now().AddDate(0, 0, 2).In(startTimezone) + startTimeUtc := startTime.UTC() + endTime := time.Now().AddDate(0, 0, 8).In(endTimezone) + endTimeUtc := endTime.UTC() + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_ScheduleWithTimezone(rName, cron, scheduleTimezone, startTime.Format(time.RFC3339), endTime.Format(time.RFC3339)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", cron), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", scheduleTimezone), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckResourceAttr(resourceName, "start_time", startTimeUtc.Format(time.RFC3339)), + resource.TestCheckResourceAttr(resourceName, "end_time", endTimeUtc.Format(time.RFC3339)), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_Schedule(rName, cron), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", cron), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckResourceAttr(resourceName, "start_time", ""), + resource.TestCheckResourceAttr(resourceName, "end_time", ""), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_RateExpression_basic(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + rate := "rate(1 day)" + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_Schedule(rName, rate), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", rate), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_Schedule_RateExpression_Timezone(t *testing.T) { + var sa applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + rate := "rate(1 day)" + timezone := "Pacific/Tahiti" + startTime := time.Now().AddDate(0, 0, 2).Format("2006-01-02T15:04:05Z") + endTime := time.Now().AddDate(0, 0, 8).Format("2006-01-02T15:04:05Z") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_ScheduleWithTimezone(rName, rate, timezone, startTime, endTime), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", rate), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", timezone), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckResourceAttr(resourceName, "start_time", startTime), + resource.TestCheckResourceAttr(resourceName, "end_time", endTime), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_MinCapacity(t *testing.T) { + var sa1, sa2 applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + schedule := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_MinCapacity(rName, schedule, 1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa1), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", schedule)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", ""), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_MinCapacity(rName, schedule, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa2), + testAccCheckAppautoscalingScheduledActionNotRecreated(&sa1, &sa2), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", ""), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_MaxCapacity(rName, schedule, 10), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa2), + testAccCheckAppautoscalingScheduledActionNotRecreated(&sa1, &sa2), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", ""), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + ), + }, + }, + }) +} + +func TestAccAWSAppautoscalingScheduledAction_MaxCapacity(t *testing.T) { + var sa1, sa2 applicationautoscaling.ScheduledAction + rName := acctest.RandomWithPrefix("tf-acc-test") + schedule := time.Now().AddDate(0, 0, 1).Format("2006-01-02T15:04:05") + resourceName := "aws_appautoscaling_scheduled_action.test" + autoscalingTargetResourceName := "aws_appautoscaling_target.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, applicationautoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppautoscalingScheduledActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppautoscalingScheduledActionConfig_MaxCapacity(rName, schedule, 10), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa1), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "service_namespace", autoscalingTargetResourceName, "service_namespace"), + resource.TestCheckResourceAttrPair(resourceName, "resource_id", autoscalingTargetResourceName, "resource_id"), + resource.TestCheckResourceAttrPair(resourceName, "scalable_dimension", autoscalingTargetResourceName, "scalable_dimension"), + resource.TestCheckResourceAttr(resourceName, "schedule", fmt.Sprintf("at(%s)", schedule)), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", ""), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "10"), + resource.TestCheckResourceAttr(resourceName, "timezone", "UTC"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "autoscaling", regexp.MustCompile(fmt.Sprintf("scheduledAction:.+:scheduledActionName/%s$", rName))), + resource.TestCheckNoResourceAttr(resourceName, "start_time"), + resource.TestCheckNoResourceAttr(resourceName, "end_time"), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_MaxCapacity(rName, schedule, 8), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa2), + testAccCheckAppautoscalingScheduledActionNotRecreated(&sa1, &sa2), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", ""), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", "8"), + ), + }, + { + Config: testAccAppautoscalingScheduledActionConfig_MinCapacity(rName, schedule, 1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAppautoscalingScheduledActionExists(resourceName, &sa2), + testAccCheckAppautoscalingScheduledActionNotRecreated(&sa1, &sa2), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.min_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "scalable_target_action.0.max_capacity", ""), ), }, }, @@ -127,20 +572,66 @@ func testAccCheckAwsAppautoscalingScheduledActionDestroy(s *terraform.State) err return nil } -func testAccCheckAwsAppautoscalingScheduledActionExists(name string) resource.TestCheckFunc { +func testAccCheckAwsAppautoscalingScheduledActionExists(name string, obj *applicationautoscaling.ScheduledAction) resource.TestCheckFunc { return func(s *terraform.State) error { - _, ok := s.RootModule().Resources[name] + rs, ok := s.RootModule().Resources[name] if !ok { return fmt.Errorf("Not found: %s", name) } + + if rs.Primary.ID == "" { + return fmt.Errorf("Application Autoscaling scheduled action (%s) ID not set", name) + } + + conn := testAccProvider.Meta().(*AWSClient).appautoscalingconn + + sa, err := finder.ScheduledAction(conn, rs.Primary.Attributes["name"], rs.Primary.Attributes["service_namespace"], rs.Primary.Attributes["resource_id"]) + if err != nil { + return err + } + + *obj = *sa + + return nil + } +} + +func testAccCheckAppautoscalingScheduledActionNotRecreated(i, j *applicationautoscaling.ScheduledAction) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !aws.TimeValue(i.CreationTime).Equal(aws.TimeValue(j.CreationTime)) { + return fmt.Errorf("Application Auto Scaling scheduled action recreated") + } + return nil } } func testAccAppautoscalingScheduledActionConfig_DynamoDB(rName, ts string) string { return fmt.Sprintf(` -resource "aws_dynamodb_table" "hoge" { - name = "tf-ddb-%s" +resource "aws_appautoscaling_scheduled_action" "test" { + name = %[1]q + service_namespace = aws_appautoscaling_target.test.service_namespace + resource_id = aws_appautoscaling_target.test.resource_id + scalable_dimension = aws_appautoscaling_target.test.scalable_dimension + + schedule = "at(%[2]s)" + + scalable_target_action { + min_capacity = 1 + max_capacity = 10 + } +} + +resource "aws_appautoscaling_target" "test" { + service_namespace = "dynamodb" + resource_id = "table/${aws_dynamodb_table.test.name}" + scalable_dimension = "dynamodb:table:ReadCapacityUnits" + min_capacity = 1 + max_capacity = 10 +} + +resource "aws_dynamodb_table" "test" { + name = %[1]q read_capacity = 5 write_capacity = 5 hash_key = "UserID" @@ -150,38 +641,77 @@ resource "aws_dynamodb_table" "hoge" { type = "S" } } +`, rName, ts) +} + +func testAccAppautoscalingScheduledActionConfig_DynamoDB_Updated(rName, ts, timezone string) string { + return fmt.Sprintf(` +resource "aws_appautoscaling_scheduled_action" "test" { + name = %[1]q + service_namespace = aws_appautoscaling_target.test.service_namespace + resource_id = aws_appautoscaling_target.test.resource_id + scalable_dimension = aws_appautoscaling_target.test.scalable_dimension + + schedule = "at(%[2]s)" + timezone = %[3]q + + scalable_target_action { + min_capacity = 2 + max_capacity = 9 + } +} -resource "aws_appautoscaling_target" "read" { +resource "aws_appautoscaling_target" "test" { service_namespace = "dynamodb" - resource_id = "table/${aws_dynamodb_table.hoge.name}" + resource_id = "table/${aws_dynamodb_table.test.name}" scalable_dimension = "dynamodb:table:ReadCapacityUnits" min_capacity = 1 max_capacity = 10 } -resource "aws_appautoscaling_scheduled_action" "hoge" { - name = "tf-appauto-%s" - service_namespace = aws_appautoscaling_target.read.service_namespace - resource_id = aws_appautoscaling_target.read.resource_id - scalable_dimension = aws_appautoscaling_target.read.scalable_dimension - schedule = "at(%s)" +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 5 + write_capacity = 5 + hash_key = "UserID" - scalable_target_action { - min_capacity = 1 - max_capacity = 10 + attribute { + name = "UserID" + type = "S" } } -`, rName, rName, ts) +`, rName, ts, timezone) } func testAccAppautoscalingScheduledActionConfig_ECS(rName, ts string) string { return fmt.Sprintf(` -resource "aws_ecs_cluster" "hoge" { - name = "tf-ecs-cluster-%s" +resource "aws_appautoscaling_scheduled_action" "test" { + name = %[1]q + service_namespace = aws_appautoscaling_target.test.service_namespace + resource_id = aws_appautoscaling_target.test.resource_id + scalable_dimension = aws_appautoscaling_target.test.scalable_dimension + schedule = "at(%[2]s)" + + scalable_target_action { + min_capacity = 1 + max_capacity = 5 + } } -resource "aws_ecs_task_definition" "hoge" { - family = "foobar%s" +resource "aws_appautoscaling_target" "test" { + service_namespace = "ecs" + resource_id = "service/${aws_ecs_cluster.test.name}/${aws_ecs_service.test.name}" + scalable_dimension = "ecs:service:DesiredCount" + min_capacity = 1 + max_capacity = 3 +} + +resource "aws_ecs_cluster" "test" { + name = %[1]q +} + +resource "aws_ecs_task_definition" "test" { + family = %[1]q container_definitions = < 0 { + input.Validators = expandAppconfigValidators(v.(*schema.Set).List()) + } + + profile, err := conn.CreateConfigurationProfile(input) + + if err != nil { + return fmt.Errorf("error creating AppConfig Configuration Profile (%s) for Application (%s): %w", name, appId, err) + } + + if profile == nil { + return fmt.Errorf("error creating AppConfig Configuration Profile (%s) for Application (%s): empty response", name, appId) + } + + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(profile.Id), aws.StringValue(profile.ApplicationId))) + + return resourceAwsAppconfigConfigurationProfileRead(d, meta) +} + +func resourceAwsAppconfigConfigurationProfileRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + confProfID, appID, err := resourceAwsAppconfigConfigurationProfileParseID(d.Id()) + + if err != nil { + return err + } + + input := &appconfig.GetConfigurationProfileInput{ + ApplicationId: aws.String(appID), + ConfigurationProfileId: aws.String(confProfID), + } + + output, err := conn.GetConfigurationProfile(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] AppConfig Configuration Profile (%s) for Application (%s) not found, removing from state", confProfID, appID) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting AppConfig Configuration Profile (%s) for Application (%s): %w", confProfID, appID, err) + } + + if output == nil { + return fmt.Errorf("error getting AppConfig Configuration Profile (%s) for Application (%s): empty response", confProfID, appID) + } + + d.Set("application_id", output.ApplicationId) + d.Set("configuration_profile_id", output.Id) + d.Set("description", output.Description) + d.Set("location_uri", output.LocationUri) + d.Set("name", output.Name) + + d.Set("retrieval_role_arn", output.RetrievalRoleArn) + + if err := d.Set("validator", flattenAwsAppconfigValidators(output.Validators)); err != nil { + return fmt.Errorf("error setting validator: %w", err) + } + + arn := arn.ARN{ + AccountID: meta.(*AWSClient).accountid, + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Resource: fmt.Sprintf("application/%s/configurationprofile/%s", appID, confProfID), + Service: "appconfig", + }.String() + + d.Set("arn", arn) + + tags, err := keyvaluetags.AppconfigListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for AppConfig Configuration Profile (%s): %w", d.Id(), err) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceAwsAppconfigConfigurationProfileUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + + if d.HasChangesExcept("tags", "tags_all") { + confProfID, appID, err := resourceAwsAppconfigConfigurationProfileParseID(d.Id()) + + if err != nil { + return err + } + + updateInput := &appconfig.UpdateConfigurationProfileInput{ + ApplicationId: aws.String(appID), + ConfigurationProfileId: aws.String(confProfID), + } + + if d.HasChange("description") { + updateInput.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("name") { + updateInput.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("retrieval_role_arn") { + updateInput.RetrievalRoleArn = aws.String(d.Get("retrieval_role_arn").(string)) + } + + if d.HasChange("validator") { + updateInput.Validators = expandAppconfigValidators(d.Get("validator").(*schema.Set).List()) + } + + _, err = conn.UpdateConfigurationProfile(updateInput) + + if err != nil { + return fmt.Errorf("error updating AppConfig Configuration Profile (%s) for Application (%s): %w", confProfID, appID, err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.AppconfigUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating AppConfig Configuration Profile (%s) tags: %w", d.Get("arn").(string), err) + } + } + + return resourceAwsAppconfigConfigurationProfileRead(d, meta) +} + +func resourceAwsAppconfigConfigurationProfileDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + + confProfID, appID, err := resourceAwsAppconfigConfigurationProfileParseID(d.Id()) + + if err != nil { + return err + } + + input := &appconfig.DeleteConfigurationProfileInput{ + ApplicationId: aws.String(appID), + ConfigurationProfileId: aws.String(confProfID), + } + + _, err = conn.DeleteConfigurationProfile(input) + + if tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting AppConfig Configuration Profile (%s) for Application (%s): %w", confProfID, appID, err) + } + + return nil +} + +func resourceAwsAppconfigConfigurationProfileParseID(id string) (string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%q), expected ConfigurationProfileID:ApplicationID", id) + } + + return parts[0], parts[1], nil +} + +func expandAppconfigValidator(tfMap map[string]interface{}) *appconfig.Validator { + if tfMap == nil { + return nil + } + + validator := &appconfig.Validator{} + + // AppConfig API supports empty content + if v, ok := tfMap["content"].(string); ok { + validator.Content = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + validator.Type = aws.String(v) + } + + return validator +} + +func expandAppconfigValidators(tfList []interface{}) []*appconfig.Validator { + // AppConfig API requires a 0 length slice instead of a nil value + // when updating from N validators to 0/nil validators + validators := make([]*appconfig.Validator, 0) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + validator := expandAppconfigValidator(tfMap) + + if validator == nil { + continue + } + + validators = append(validators, validator) + } + + return validators +} + +func flattenAwsAppconfigValidator(validator *appconfig.Validator) map[string]interface{} { + if validator == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := validator.Content; v != nil { + tfMap["content"] = aws.StringValue(v) + } + + if v := validator.Type; v != nil { + tfMap["type"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenAwsAppconfigValidators(validators []*appconfig.Validator) []interface{} { + if len(validators) == 0 { + return nil + } + + var tfList []interface{} + + for _, validator := range validators { + if validator == nil { + continue + } + + tfList = append(tfList, flattenAwsAppconfigValidator(validator)) + } + + return tfList +} diff --git a/aws/resource_aws_appconfig_configuration_profile_test.go b/aws/resource_aws_appconfig_configuration_profile_test.go new file mode 100644 index 000000000000..e5029e083992 --- /dev/null +++ b/aws/resource_aws_appconfig_configuration_profile_test.go @@ -0,0 +1,656 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appconfig" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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" +) + +func init() { + resource.AddTestSweepers("aws_appconfig_configuration_profile", &resource.Sweeper{ + Name: "aws_appconfig_configuration_profile", + F: testSweepAppConfigConfigurationProfiles, + Dependencies: []string{ + "aws_appconfig_hosted_configuration_version", + }, + }) +} + +func testSweepAppConfigConfigurationProfiles(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).appconfigconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + + input := &appconfig.ListApplicationsInput{} + + err = conn.ListApplicationsPages(input, func(page *appconfig.ListApplicationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.Items { + if item == nil { + continue + } + + appId := aws.StringValue(item.Id) + + profilesInput := &appconfig.ListConfigurationProfilesInput{ + ApplicationId: item.Id, + } + + err := conn.ListConfigurationProfilesPages(profilesInput, func(page *appconfig.ListConfigurationProfilesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.Items { + if item == nil { + continue + } + + id := fmt.Sprintf("%s:%s", aws.StringValue(item.Id), appId) + + log.Printf("[INFO] Deleting AppConfig Configuration Profile (%s)", id) + r := resourceAwsAppconfigConfigurationProfile() + d := r.Data(nil) + d.SetId(id) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppConfig Configuration Profiles for Application (%s): %w", appId, err)) + } + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppConfig Applications: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppConfig Configuration Profiles for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping AppConfig Configuration Profiles sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func TestAccAWSAppConfigConfigurationProfile_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + appResourceName := "aws_appconfig_application.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "appconfig", regexp.MustCompile(`application/[a-z0-9]{4,7}/configurationprofile/[a-z0-9]{4,7}`)), + resource.TestCheckResourceAttrPair(resourceName, "application_id", appResourceName, "id"), + resource.TestMatchResourceAttr(resourceName, "configuration_profile_id", regexp.MustCompile(`[a-z0-9]{4,7}`)), + resource.TestCheckResourceAttr(resourceName, "location_uri", "hosted"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppconfigConfigurationProfile(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_Validators_JSON(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigValidator_JSON(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "validator.*", map[string]string{ + "type": appconfig.ValidatorTypeJsonSchema, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigConfigurationProfileConfigValidator_NoJSONContent(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "validator.*", map[string]string{ + "content": "", + "type": appconfig.ValidatorTypeJsonSchema, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Validator Removal + Config: testAccAWSAppConfigConfigurationProfileConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "0"), + ), + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_Validators_Lambda(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigValidator_Lambda(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "validator.*.content", "aws_lambda_function.test", "arn"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "validator.*", map[string]string{ + "type": appconfig.ValidatorTypeLambda, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Validator Removal + Config: testAccAWSAppConfigConfigurationProfileConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "0"), + ), + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_Validators_Multiple(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigValidator_Multiple(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "validator.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "validator.*", map[string]string{ + "content": "{\"$schema\":\"http://json-schema.org/draft-05/schema#\",\"description\":\"BasicFeatureToggle-1\",\"title\":\"$id$\"}", + "type": appconfig.ValidatorTypeJsonSchema, + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "validator.*.content", "aws_lambda_function.test", "arn"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "validator.*", map[string]string{ + "type": appconfig.ValidatorTypeLambda, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_updateName(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + rNameUpdated := acctest.RandomWithPrefix("tf-acc-test-update") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + ), + }, + { + Config: testAccAWSAppConfigConfigurationProfileConfigName(rNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rNameUpdated), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_updateDescription(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + description := acctest.RandomWithPrefix("tf-acc-test-update") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileConfigDescription(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigConfigurationProfileConfigDescription(rName, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", description), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigConfigurationProfile_Tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_configuration_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigConfigurationProfileDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigConfigurationProfileTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigConfigurationProfileTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAppConfigConfigurationProfileTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigConfigurationProfileExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAppConfigConfigurationProfileDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appconfigconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appconfig_configuration_profile" { + continue + } + + confProfID, appID, err := resourceAwsAppconfigConfigurationProfileParseID(rs.Primary.ID) + + if err != nil { + return err + } + + input := &appconfig.GetConfigurationProfileInput{ + ApplicationId: aws.String(appID), + ConfigurationProfileId: aws.String(confProfID), + } + + output, err := conn.GetConfigurationProfile(input) + + if tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("error reading AppConfig Configuration Profile (%s) for Application (%s): %w", confProfID, appID, err) + } + + if output != nil { + return fmt.Errorf("AppConfig Configuration Profile (%s) for Application (%s) still exists", confProfID, appID) + } + } + + return nil +} + +func testAccCheckAWSAppConfigConfigurationProfileExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Resource (%s) ID not set", resourceName) + } + + confProfID, appID, err := resourceAwsAppconfigConfigurationProfileParseID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).appconfigconn + + output, err := conn.GetConfigurationProfile(&appconfig.GetConfigurationProfileInput{ + ApplicationId: aws.String(appID), + ConfigurationProfileId: aws.String(confProfID), + }) + + if err != nil { + return fmt.Errorf("error reading AppConfig Configuration Profile (%s) for Application (%s): %w", confProfID, appID, err) + } + + if output == nil { + return fmt.Errorf("AppConfig Configuration Profile (%s) for Application (%s) not found", confProfID, appID) + } + + return nil + } +} + +func testAccAWSAppConfigConfigurationProfileConfigName(rName string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +resource "aws_appconfig_configuration_profile" "test" { + application_id = aws_appconfig_application.test.id + name = %q + location_uri = "hosted" +} +`, rName)) +} + +func testAccAWSAppConfigConfigurationProfileConfigDescription(rName, description string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigDescription(rName, description), + fmt.Sprintf(` +resource "aws_appconfig_configuration_profile" "test" { + application_id = aws_appconfig_application.test.id + name = %[1]q + description = %[2]q + location_uri = "hosted" +} +`, rName, description)) +} + +func testAccAWSAppConfigConfigurationProfileConfigValidator_JSON(rName string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +resource "aws_appconfig_configuration_profile" "test" { + application_id = aws_appconfig_application.test.id + name = %q + location_uri = "hosted" + + validator { + content = jsonencode({ + "$schema" = "http://json-schema.org/draft-04/schema#" + title = "$id$" + description = "BasicFeatureToggle-1" + type = "object" + additionalProperties = false + patternProperties = { + "[^\\s]+$" = { + type = "boolean" + } + } + minProperties = 1 + }) + + type = "JSON_SCHEMA" + } +} +`, rName)) +} + +func testAccAWSAppConfigConfigurationProfileConfigValidator_NoJSONContent(rName string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +resource "aws_appconfig_configuration_profile" "test" { + application_id = aws_appconfig_application.test.id + name = %q + location_uri = "hosted" + + validator { + type = "JSON_SCHEMA" + } +} +`, rName)) +} + +func testAccAWSAppConfigApplicationConfigLambdaBase(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "lambda" { + name = "%[1]s-lambda" + + assume_role_policy = < 0 { + input.Monitors = expandAppconfigEnvironmentMonitors(v.(*schema.Set).List()) + } + + environment, err := conn.CreateEnvironment(input) + + if err != nil { + return fmt.Errorf("error creating AppConfig Environment for Application (%s): %w", appId, err) + } + + if environment == nil { + return fmt.Errorf("error creating AppConfig Environment for Application (%s): empty response", appId) + } + + d.Set("environment_id", environment.Id) + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(environment.Id), aws.StringValue(environment.ApplicationId))) + + return resourceAwsAppconfigEnvironmentRead(d, meta) +} + +func resourceAwsAppconfigEnvironmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + envID, appID, err := resourceAwsAppconfigEnvironmentParseID(d.Id()) + + if err != nil { + return err + } + + input := &appconfig.GetEnvironmentInput{ + ApplicationId: aws.String(appID), + EnvironmentId: aws.String(envID), + } + + output, err := conn.GetEnvironment(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appconfig Environment (%s) for Application (%s) not found, removing from state", envID, appID) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting AppConfig Environment (%s) for Application (%s): %w", envID, appID, err) + } + + if output == nil { + return fmt.Errorf("error getting AppConfig Environment (%s) for Application (%s): empty response", envID, appID) + } + + d.Set("application_id", output.ApplicationId) + d.Set("environment_id", output.Id) + d.Set("description", output.Description) + d.Set("name", output.Name) + d.Set("state", output.State) + + if err := d.Set("monitor", flattenAwsAppconfigEnvironmentMonitors(output.Monitors)); err != nil { + return fmt.Errorf("error setting monitor: %w", err) + } + + arn := arn.ARN{ + AccountID: meta.(*AWSClient).accountid, + Partition: meta.(*AWSClient).partition, + Region: meta.(*AWSClient).region, + Resource: fmt.Sprintf("application/%s/environment/%s", appID, envID), + Service: "appconfig", + }.String() + + d.Set("arn", arn) + + tags, err := keyvaluetags.AppconfigListTags(conn, arn) + + if err != nil { + return fmt.Errorf("error listing tags for AppConfig Environment (%s): %s", d.Id(), err) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceAwsAppconfigEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + + if d.HasChangesExcept("tags", "tags_all") { + envID, appID, err := resourceAwsAppconfigEnvironmentParseID(d.Id()) + + if err != nil { + return err + } + + updateInput := &appconfig.UpdateEnvironmentInput{ + EnvironmentId: aws.String(envID), + ApplicationId: aws.String(appID), + } + + if d.HasChange("description") { + updateInput.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("name") { + updateInput.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("monitor") { + updateInput.Monitors = expandAppconfigEnvironmentMonitors(d.Get("monitor").(*schema.Set).List()) + } + + _, err = conn.UpdateEnvironment(updateInput) + + if err != nil { + return fmt.Errorf("error updating AppConfig Environment (%s) for Application (%s): %w", envID, appID, err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.AppconfigUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating AppConfig Environment (%s) tags: %w", d.Get("arn").(string), err) + } + } + + return resourceAwsAppconfigEnvironmentRead(d, meta) +} + +func resourceAwsAppconfigEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appconfigconn + + envID, appID, err := resourceAwsAppconfigEnvironmentParseID(d.Id()) + + if err != nil { + return err + } + + input := &appconfig.DeleteEnvironmentInput{ + EnvironmentId: aws.String(envID), + ApplicationId: aws.String(appID), + } + + _, err = conn.DeleteEnvironment(input) + + if tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Appconfig Environment (%s) for Application (%s): %w", envID, appID, err) + } + + return nil +} + +func resourceAwsAppconfigEnvironmentParseID(id string) (string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%q), expected EnvironmentID:ApplicationID", id) + } + + return parts[0], parts[1], nil +} + +func expandAppconfigEnvironmentMonitor(tfMap map[string]interface{}) *appconfig.Monitor { + if tfMap == nil { + return nil + } + + monitor := &appconfig.Monitor{} + + if v, ok := tfMap["alarm_arn"].(string); ok && v != "" { + monitor.AlarmArn = aws.String(v) + } + + if v, ok := tfMap["alarm_role_arn"].(string); ok && v != "" { + monitor.AlarmRoleArn = aws.String(v) + } + + return monitor +} + +func expandAppconfigEnvironmentMonitors(tfList []interface{}) []*appconfig.Monitor { + // AppConfig API requires a 0 length slice instead of a nil value + // when updating from N monitors to 0/nil monitors + monitors := make([]*appconfig.Monitor, 0) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + monitor := expandAppconfigEnvironmentMonitor(tfMap) + + if monitor == nil { + continue + } + + monitors = append(monitors, monitor) + } + + return monitors +} + +func flattenAwsAppconfigEnvironmentMonitor(monitor *appconfig.Monitor) map[string]interface{} { + if monitor == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := monitor.AlarmArn; v != nil { + tfMap["alarm_arn"] = aws.StringValue(v) + } + + if v := monitor.AlarmRoleArn; v != nil { + tfMap["alarm_role_arn"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenAwsAppconfigEnvironmentMonitors(monitors []*appconfig.Monitor) []interface{} { + if len(monitors) == 0 { + return nil + } + + var tfList []interface{} + + for _, monitor := range monitors { + if monitor == nil { + continue + } + + tfList = append(tfList, flattenAwsAppconfigEnvironmentMonitor(monitor)) + } + + return tfList +} diff --git a/aws/resource_aws_appconfig_environment_test.go b/aws/resource_aws_appconfig_environment_test.go new file mode 100644 index 000000000000..e1b2cf24061c --- /dev/null +++ b/aws/resource_aws_appconfig_environment_test.go @@ -0,0 +1,598 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appconfig" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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" +) + +func init() { + resource.AddTestSweepers("aws_appconfig_environment", &resource.Sweeper{ + Name: "aws_appconfig_environment", + F: testSweepAppConfigEnvironments, + }) +} + +func testSweepAppConfigEnvironments(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).appconfigconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + + input := &appconfig.ListApplicationsInput{} + + err = conn.ListApplicationsPages(input, func(page *appconfig.ListApplicationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.Items { + if item == nil { + continue + } + + appId := aws.StringValue(item.Id) + + envInput := &appconfig.ListEnvironmentsInput{ + ApplicationId: item.Id, + } + + err := conn.ListEnvironmentsPages(envInput, func(page *appconfig.ListEnvironmentsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, item := range page.Items { + if item == nil { + continue + } + + id := fmt.Sprintf("%s:%s", aws.StringValue(item.Id), appId) + + log.Printf("[INFO] Deleting AppConfig Environment (%s)", id) + r := resourceAwsAppconfigEnvironment() + d := r.Data(nil) + d.SetId(id) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppConfig Environments for Application (%s): %w", appId, err)) + } + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppConfig Applications: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppConfig Environments for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping AppConfig Environments sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func TestAccAWSAppConfigEnvironment_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_environment.test" + appResourceName := "aws_appconfig_application.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "appconfig", regexp.MustCompile(`application/[a-z0-9]{4,7}/environment/[a-z0-9]{4,7}`)), + resource.TestCheckResourceAttrPair(resourceName, "application_id", appResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "monitor.#", "0"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppconfigEnvironment(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_updateName(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + rNameUpdated := acctest.RandomWithPrefix("tf-acc-test-update") + resourceName := "aws_appconfig_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + ), + }, + { + Config: testAccAWSAppConfigEnvironmentConfigBasic(rNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rNameUpdated), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_updateDescription(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + description := acctest.RandomWithPrefix("tf-acc-test-update") + resourceName := "aws_appconfig_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentConfigDescription(rName, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigEnvironmentConfigDescription(rName, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", description), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Description Removal + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + ), + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_Monitors(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentWithMonitors(rName, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "monitor.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_arn", "aws_cloudwatch_metric_alarm.test.0", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigEnvironmentWithMonitors(rName, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "monitor.#", "2"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_arn", "aws_cloudwatch_metric_alarm.test.0", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_arn", "aws_cloudwatch_metric_alarm.test.1", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "monitor.*.alarm_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Monitor Removal + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "monitor.#", "0"), + ), + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_MultipleEnvironments(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName1 := "aws_appconfig_environment.test" + resourceName2 := "aws_appconfig_environment.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentConfigMultiple(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName1), + testAccCheckAWSAppConfigEnvironmentExists(resourceName2), + ), + }, + { + ResourceName: resourceName1, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: resourceName2, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigEnvironmentConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName1), + ), + }, + { + ResourceName: resourceName1, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAppConfigEnvironment_Tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_appconfig_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, appconfig.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppConfigEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAppConfigEnvironmentTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAppConfigEnvironmentTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAppConfigEnvironmentTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAppConfigEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAppConfigEnvironmentDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appconfigconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appconfig_environment" { + continue + } + + envID, appID, err := resourceAwsAppconfigEnvironmentParseID(rs.Primary.ID) + + if err != nil { + return err + } + + input := &appconfig.GetEnvironmentInput{ + ApplicationId: aws.String(appID), + EnvironmentId: aws.String(envID), + } + + output, err := conn.GetEnvironment(input) + + if tfawserr.ErrCodeEquals(err, appconfig.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("error reading AppConfig Environment (%s) for Application (%s): %w", envID, appID, err) + } + + if output != nil { + return fmt.Errorf("AppConfig Environment (%s) for Application (%s) still exists", envID, appID) + } + } + + return nil +} + +func testAccCheckAWSAppConfigEnvironmentExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Resource (%s) ID not set", resourceName) + } + + envID, appID, err := resourceAwsAppconfigEnvironmentParseID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).appconfigconn + + input := &appconfig.GetEnvironmentInput{ + ApplicationId: aws.String(appID), + EnvironmentId: aws.String(envID), + } + + output, err := conn.GetEnvironment(input) + + if err != nil { + return fmt.Errorf("error reading AppConfig Environment (%s) for Application (%s): %w", envID, appID, err) + } + + if output == nil { + return fmt.Errorf("AppConfig Environment (%s) for Application (%s) not found", envID, appID) + } + + return nil + } +} + +func testAccAWSAppConfigEnvironmentConfigBasic(rName string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +resource "aws_appconfig_environment" "test" { + name = %q + application_id = aws_appconfig_application.test.id +} +`, rName)) +} + +func testAccAWSAppConfigEnvironmentConfigDescription(rName, description string) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +resource "aws_appconfig_environment" "test" { + name = %q + description = %q + application_id = aws_appconfig_application.test.id +} +`, rName, description)) +} + +func testAccAWSAppConfigEnvironmentWithMonitors(rName string, count int) string { + return composeConfig( + testAccAWSAppConfigApplicationConfigName(rName), + fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = < 0 && vSds[0] != nil { + sds := &appmesh.VirtualGatewayListenerTlsSdsCertificate{} + + mSds := vSds[0].(map[string]interface{}) + + if vSecretName, ok := mSds["secret_name"].(string); ok && vSecretName != "" { + sds.SecretName = aws.String(vSecretName) + } + + certificate.Sds = sds + } + tls.Certificate = certificate } + if vValidation, ok := mTls["validation"].([]interface{}); ok && len(vValidation) > 0 && vValidation[0] != nil { + validation := &appmesh.VirtualGatewayListenerTlsValidationContext{} + + mValidation := vValidation[0].(map[string]interface{}) + + if vSubjectAlternativeNames, ok := mValidation["subject_alternative_names"].([]interface{}); ok && len(vSubjectAlternativeNames) > 0 && vSubjectAlternativeNames[0] != nil { + subjectAlternativeNames := &appmesh.SubjectAlternativeNames{} + + mSubjectAlternativeNames := vSubjectAlternativeNames[0].(map[string]interface{}) + + if vMatch, ok := mSubjectAlternativeNames["match"].([]interface{}); ok && len(vMatch) > 0 && vMatch[0] != nil { + match := &appmesh.SubjectAlternativeNameMatchers{} + + mMatch := vMatch[0].(map[string]interface{}) + + if vExact, ok := mMatch["exact"].(*schema.Set); ok && vExact.Len() > 0 { + match.Exact = expandStringSet(vExact) + } + + subjectAlternativeNames.Match = match + } + + validation.SubjectAlternativeNames = subjectAlternativeNames + } + + if vTrust, ok := mValidation["trust"].([]interface{}); ok && len(vTrust) > 0 && vTrust[0] != nil { + trust := &appmesh.VirtualGatewayListenerTlsValidationContextTrust{} + + mTrust := vTrust[0].(map[string]interface{}) + + if vFile, ok := mTrust["file"].([]interface{}); ok && len(vFile) > 0 && vFile[0] != nil { + file := &appmesh.VirtualGatewayTlsValidationContextFileTrust{} + + mFile := vFile[0].(map[string]interface{}) + + if vCertificateChain, ok := mFile["certificate_chain"].(string); ok && vCertificateChain != "" { + file.CertificateChain = aws.String(vCertificateChain) + } + + trust.File = file + } + + if vSds, ok := mTrust["sds"].([]interface{}); ok && len(vSds) > 0 && vSds[0] != nil { + sds := &appmesh.VirtualGatewayTlsValidationContextSdsTrust{} + + mSds := vSds[0].(map[string]interface{}) + + if vSecretName, ok := mSds["secret_name"].(string); ok && vSecretName != "" { + sds.SecretName = aws.String(vSecretName) + } + + trust.Sds = sds + } + + validation.Trust = trust + } + + tls.Validation = validation + } + listener.Tls = tls } @@ -833,6 +1171,41 @@ func expandAppmeshVirtualGatewayClientPolicy(vClientPolicy []interface{}) *appme mTls := vTls[0].(map[string]interface{}) + if vCertificate, ok := mTls["certificate"].([]interface{}); ok && len(vCertificate) > 0 && vCertificate[0] != nil { + certificate := &appmesh.VirtualGatewayClientTlsCertificate{} + + mCertificate := vCertificate[0].(map[string]interface{}) + + if vFile, ok := mCertificate["file"].([]interface{}); ok && len(vFile) > 0 && vFile[0] != nil { + file := &appmesh.VirtualGatewayListenerTlsFileCertificate{} + + mFile := vFile[0].(map[string]interface{}) + + if vCertificateChain, ok := mFile["certificate_chain"].(string); ok && vCertificateChain != "" { + file.CertificateChain = aws.String(vCertificateChain) + } + if vPrivateKey, ok := mFile["private_key"].(string); ok && vPrivateKey != "" { + file.PrivateKey = aws.String(vPrivateKey) + } + + certificate.File = file + } + + if vSds, ok := mCertificate["sds"].([]interface{}); ok && len(vSds) > 0 && vSds[0] != nil { + sds := &appmesh.VirtualGatewayListenerTlsSdsCertificate{} + + mSds := vSds[0].(map[string]interface{}) + + if vSecretName, ok := mSds["secret_name"].(string); ok && vSecretName != "" { + sds.SecretName = aws.String(vSecretName) + } + + certificate.Sds = sds + } + + tls.Certificate = certificate + } + if vEnforce, ok := mTls["enforce"].(bool); ok { tls.Enforce = aws.Bool(vEnforce) } @@ -846,6 +1219,26 @@ func expandAppmeshVirtualGatewayClientPolicy(vClientPolicy []interface{}) *appme mValidation := vValidation[0].(map[string]interface{}) + if vSubjectAlternativeNames, ok := mValidation["subject_alternative_names"].([]interface{}); ok && len(vSubjectAlternativeNames) > 0 && vSubjectAlternativeNames[0] != nil { + subjectAlternativeNames := &appmesh.SubjectAlternativeNames{} + + mSubjectAlternativeNames := vSubjectAlternativeNames[0].(map[string]interface{}) + + if vMatch, ok := mSubjectAlternativeNames["match"].([]interface{}); ok && len(vMatch) > 0 && vMatch[0] != nil { + match := &appmesh.SubjectAlternativeNameMatchers{} + + mMatch := vMatch[0].(map[string]interface{}) + + if vExact, ok := mMatch["exact"].(*schema.Set); ok && vExact.Len() > 0 { + match.Exact = expandStringSet(vExact) + } + + subjectAlternativeNames.Match = match + } + + validation.SubjectAlternativeNames = subjectAlternativeNames + } + if vTrust, ok := mValidation["trust"].([]interface{}); ok && len(vTrust) > 0 && vTrust[0] != nil { trust := &appmesh.VirtualGatewayTlsValidationContextTrust{} @@ -875,6 +1268,18 @@ func expandAppmeshVirtualGatewayClientPolicy(vClientPolicy []interface{}) *appme trust.File = file } + if vSds, ok := mTrust["sds"].([]interface{}); ok && len(vSds) > 0 && vSds[0] != nil { + sds := &appmesh.VirtualGatewayTlsValidationContextSdsTrust{} + + mSds := vSds[0].(map[string]interface{}) + + if vSecretName, ok := mSds["secret_name"].(string); ok && vSecretName != "" { + sds.SecretName = aws.String(vSecretName) + } + + trust.Sds = sds + } + validation.Trust = trust } @@ -981,9 +1386,59 @@ func flattenAppmeshVirtualGatewaySpec(spec *appmesh.VirtualGatewaySpec) []interf mCertificate["file"] = []interface{}{mFile} } + if sds := certificate.Sds; sds != nil { + mSds := map[string]interface{}{ + "secret_name": aws.StringValue(sds.SecretName), + } + + mCertificate["sds"] = []interface{}{mSds} + } + mTls["certificate"] = []interface{}{mCertificate} } + if validation := tls.Validation; validation != nil { + mValidation := map[string]interface{}{} + + if subjectAlternativeNames := validation.SubjectAlternativeNames; subjectAlternativeNames != nil { + mSubjectAlternativeNames := map[string]interface{}{} + + if match := subjectAlternativeNames.Match; match != nil { + mMatch := map[string]interface{}{ + "exact": flattenStringSet(match.Exact), + } + + mSubjectAlternativeNames["match"] = []interface{}{mMatch} + } + + mValidation["subject_alternative_names"] = []interface{}{mSubjectAlternativeNames} + } + + if trust := validation.Trust; trust != nil { + mTrust := map[string]interface{}{} + + if file := trust.File; file != nil { + mFile := map[string]interface{}{ + "certificate_chain": aws.StringValue(file.CertificateChain), + } + + mTrust["file"] = []interface{}{mFile} + } + + if sds := trust.Sds; sds != nil { + mSds := map[string]interface{}{ + "secret_name": aws.StringValue(sds.SecretName), + } + + mTrust["sds"] = []interface{}{mSds} + } + + mValidation["trust"] = []interface{}{mTrust} + } + + mTls["validation"] = []interface{}{mValidation} + } + mListener["tls"] = []interface{}{mTls} } @@ -1026,9 +1481,46 @@ func flattenAppmeshVirtualGatewayClientPolicy(clientPolicy *appmesh.VirtualGatew "ports": flattenInt64Set(tls.Ports), } + if certificate := tls.Certificate; certificate != nil { + mCertificate := map[string]interface{}{} + + if file := certificate.File; file != nil { + mFile := map[string]interface{}{ + "certificate_chain": aws.StringValue(file.CertificateChain), + "private_key": aws.StringValue(file.PrivateKey), + } + + mCertificate["file"] = []interface{}{mFile} + } + + if sds := certificate.Sds; sds != nil { + mSds := map[string]interface{}{ + "secret_name": aws.StringValue(sds.SecretName), + } + + mCertificate["sds"] = []interface{}{mSds} + } + + mTls["certificate"] = []interface{}{mCertificate} + } + if validation := tls.Validation; validation != nil { mValidation := map[string]interface{}{} + if subjectAlternativeNames := validation.SubjectAlternativeNames; subjectAlternativeNames != nil { + mSubjectAlternativeNames := map[string]interface{}{} + + if match := subjectAlternativeNames.Match; match != nil { + mMatch := map[string]interface{}{ + "exact": flattenStringSet(match.Exact), + } + + mSubjectAlternativeNames["match"] = []interface{}{mMatch} + } + + mValidation["subject_alternative_names"] = []interface{}{mSubjectAlternativeNames} + } + if trust := validation.Trust; trust != nil { mTrust := map[string]interface{}{} @@ -1048,6 +1540,14 @@ func flattenAppmeshVirtualGatewayClientPolicy(clientPolicy *appmesh.VirtualGatew mTrust["file"] = []interface{}{mFile} } + if sds := trust.Sds; sds != nil { + mSds := map[string]interface{}{ + "secret_name": aws.StringValue(sds.SecretName), + } + + mTrust["sds"] = []interface{}{mSds} + } + mValidation["trust"] = []interface{}{mTrust} } diff --git a/aws/resource_aws_appmesh_virtual_gateway_test.go b/aws/resource_aws_appmesh_virtual_gateway_test.go index 7d54900b1036..d150928afee2 100644 --- a/aws/resource_aws_appmesh_virtual_gateway_test.go +++ b/aws/resource_aws_appmesh_virtual_gateway_test.go @@ -34,17 +34,17 @@ func testSweepAppmeshVirtualGateways(region string) error { var sweeperErrs *multierror.Error - err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, isLast bool) bool { + err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, mesh := range page.Meshes { meshName := aws.StringValue(mesh.MeshName) - err = conn.ListVirtualGatewaysPages(&appmesh.ListVirtualGatewaysInput{MeshName: mesh.MeshName}, func(page *appmesh.ListVirtualGatewaysOutput, isLast bool) bool { + err = conn.ListVirtualGatewaysPages(&appmesh.ListVirtualGatewaysInput{MeshName: mesh.MeshName}, func(page *appmesh.ListVirtualGatewaysOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, virtualGateway := range page.VirtualGateways { @@ -65,7 +65,7 @@ func testSweepAppmeshVirtualGateways(region string) error { } } - return !isLast + return !lastPage }) if err != nil { @@ -73,7 +73,7 @@ func testSweepAppmeshVirtualGateways(region string) error { } } - return !isLast + return !lastPage }) if testSweepSkipSweepError(err) { log.Printf("[WARN] Skipping Appmesh virtual gateway sweep for %s: %s", region, err) @@ -94,6 +94,7 @@ func testAccAwsAppmeshVirtualGateway_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -138,6 +139,7 @@ func testAccAwsAppmeshVirtualGateway_disappears(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -161,6 +163,7 @@ func testAccAwsAppmeshVirtualGateway_BackendDefaults(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -175,6 +178,7 @@ func testAccAwsAppmeshVirtualGateway_BackendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "1"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "8443"), @@ -183,6 +187,7 @@ func testAccAwsAppmeshVirtualGateway_BackendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), @@ -208,6 +213,7 @@ func testAccAwsAppmeshVirtualGateway_BackendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "2"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "443"), @@ -217,6 +223,71 @@ func testAccAwsAppmeshVirtualGateway_BackendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/etc/ssl/certs/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.protocol", "http"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualGateway/%s", meshName, vgName)), + ), + }, + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s/%s", meshName, vgName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAwsAppmeshVirtualGateway_BackendDefaultsCertificate(t *testing.T) { + var v appmesh.VirtualGatewayData + resourceName := "aws_appmesh_virtual_gateway.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") + vgName := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppmeshVirtualGatewayConfigBackendDefaultsCertificate(meshName, vgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualGatewayExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "name", vgName), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.0.private_key", "tell-nobody"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "def.example.com"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.0.secret_name", "restricted"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), @@ -249,6 +320,7 @@ func testAccAwsAppmeshVirtualGateway_ListenerConnectionPool(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -325,6 +397,7 @@ func testAccAwsAppmeshVirtualGateway_ListenerHealthChecks(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -404,11 +477,14 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { resourceName := "aws_appmesh_virtual_gateway.test" acmCAResourceName := "aws_acmpca_certificate_authority.test" acmCertificateResourceName := "aws_acm_certificate.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") vgName := acctest.RandomWithPrefix("tf-acc-test") + domain := testAccRandomDomainName() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -433,7 +509,9 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.0.certificate_chain", "/cert_chain.pem"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.0.private_key", "/key.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "PERMISSIVE"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttrSet(resourceName, "created_date"), resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), @@ -449,14 +527,14 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { }, // We need to create and activate the CA before issuing a certificate. { - Config: testAccAppmeshVirtualGatewayConfigRootCA(vgName), + Config: testAccAppmeshVirtualGatewayConfigRootCA(domain), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(acmCAResourceName, &ca), testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(&ca), ), }, { - Config: testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName), + Config: testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName, domain), Check: resource.ComposeTestCheckFunc( testAccCheckAppmeshVirtualGatewayExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), @@ -474,7 +552,9 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "1"), resource.TestCheckResourceAttrPair(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.0.certificate_arn", acmCertificateResourceName, "arn"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "STRICT"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttrSet(resourceName, "created_date"), resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), @@ -489,7 +569,7 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName), + Config: testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName, domain), Check: resource.ComposeTestCheckFunc( // CA must be DISABLED for deletion. testAccCheckAwsAcmpcaCertificateAuthorityDisableCA(&ca), @@ -500,6 +580,104 @@ func testAccAwsAppmeshVirtualGateway_ListenerTls(t *testing.T) { }) } +func testAccAwsAppmeshVirtualGateway_ListenerValidation(t *testing.T) { + var v appmesh.VirtualGatewayData + resourceName := "aws_appmesh_virtual_gateway.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") + vgName := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppmeshVirtualGatewayConfigListenerValidation(meshName, vgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualGatewayExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "name", vgName), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.protocol", "http"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.0.secret_name", "very-secret"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "PERMISSIVE"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "abc.example.com"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "xyz.example.com"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualGateway/%s", meshName, vgName)), + ), + }, + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s/%s", meshName, vgName), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppmeshVirtualGatewayConfigListenerValidationUpdated(meshName, vgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualGatewayExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "name", vgName), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.protocol", "http"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.0.secret_name", "top-secret"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "STRICT"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.0.secret_name", "confidential"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualGateway/%s", meshName, vgName)), + ), + }, + }, + }) +} + func testAccAwsAppmeshVirtualGateway_Logging(t *testing.T) { var v appmesh.VirtualGatewayData resourceName := "aws_appmesh_virtual_gateway.test" @@ -508,6 +686,7 @@ func testAccAwsAppmeshVirtualGateway_Logging(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -579,6 +758,7 @@ func testAccAwsAppmeshVirtualGateway_Tags(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualGatewayDestroy, Steps: []resource.TestStep{ @@ -760,6 +940,55 @@ resource "aws_appmesh_virtual_gateway" "test" { `, meshName, vgName) } +func testAccAppmeshVirtualGatewayConfigBackendDefaultsCertificate(meshName, vgName string) string { + return fmt.Sprintf(` +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +resource "aws_appmesh_virtual_gateway" "test" { + name = %[2]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + listener { + port_mapping { + port = 8080 + protocol = "http" + } + } + + backend_defaults { + client_policy { + tls { + certificate { + file { + certificate_chain = "/cert_chain.pem" + private_key = "tell-nobody" + } + } + + validation { + subject_alternative_names { + match { + exact = ["def.example.com"] + } + } + + trust { + sds { + secret_name = "restricted" + } + } + } + } + } + } + } +} +`, meshName, vgName) +} + func testAccAppmeshVirtualGatewayConfigListenerConnectionPool(meshName, vgName string) string { return fmt.Sprintf(` resource "aws_appmesh_mesh" "test" { @@ -879,7 +1108,7 @@ resource "aws_appmesh_virtual_gateway" "test" { `, meshName, vgName) } -func testAccAppmeshVirtualGatewayConfigRootCA(rName string) string { +func testAccAppmeshVirtualGatewayConfigRootCA(domain string) string { return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -890,23 +1119,23 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "%[1]s.com" + common_name = %[1]q } } } -`, rName) +`, domain) } -func testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName string) string { +func testAccAppmeshVirtualGatewayConfigListenerTlsAcm(meshName, vgName, domain string) string { return composeConfig( - testAccAppmeshVirtualGatewayConfigRootCA(vgName), + testAccAppmeshVirtualGatewayConfigRootCA(domain), fmt.Sprintf(` resource "aws_appmesh_mesh" "test" { name = %[1]q } resource "aws_acm_certificate" "test" { - domain_name = "test.%[2]s.com" + domain_name = "test.%[3]s" certificate_authority_arn = aws_acmpca_certificate_authority.test.arn } @@ -933,7 +1162,7 @@ resource "aws_appmesh_virtual_gateway" "test" { } } } -`, meshName, vgName)) +`, meshName, vgName, domain)) } func testAccAppmeshVirtualGatewayConfigListenerTlsFile(meshName, vgName string) string { @@ -969,6 +1198,92 @@ resource "aws_appmesh_virtual_gateway" "test" { `, meshName, vgName) } +func testAccAppmeshVirtualGatewayConfigListenerValidation(meshName, vgName string) string { + return fmt.Sprintf(` +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +resource "aws_appmesh_virtual_gateway" "test" { + name = %[2]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + listener { + port_mapping { + port = 8080 + protocol = "http" + } + + tls { + certificate { + sds { + secret_name = "very-secret" + } + } + + mode = "PERMISSIVE" + + validation { + subject_alternative_names { + match { + exact = ["abc.example.com", "xyz.example.com"] + } + } + + trust { + file { + certificate_chain = "/cert_chain.pem" + } + } + } + } + } + } +} +`, meshName, vgName) +} + +func testAccAppmeshVirtualGatewayConfigListenerValidationUpdated(meshName, vgName string) string { + return fmt.Sprintf(` +resource "aws_appmesh_mesh" "test" { + name = %[1]q +} + +resource "aws_appmesh_virtual_gateway" "test" { + name = %[2]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + listener { + port_mapping { + port = 8080 + protocol = "http" + } + + tls { + certificate { + sds { + secret_name = "top-secret" + } + } + + mode = "STRICT" + + validation { + trust { + sds { + secret_name = "confidential" + } + } + } + } + } + } +} +`, meshName, vgName) +} + func testAccAppmeshVirtualGatewayConfigLogging(meshName, vgName, path string) string { return fmt.Sprintf(` resource "aws_appmesh_mesh" "test" { diff --git a/aws/resource_aws_appmesh_virtual_node.go b/aws/resource_aws_appmesh_virtual_node.go index 8f0bbaf2d15c..14015916546e 100644 --- a/aws/resource_aws_appmesh_virtual_node.go +++ b/aws/resource_aws_appmesh_virtual_node.go @@ -8,9 +8,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appmesh/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsAppmeshVirtualNode() *schema.Resource { @@ -61,7 +65,7 @@ func resourceAwsAppmeshVirtualNode() *schema.Resource { Type: schema.TypeSet, Optional: true, MinItems: 0, - MaxItems: 25, + MaxItems: 50, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "virtual_service": { @@ -594,7 +598,11 @@ func resourceAwsAppmeshVirtualNode() *schema.Resource { }, }, }, - ExactlyOneOf: []string{"spec.0.listener.0.tls.0.certificate.0.acm", "spec.0.listener.0.tls.0.certificate.0.file"}, + ExactlyOneOf: []string{ + "spec.0.listener.0.tls.0.certificate.0.acm", + "spec.0.listener.0.tls.0.certificate.0.file", + "spec.0.listener.0.tls.0.certificate.0.sds", + }, }, "file": { @@ -617,7 +625,31 @@ func resourceAwsAppmeshVirtualNode() *schema.Resource { }, }, }, - ExactlyOneOf: []string{"spec.0.listener.0.tls.0.certificate.0.acm", "spec.0.listener.0.tls.0.certificate.0.file"}, + ExactlyOneOf: []string{ + "spec.0.listener.0.tls.0.certificate.0.acm", + "spec.0.listener.0.tls.0.certificate.0.file", + "spec.0.listener.0.tls.0.certificate.0.sds", + }, + }, + + "sds": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + ExactlyOneOf: []string{ + "spec.0.listener.0.tls.0.certificate.0.acm", + "spec.0.listener.0.tls.0.certificate.0.file", + "spec.0.listener.0.tls.0.certificate.0.sds", + }, }, }, }, @@ -628,6 +660,93 @@ func resourceAwsAppmeshVirtualNode() *schema.Resource { Required: true, ValidateFunc: validation.StringInSlice(appmesh.ListenerTlsMode_Values(), false), }, + + "validation": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subject_alternative_names": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "match": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exact": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, + }, + }, + }, + + "trust": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "file": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate_chain": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + ExactlyOneOf: []string{ + "spec.0.listener.0.tls.0.validation.0.trust.0.file", + "spec.0.listener.0.tls.0.validation.0.trust.0.sds", + }, + }, + + "sds": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + ExactlyOneOf: []string{ + "spec.0.listener.0.tls.0.validation.0.trust.0.file", + "spec.0.listener.0.tls.0.validation.0.trust.0.sds", + }, + }, + }, + }, + }, + }, + }, + }, }, }, }, @@ -751,7 +870,11 @@ func resourceAwsAppmeshVirtualNode() *schema.Resource { }, "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } @@ -771,6 +894,53 @@ func appmeshVirtualNodeClientPolicySchema() *schema.Schema { MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "certificate": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "file": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate_chain": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "private_key": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + }, + + "sds": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "enforce": { Type: schema.TypeBool, Optional: true, @@ -791,6 +961,33 @@ func appmeshVirtualNodeClientPolicySchema() *schema.Schema { MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "subject_alternative_names": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "match": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exact": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, + }, + }, + }, + "trust": { Type: schema.TypeList, Required: true, @@ -830,6 +1027,22 @@ func appmeshVirtualNodeClientPolicySchema() *schema.Schema { }, }, }, + + "sds": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + }, + }, + }, }, }, }, @@ -846,12 +1059,14 @@ func appmeshVirtualNodeClientPolicySchema() *schema.Schema { func resourceAwsAppmeshVirtualNodeCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &appmesh.CreateVirtualNodeInput{ MeshName: aws.String(d.Get("mesh_name").(string)), VirtualNodeName: aws.String(d.Get("name").(string)), Spec: expandAppmeshVirtualNodeSpec(d.Get("spec").([]interface{})), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AppmeshTags(), + Tags: tags.IgnoreAws().AppmeshTags(), } if v, ok := d.GetOk("mesh_owner"); ok { req.MeshOwner = aws.String(v.(string)) @@ -871,6 +1086,7 @@ func resourceAwsAppmeshVirtualNodeCreate(d *schema.ResourceData, meta interface{ func resourceAwsAppmeshVirtualNodeRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig req := &appmesh.DescribeVirtualNodeInput{ @@ -881,20 +1097,48 @@ func resourceAwsAppmeshVirtualNodeRead(d *schema.ResourceData, meta interface{}) req.MeshOwner = aws.String(v.(string)) } - resp, err := conn.DescribeVirtualNode(req) + var resp *appmesh.DescribeVirtualNodeOutput - if isAWSErr(err, appmesh.ErrCodeNotFoundException, "") { - log.Printf("[WARN] App Mesh virtual node (%s) not found, removing from state", d.Id()) + err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + + resp, err = conn.DescribeVirtualNode(req) + + if d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + resp, err = conn.DescribeVirtualNode(req) + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + log.Printf("[WARN] App Mesh Virtual Node (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading App Mesh virtual node (%s): %w", d.Id(), err) + return fmt.Errorf("error reading App Mesh Virtual Node: %w", err) + } + + if resp == nil || resp.VirtualNode == nil { + return fmt.Errorf("error reading App Mesh Virtual Node: empty response") } if aws.StringValue(resp.VirtualNode.Status.Status) == appmesh.VirtualNodeStatusCodeDeleted { - log.Printf("[WARN] App Mesh virtual node (%s) not found, removing from state", d.Id()) + if d.IsNewResource() { + return fmt.Errorf("error reading App Mesh Virtual Node: %s after creation", aws.StringValue(resp.VirtualNode.Status.Status)) + } + + log.Printf("[WARN] App Mesh Virtual Node (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -918,10 +1162,17 @@ func resourceAwsAppmeshVirtualNodeRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error listing tags for App Mesh virtual node (%s): %w", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } @@ -948,8 +1199,8 @@ func resourceAwsAppmeshVirtualNodeUpdate(d *schema.ResourceData, meta interface{ } arn := d.Get("arn").(string) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AppmeshUpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating App Mesh virtual node (%s) tags: %w", arn, err) diff --git a/aws/resource_aws_appmesh_virtual_node_test.go b/aws/resource_aws_appmesh_virtual_node_test.go index 0fc1783a87bc..2f4ca986ab07 100644 --- a/aws/resource_aws_appmesh_virtual_node_test.go +++ b/aws/resource_aws_appmesh_virtual_node_test.go @@ -30,9 +30,9 @@ func testSweepAppmeshVirtualNodes(region string) error { var sweeperErrs *multierror.Error - err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, isLast bool) bool { + err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, mesh := range page.Meshes { @@ -41,9 +41,9 @@ func testSweepAppmeshVirtualNodes(region string) error { } meshName := aws.StringValue(mesh.MeshName) - err := conn.ListVirtualNodesPages(listVirtualNodesInput, func(page *appmesh.ListVirtualNodesOutput, isLast bool) bool { + err := conn.ListVirtualNodesPages(listVirtualNodesInput, func(page *appmesh.ListVirtualNodesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, virtualNode := range page.VirtualNodes { @@ -64,7 +64,7 @@ func testSweepAppmeshVirtualNodes(region string) error { } } - return !isLast + return !lastPage }) if err != nil { @@ -72,7 +72,7 @@ func testSweepAppmeshVirtualNodes(region string) error { } } - return !isLast + return !lastPage }) if err != nil { if testSweepSkipSweepError(err) { @@ -93,6 +93,7 @@ func testAccAwsAppmeshVirtualNode_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -133,6 +134,7 @@ func testAccAwsAppmeshVirtualNode_disappears(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -153,24 +155,27 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyAcm(t *testing.T) { var ca acmpca.CertificateAuthority resourceName := "aws_appmesh_virtual_node.test" acmCAResourceName := "aws_acmpca_certificate_authority.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") vnName := acctest.RandomWithPrefix("tf-acc-test") + domain := testAccRandomDomainName() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ // We need to create and activate the CA before issuing a certificate. { - Config: testAccAppmeshVirtualNodeConfigRootCA(vnName), + Config: testAccAppmeshVirtualNodeConfigRootCA(domain), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(acmCAResourceName, &ca), testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(&ca), ), }, { - Config: testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName), + Config: testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName, domain), Check: resource.ComposeTestCheckFunc( testAccCheckAppmeshVirtualNodeExists(resourceName, &vn), resource.TestCheckResourceAttr(resourceName, "name", vnName), @@ -179,16 +184,19 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyAcm(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "spec.0.backend.*", map[string]string{ - "virtual_service.#": "1", - "virtual_service.0.client_policy.#": "1", - "virtual_service.0.client_policy.0.tls.#": "1", - "virtual_service.0.client_policy.0.tls.0.enforce": "true", - "virtual_service.0.client_policy.0.tls.0.ports.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.#": "0", - "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", + "virtual_service.#": "1", + "virtual_service.0.client_policy.#": "1", + "virtual_service.0.client_policy.0.tls.#": "1", + "virtual_service.0.client_policy.0.tls.0.certificate.#": "0", + "virtual_service.0.client_policy.0.tls.0.enforce": "true", + "virtual_service.0.client_policy.0.tls.0.ports.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#": "0", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.#": "0", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.sds.#": "0", + "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", }), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.ports.*", "8443"), resource.TestCheckTypeSetElemAttrPair(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.0.certificate_authority_arns.*", acmCAResourceName, "arn"), @@ -218,7 +226,7 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyAcm(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName), + Config: testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName, domain), Check: resource.ComposeTestCheckFunc( // CA must be DISABLED for deletion. testAccCheckAwsAcmpcaCertificateAuthorityDisableCA(&ca), @@ -237,6 +245,7 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyFile(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -253,13 +262,16 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyFile(t *testing.T) { "virtual_service.#": "1", "virtual_service.0.client_policy.#": "1", "virtual_service.0.client_policy.0.tls.#": "1", + "virtual_service.0.client_policy.0.tls.0.certificate.#": "0", "virtual_service.0.client_policy.0.tls.0.enforce": "true", "virtual_service.0.client_policy.0.tls.0.ports.#": "1", "virtual_service.0.client_policy.0.tls.0.validation.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#": "0", "virtual_service.0.client_policy.0.tls.0.validation.0.trust.#": "1", "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.#": "0", "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.#": "1", "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain": "/cert_chain.pem", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.sds.#": "0", "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", }), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.ports.*", "8443"), @@ -292,20 +304,24 @@ func testAccAwsAppmeshVirtualNode_backendClientPolicyFile(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "spec.0.backend.*", map[string]string{ - "virtual_service.#": "1", - "virtual_service.0.client_policy.#": "1", - "virtual_service.0.client_policy.0.tls.#": "1", - "virtual_service.0.client_policy.0.tls.0.enforce": "true", - "virtual_service.0.client_policy.0.tls.0.ports.#": "2", - "virtual_service.0.client_policy.0.tls.0.validation.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.#": "0", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.#": "1", - "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain": "/etc/ssl/certs/cert_chain.pem", - "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", + "virtual_service.#": "1", + "virtual_service.0.client_policy.#": "1", + "virtual_service.0.client_policy.0.tls.#": "1", + "virtual_service.0.client_policy.0.tls.0.enforce": "true", + "virtual_service.0.client_policy.0.tls.0.ports.#": "2", + "virtual_service.0.client_policy.0.tls.0.validation.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.acm.#": "0", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.#": "1", + "virtual_service.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain": "/etc/ssl/certs/cert_chain.pem", + "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", }), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.ports.*", "443"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.ports.*", "8443"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend.*.virtual_service.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "abc.example.com"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), @@ -343,6 +359,7 @@ func testAccAwsAppmeshVirtualNode_backendDefaults(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -358,14 +375,17 @@ func testAccAwsAppmeshVirtualNode_backendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "1"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "8443"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "0"), @@ -387,15 +407,79 @@ func testAccAwsAppmeshVirtualNode_backendDefaults(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "2"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "443"), resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "8443"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/etc/ssl/certs/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualNode/%s", meshName, vnName)), + ), + }, + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s/%s", meshName, vnName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAwsAppmeshVirtualNode_backendDefaultsCertificate(t *testing.T) { + var vn appmesh.VirtualNodeData + resourceName := "aws_appmesh_virtual_node.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") + vnName := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppmeshVirtualNodeConfig_backendDefaultsCertificate(meshName, vnName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualNodeExists(resourceName, &vn), + resource.TestCheckResourceAttr(resourceName, "name", vnName), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.file.0.private_key", "tell-nobody"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.certificate.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.enforce", "true"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.ports.*", "8443"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "def.example.com"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.0.client_policy.0.tls.0.validation.0.trust.0.sds.0.secret_name", "restricted"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "0"), @@ -426,6 +510,7 @@ func testAccAwsAppmeshVirtualNode_cloudMapServiceDiscovery(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -477,6 +562,7 @@ func testAccAwsAppmeshVirtualNode_listenerConnectionPool(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -565,6 +651,7 @@ func testAccAwsAppmeshVirtualNode_listenerHealthChecks(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -672,6 +759,7 @@ func testAccAwsAppmeshVirtualNode_listenerOutlierDetection(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -765,6 +853,7 @@ func testAccAwsAppmeshVirtualNode_listenerTimeout(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -857,11 +946,14 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { resourceName := "aws_appmesh_virtual_node.test" acmCAResourceName := "aws_acmpca_certificate_authority.test" acmCertificateResourceName := "aws_acm_certificate.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") vnName := acctest.RandomWithPrefix("tf-acc-test") + domain := testAccRandomDomainName() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -893,7 +985,9 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.0.certificate_chain", "/cert_chain.pem"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.0.private_key", "/key.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "PERMISSIVE"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.#", "1"), @@ -912,14 +1006,14 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { }, // We need to create and activate the CA before issuing a certificate. { - Config: testAccAppmeshVirtualNodeConfigRootCA(vnName), + Config: testAccAppmeshVirtualNodeConfigRootCA(domain), Check: resource.ComposeTestCheckFunc( testAccCheckAwsAcmpcaCertificateAuthorityExists(acmCAResourceName, &ca), testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(&ca), ), }, { - Config: testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName), + Config: testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName, domain), Check: resource.ComposeTestCheckFunc( testAccCheckAppmeshVirtualNodeExists(resourceName, &vn), resource.TestCheckResourceAttr(resourceName, "name", vnName), @@ -945,7 +1039,9 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "1"), resource.TestCheckResourceAttrPair(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.0.certificate_arn", acmCertificateResourceName, "arn"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "STRICT"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "1"), resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.#", "1"), @@ -963,7 +1059,7 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName), + Config: testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName, domain), Check: resource.ComposeTestCheckFunc( // CA must be DISABLED for deletion. testAccCheckAwsAcmpcaCertificateAuthorityDisableCA(&ca), @@ -974,6 +1070,124 @@ func testAccAwsAppmeshVirtualNode_listenerTls(t *testing.T) { }) } +func testAccAwsAppmeshVirtualNode_listenerValidation(t *testing.T) { + var vn appmesh.VirtualNodeData + resourceName := "aws_appmesh_virtual_node.test" + meshName := acctest.RandomWithPrefix("tf-acc-test") + vnName := acctest.RandomWithPrefix("tf-acc-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppmeshVirtualNodeConfig_listenerValidation(meshName, vnName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualNodeExists(resourceName, &vn), + resource.TestCheckResourceAttr(resourceName, "name", vnName), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "spec.0.backend.*", map[string]string{ + "virtual_service.#": "1", + "virtual_service.0.client_policy.#": "0", + "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", + }), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.outlier_detection.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.protocol", "http"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.0.secret_name", "very-secret"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "PERMISSIVE"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "abc.example.com"), + resource.TestCheckTypeSetElemAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.0.match.0.exact.*", "xyz.example.com"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.0.certificate_chain", "/cert_chain.pem"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.0.hostname", "serviceb.simpleapp.local"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualNode/%s", meshName, vnName)), + ), + }, + { + ResourceName: resourceName, + ImportStateId: fmt.Sprintf("%s/%s", meshName, vnName), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppmeshVirtualNodeConfig_listenerValidationUpdated(meshName, vnName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAppmeshVirtualNodeExists(resourceName, &vn), + resource.TestCheckResourceAttr(resourceName, "name", vnName), + resource.TestCheckResourceAttr(resourceName, "mesh_name", meshName), + testAccCheckResourceAttrAccountID(resourceName, "mesh_owner"), + resource.TestCheckResourceAttr(resourceName, "spec.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "spec.0.backend.*", map[string]string{ + "virtual_service.#": "1", + "virtual_service.0.client_policy.#": "0", + "virtual_service.0.virtual_service_name": "servicea.simpleapp.local", + }), + resource.TestCheckResourceAttr(resourceName, "spec.0.backend_defaults.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.connection_pool.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.outlier_detection.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.port_mapping.0.protocol", "http"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.certificate.0.sds.0.secret_name", "top-secret"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.mode", "PERMISSIVE"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.acm.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.file.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.listener.0.tls.0.validation.0.trust.0.sds.0.secret_name", "confidential"), + resource.TestCheckResourceAttr(resourceName, "spec.0.logging.#", "0"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "spec.0.service_discovery.0.dns.0.hostname", "serviceb.simpleapp.local"), + resource.TestCheckResourceAttrSet(resourceName, "created_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_date"), + testAccCheckResourceAttrAccountID(resourceName, "resource_owner"), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "appmesh", fmt.Sprintf("mesh/%s/virtualNode/%s", meshName, vnName)), + ), + }, + }, + }) +} + func testAccAwsAppmeshVirtualNode_logging(t *testing.T) { var vn appmesh.VirtualNodeData resourceName := "aws_appmesh_virtual_node.test" @@ -982,6 +1196,7 @@ func testAccAwsAppmeshVirtualNode_logging(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -1030,6 +1245,7 @@ func testAccAwsAppmeshVirtualNode_tags(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualNodeDestroy, Steps: []resource.TestStep{ @@ -1126,7 +1342,7 @@ resource "aws_appmesh_mesh" "test" { `, rName) } -func testAccAppmeshVirtualNodeConfigRootCA(rName string) string { +func testAccAppmeshVirtualNodeConfigRootCA(domain string) string { return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -1137,20 +1353,20 @@ resource "aws_acmpca_certificate_authority" "test" { signing_algorithm = "SHA512WITHRSA" subject { - common_name = "%[1]s.com" + common_name = %[1]q } } } -`, rName) +`, domain) } -func testAccAppmeshVirtualNodeConfigPrivateCert(rName string) string { +func testAccAppmeshVirtualNodeConfigPrivateCert(domain string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { - domain_name = "test.%[1]s.com" + domain_name = "test.%[1]s" certificate_authority_arn = aws_acmpca_certificate_authority.test.arn } -`, rName) +`, domain) } func testAccAppmeshVirtualNodeConfig_basic(meshName, vnName string) string { @@ -1218,10 +1434,50 @@ resource "aws_appmesh_virtual_node" "test" { `, vnName)) } -func testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName string) string { +func testAccAppmeshVirtualNodeConfig_backendDefaultsCertificate(meshName, vnName string) string { + return composeConfig(testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` +resource "aws_appmesh_virtual_node" "test" { + name = %[1]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + backend_defaults { + client_policy { + tls { + certificate { + file { + certificate_chain = "/cert_chain.pem" + private_key = "tell-nobody" + } + } + + ports = [8443] + + validation { + subject_alternative_names { + match { + exact = ["def.example.com"] + } + } + + trust { + sds { + secret_name = "restricted" + } + } + } + } + } + } + } +} +`, vnName)) +} + +func testAccAppmeshVirtualNodeConfig_backendClientPolicyAcm(meshName, vnName, domain string) string { return composeConfig( - testAccAppmeshVirtualNodeConfigRootCA(vnName), - testAccAppmeshVirtualNodeConfigPrivateCert(vnName), + testAccAppmeshVirtualNodeConfigRootCA(domain), + testAccAppmeshVirtualNodeConfigPrivateCert(domain), testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` resource "aws_appmesh_virtual_node" "test" { @@ -1326,6 +1582,12 @@ resource "aws_appmesh_virtual_node" "test" { ports = [443, 8443] validation { + subject_alternative_names { + match { + exact = ["abc.example.com"] + } + } + trust { file { certificate_chain = "/etc/ssl/certs/cert_chain.pem" @@ -1764,10 +2026,10 @@ resource "aws_appmesh_virtual_node" "test" { `, vnName)) } -func testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName string) string { +func testAccAppmeshVirtualNodeConfig_listenerTlsAcm(meshName, vnName, domain string) string { return composeConfig( - testAccAppmeshVirtualNodeConfigRootCA(vnName), - testAccAppmeshVirtualNodeConfigPrivateCert(vnName), + testAccAppmeshVirtualNodeConfigRootCA(domain), + testAccAppmeshVirtualNodeConfigPrivateCert(domain), testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` resource "aws_appmesh_virtual_node" "test" { @@ -1808,6 +2070,108 @@ resource "aws_appmesh_virtual_node" "test" { `, vnName)) } +func testAccAppmeshVirtualNodeConfig_listenerValidation(meshName, vnName string) string { + return composeConfig(testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` +resource "aws_appmesh_virtual_node" "test" { + name = %[1]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + backend { + virtual_service { + virtual_service_name = "servicea.simpleapp.local" + } + } + + listener { + port_mapping { + port = 8080 + protocol = "http" + } + + tls { + certificate { + sds { + secret_name = "very-secret" + } + } + + mode = "PERMISSIVE" + + validation { + subject_alternative_names { + match { + exact = ["abc.example.com", "xyz.example.com"] + } + } + + trust { + file { + certificate_chain = "/cert_chain.pem" + } + } + } + } + } + + service_discovery { + dns { + hostname = "serviceb.simpleapp.local" + } + } + } +} +`, vnName)) +} + +func testAccAppmeshVirtualNodeConfig_listenerValidationUpdated(meshName, vnName string) string { + return composeConfig(testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` +resource "aws_appmesh_virtual_node" "test" { + name = %[1]q + mesh_name = aws_appmesh_mesh.test.id + + spec { + backend { + virtual_service { + virtual_service_name = "servicea.simpleapp.local" + } + } + + listener { + port_mapping { + port = 8080 + protocol = "http" + } + + tls { + certificate { + sds { + secret_name = "top-secret" + } + } + + mode = "PERMISSIVE" + + validation { + trust { + sds { + secret_name = "confidential" + } + } + } + } + } + + service_discovery { + dns { + hostname = "serviceb.simpleapp.local" + } + } + } +} +`, vnName)) +} + func testAccAppmeshVirtualNodeConfig_logging(meshName, vnName, path string) string { return composeConfig(testAccAppmeshVirtualNodeConfig_mesh(meshName), fmt.Sprintf(` resource "aws_appmesh_virtual_node" "test" { diff --git a/aws/resource_aws_appmesh_virtual_router.go b/aws/resource_aws_appmesh_virtual_router.go index 888b1ee7b0aa..808afb9c69c0 100644 --- a/aws/resource_aws_appmesh_virtual_router.go +++ b/aws/resource_aws_appmesh_virtual_router.go @@ -8,9 +8,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appmesh/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsAppmeshVirtualRouter() *schema.Resource { @@ -113,18 +117,24 @@ func resourceAwsAppmeshVirtualRouter() *schema.Resource { }, "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAppmeshVirtualRouterCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &appmesh.CreateVirtualRouterInput{ MeshName: aws.String(d.Get("mesh_name").(string)), VirtualRouterName: aws.String(d.Get("name").(string)), Spec: expandAppmeshVirtualRouterSpec(d.Get("spec").([]interface{})), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AppmeshTags(), + Tags: tags.IgnoreAws().AppmeshTags(), } if v, ok := d.GetOk("mesh_owner"); ok { req.MeshOwner = aws.String(v.(string)) @@ -143,6 +153,7 @@ func resourceAwsAppmeshVirtualRouterCreate(d *schema.ResourceData, meta interfac func resourceAwsAppmeshVirtualRouterRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig req := &appmesh.DescribeVirtualRouterInput{ @@ -153,17 +164,48 @@ func resourceAwsAppmeshVirtualRouterRead(d *schema.ResourceData, meta interface{ req.MeshOwner = aws.String(v.(string)) } - resp, err := conn.DescribeVirtualRouter(req) - if isAWSErr(err, appmesh.ErrCodeNotFoundException, "") { - log.Printf("[WARN] App Mesh virtual router (%s) not found, removing from state", d.Id()) + var resp *appmesh.DescribeVirtualRouterOutput + + err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + + resp, err = conn.DescribeVirtualRouter(req) + + if d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + resp, err = conn.DescribeVirtualRouter(req) + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + log.Printf("[WARN] App Mesh Virtual Router (%s) not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { - return fmt.Errorf("error reading App Mesh virtual router: %s", err) + return fmt.Errorf("error reading App Mesh Virtual Router: %w", err) + } + + if resp == nil || resp.VirtualRouter == nil { + return fmt.Errorf("error reading App Mesh Virtual Router: empty response") } + if aws.StringValue(resp.VirtualRouter.Status.Status) == appmesh.VirtualRouterStatusCodeDeleted { - log.Printf("[WARN] App Mesh virtual router (%s) not found, removing from state", d.Id()) + if d.IsNewResource() { + return fmt.Errorf("error reading App Mesh Virtual Router: %s after creation", aws.StringValue(resp.VirtualRouter.Status.Status)) + } + + log.Printf("[WARN] App Mesh Virtual Router (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -187,8 +229,15 @@ func resourceAwsAppmeshVirtualRouterRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("error listing tags for App Mesh virtual router (%s): %s", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -216,8 +265,8 @@ func resourceAwsAppmeshVirtualRouterUpdate(d *schema.ResourceData, meta interfac } arn := d.Get("arn").(string) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AppmeshUpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating App Mesh virtual router (%s) tags: %s", arn, err) diff --git a/aws/resource_aws_appmesh_virtual_router_test.go b/aws/resource_aws_appmesh_virtual_router_test.go index 78c5208cfa64..f0892cad7c94 100644 --- a/aws/resource_aws_appmesh_virtual_router_test.go +++ b/aws/resource_aws_appmesh_virtual_router_test.go @@ -29,9 +29,9 @@ func testSweepAppmeshVirtualRouters(region string) error { } conn := client.(*AWSClient).appmeshconn - err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, isLast bool) bool { + err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, mesh := range page.Meshes { @@ -40,9 +40,9 @@ func testSweepAppmeshVirtualRouters(region string) error { } meshName := aws.StringValue(mesh.MeshName) - err := conn.ListVirtualRoutersPages(listVirtualRoutersInput, func(page *appmesh.ListVirtualRoutersOutput, isLast bool) bool { + err := conn.ListVirtualRoutersPages(listVirtualRoutersInput, func(page *appmesh.ListVirtualRoutersOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, virtualRouter := range page.VirtualRouters { @@ -60,7 +60,7 @@ func testSweepAppmeshVirtualRouters(region string) error { } } - return !isLast + return !lastPage }) if err != nil { @@ -68,7 +68,7 @@ func testSweepAppmeshVirtualRouters(region string) error { } } - return !isLast + return !lastPage }) if err != nil { if testSweepSkipSweepError(err) { @@ -89,6 +89,7 @@ func testAccAwsAppmeshVirtualRouter_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualRouterDestroy, Steps: []resource.TestStep{ @@ -142,6 +143,7 @@ func testAccAwsAppmeshVirtualRouter_tags(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualRouterDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appmesh_virtual_service.go b/aws/resource_aws_appmesh_virtual_service.go index 94b71d7bf704..2a68730e7aba 100644 --- a/aws/resource_aws_appmesh_virtual_service.go +++ b/aws/resource_aws_appmesh_virtual_service.go @@ -8,9 +8,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appmesh" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appmesh/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsAppmeshVirtualService() *schema.Resource { @@ -121,18 +125,24 @@ func resourceAwsAppmeshVirtualService() *schema.Resource { }, "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAppmeshVirtualServiceCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) req := &appmesh.CreateVirtualServiceInput{ MeshName: aws.String(d.Get("mesh_name").(string)), VirtualServiceName: aws.String(d.Get("name").(string)), Spec: expandAppmeshVirtualServiceSpec(d.Get("spec").([]interface{})), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().AppmeshTags(), + Tags: tags.IgnoreAws().AppmeshTags(), } if v, ok := d.GetOk("mesh_owner"); ok { req.MeshOwner = aws.String(v.(string)) @@ -151,6 +161,7 @@ func resourceAwsAppmeshVirtualServiceCreate(d *schema.ResourceData, meta interfa func resourceAwsAppmeshVirtualServiceRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appmeshconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig req := &appmesh.DescribeVirtualServiceInput{ @@ -161,17 +172,48 @@ func resourceAwsAppmeshVirtualServiceRead(d *schema.ResourceData, meta interface req.MeshOwner = aws.String(v.(string)) } - resp, err := conn.DescribeVirtualService(req) - if isAWSErr(err, appmesh.ErrCodeNotFoundException, "") { - log.Printf("[WARN] App Mesh virtual service (%s) not found, removing from state", d.Id()) + var resp *appmesh.DescribeVirtualServiceOutput + + err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + + resp, err = conn.DescribeVirtualService(req) + + if d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + resp, err = conn.DescribeVirtualService(req) + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appmesh.ErrCodeNotFoundException) { + log.Printf("[WARN] App Mesh Virtual Service (%s) not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { - return fmt.Errorf("error reading App Mesh virtual service: %s", err) + return fmt.Errorf("error reading App Mesh Virtual Service: %w", err) + } + + if resp == nil || resp.VirtualService == nil { + return fmt.Errorf("error reading App Mesh Virtual Service: empty response") } + if aws.StringValue(resp.VirtualService.Status.Status) == appmesh.VirtualServiceStatusCodeDeleted { - log.Printf("[WARN] App Mesh virtual service (%s) not found, removing from state", d.Id()) + if d.IsNewResource() { + return fmt.Errorf("error reading App Mesh Virtual Service: %s after creation", aws.StringValue(resp.VirtualService.Status.Status)) + } + + log.Printf("[WARN] App Mesh Virtual Service (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -195,8 +237,15 @@ func resourceAwsAppmeshVirtualServiceRead(d *schema.ResourceData, meta interface return fmt.Errorf("error listing tags for App Mesh virtual service (%s): %s", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -224,8 +273,8 @@ func resourceAwsAppmeshVirtualServiceUpdate(d *schema.ResourceData, meta interfa } arn := d.Get("arn").(string) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AppmeshUpdateTags(conn, arn, o, n); err != nil { return fmt.Errorf("error updating App Mesh virtual service (%s) tags: %s", arn, err) diff --git a/aws/resource_aws_appmesh_virtual_service_test.go b/aws/resource_aws_appmesh_virtual_service_test.go index e9828a5f1357..5e09e8447456 100644 --- a/aws/resource_aws_appmesh_virtual_service_test.go +++ b/aws/resource_aws_appmesh_virtual_service_test.go @@ -26,9 +26,9 @@ func testSweepAppmeshVirtualServices(region string) error { } conn := client.(*AWSClient).appmeshconn - err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, isLast bool) bool { + err = conn.ListMeshesPages(&appmesh.ListMeshesInput{}, func(page *appmesh.ListMeshesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, mesh := range page.Meshes { @@ -37,9 +37,9 @@ func testSweepAppmeshVirtualServices(region string) error { } meshName := aws.StringValue(mesh.MeshName) - err := conn.ListVirtualServicesPages(listVirtualServicesInput, func(page *appmesh.ListVirtualServicesOutput, isLast bool) bool { + err := conn.ListVirtualServicesPages(listVirtualServicesInput, func(page *appmesh.ListVirtualServicesOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, virtualService := range page.VirtualServices { @@ -57,7 +57,7 @@ func testSweepAppmeshVirtualServices(region string) error { } } - return !isLast + return !lastPage }) if err != nil { @@ -65,7 +65,7 @@ func testSweepAppmeshVirtualServices(region string) error { } } - return !isLast + return !lastPage }) if err != nil { if testSweepSkipSweepError(err) { @@ -88,6 +88,7 @@ func testAccAwsAppmeshVirtualService_virtualNode(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualServiceDestroy, Steps: []resource.TestStep{ @@ -141,6 +142,7 @@ func testAccAwsAppmeshVirtualService_virtualRouter(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualServiceDestroy, Steps: []resource.TestStep{ @@ -187,6 +189,7 @@ func testAccAwsAppmeshVirtualService_tags(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appmesh.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appmesh.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAppmeshVirtualServiceDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_apprunner_auto_scaling_configuration_version.go b/aws/resource_aws_apprunner_auto_scaling_configuration_version.go new file mode 100644 index 000000000000..419a182ea3c1 --- /dev/null +++ b/aws/resource_aws_apprunner_auto_scaling_configuration_version.go @@ -0,0 +1,231 @@ +package aws + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" +) + +func resourceAwsAppRunnerAutoScalingConfigurationVersion() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppRunnerAutoScalingConfigurationCreate, + ReadWithoutTimeout: resourceAwsAppRunnerAutoScalingConfigurationRead, + UpdateWithoutTimeout: resourceAwsAppRunnerAutoScalingConfigurationUpdate, + DeleteWithoutTimeout: resourceAwsAppRunnerAutoScalingConfigurationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "auto_scaling_configuration_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "auto_scaling_configuration_revision": { + Type: schema.TypeInt, + Computed: true, + }, + "latest": { + Type: schema.TypeBool, + Computed: true, + }, + "max_concurrency": { + Type: schema.TypeInt, + Optional: true, + Default: 100, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 200), + }, + "max_size": { + Type: schema.TypeInt, + Optional: true, + Default: 25, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 25), + }, + "min_size": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 25), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsAppRunnerAutoScalingConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("auto_scaling_configuration_name").(string) + + input := &apprunner.CreateAutoScalingConfigurationInput{ + AutoScalingConfigurationName: aws.String(name), + } + + if v, ok := d.GetOk("max_concurrency"); ok { + input.MaxConcurrency = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("max_size"); ok { + input.MaxSize = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("min_size"); ok { + input.MinSize = aws.Int64(int64(v.(int))) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().ApprunnerTags() + } + + output, err := conn.CreateAutoScalingConfigurationWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating App Runner AutoScaling Configuration Version (%s): %w", name, err)) + } + + if output == nil || output.AutoScalingConfiguration == nil { + return diag.FromErr(fmt.Errorf("error creating App Runner AutoScaling Configuration Version (%s): empty output", name)) + } + + d.SetId(aws.StringValue(output.AutoScalingConfiguration.AutoScalingConfigurationArn)) + + if err := waiter.AutoScalingConfigurationActive(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for AutoScaling Configuration Version (%s) creation: %w", d.Id(), err)) + } + + return resourceAwsAppRunnerAutoScalingConfigurationRead(ctx, d, meta) +} + +func resourceAwsAppRunnerAutoScalingConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &apprunner.DescribeAutoScalingConfigurationInput{ + AutoScalingConfigurationArn: aws.String(d.Id()), + } + + output, err := conn.DescribeAutoScalingConfigurationWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] App Runner AutoScaling Configuration Version (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading App Runner AutoScaling Configuration Version (%s): %w", d.Id(), err)) + } + + if output == nil || output.AutoScalingConfiguration == nil { + return diag.FromErr(fmt.Errorf("error reading App Runner AutoScaling Configuration Version (%s): empty output", d.Id())) + } + + if aws.StringValue(output.AutoScalingConfiguration.Status) == waiter.AutoScalingConfigurationStatusInactive { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading App Runner AutoScaling Configuration Version (%s): %s after creation", d.Id(), aws.StringValue(output.AutoScalingConfiguration.Status))) + } + log.Printf("[WARN] App Runner AutoScaling Configuration Version (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + config := output.AutoScalingConfiguration + arn := aws.StringValue(config.AutoScalingConfigurationArn) + + d.Set("arn", arn) + d.Set("auto_scaling_configuration_name", config.AutoScalingConfigurationName) + d.Set("auto_scaling_configuration_revision", config.AutoScalingConfigurationRevision) + d.Set("latest", config.Latest) + d.Set("max_concurrency", config.MaxConcurrency) + d.Set("max_size", config.MaxSize) + d.Set("min_size", config.MinSize) + d.Set("status", config.Status) + + tags, err := keyvaluetags.ApprunnerListTags(conn, arn) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing tags for App Runner AutoScaling Configuration Version (%s): %s", arn, err)) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) + } + + return nil +} + +func resourceAwsAppRunnerAutoScalingConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := keyvaluetags.ApprunnerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating App Runner AutoScaling Configuration Version (%s) tags: %s", d.Get("arn").(string), err)) + } + } + + return resourceAwsAppRunnerAutoScalingConfigurationRead(ctx, d, meta) +} + +func resourceAwsAppRunnerAutoScalingConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + input := &apprunner.DeleteAutoScalingConfigurationInput{ + AutoScalingConfigurationArn: aws.String(d.Id()), + } + + _, err := conn.DeleteAutoScalingConfigurationWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting App Runner AutoScaling Configuration Version (%s): %w", d.Id(), err)) + } + + if err := waiter.AutoScalingConfigurationInactive(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error waiting for AutoScaling Configuration Version (%s) deletion: %w", d.Id(), err)) + } + + return nil +} diff --git a/aws/resource_aws_apprunner_auto_scaling_configuration_version_test.go b/aws/resource_aws_apprunner_auto_scaling_configuration_version_test.go new file mode 100644 index 000000000000..de7209c65f33 --- /dev/null +++ b/aws/resource_aws_apprunner_auto_scaling_configuration_version_test.go @@ -0,0 +1,499 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" +) + +func init() { + resource.AddTestSweepers("aws_apprunner_auto_scaling_configuration_version", &resource.Sweeper{ + Name: "aws_apprunner_auto_scaling_configuration_version", + F: testSweepAppRunnerAutoScalingConfigurationVersions, + Dependencies: []string{"aws_apprunner_service"}, + }) +} + +func testSweepAppRunnerAutoScalingConfigurationVersions(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).apprunnerconn + sweepResources := make([]*testSweepResource, 0) + ctx := context.Background() + var errs *multierror.Error + + input := &apprunner.ListAutoScalingConfigurationsInput{} + + err = conn.ListAutoScalingConfigurationsPagesWithContext(ctx, input, func(page *apprunner.ListAutoScalingConfigurationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, summaryConfig := range page.AutoScalingConfigurationSummaryList { + if summaryConfig == nil { + continue + } + + // Skip DefaultConfigurations as deletion not supported by the AppRunner service + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/19840 + if aws.StringValue(summaryConfig.AutoScalingConfigurationName) == "DefaultConfiguration" { + log.Printf("[INFO] Skipping App Runner AutoScaling Configuration: DefaultConfiguration") + continue + } + + arn := aws.StringValue(summaryConfig.AutoScalingConfigurationArn) + + log.Printf("[INFO] Deleting App Runner AutoScaling Configuration Version (%s)", arn) + r := resourceAwsAppRunnerAutoScalingConfigurationVersion() + d := r.Data(nil) + d.SetId(arn) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing App Runner AutoScaling Configuration Versions: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping App Runner AutoScaling Configuration Version for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping App Runner AutoScaling Configuration Versions sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "true"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "100"), + resource.TestCheckResourceAttr(resourceName, "max_size", "25"), + resource.TestCheckResourceAttr(resourceName, "min_size", "1"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_complex(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_withNonDefaults(rName, 50, 10, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "true"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "50"), + resource.TestCheckResourceAttr(resourceName, "max_size", "10"), + resource.TestCheckResourceAttr(resourceName, "min_size", "2"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test resource recreation such that the revision number is still 1 + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_withNonDefaults(rName, 150, 20, 5), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "true"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "150"), + resource.TestCheckResourceAttr(resourceName, "max_size", "20"), + resource.TestCheckResourceAttr(resourceName, "min_size", "5"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test resource recreation such that the revision number is still 1 + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "true"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "100"), + resource.TestCheckResourceAttr(resourceName, "max_size", "25"), + resource.TestCheckResourceAttr(resourceName, "min_size", "1"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + }, + }) +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_MultipleVersions(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + otherResourceName := "aws_apprunner_auto_scaling_configuration_version.other" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_multipleVersions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(otherResourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "true"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "100"), + resource.TestCheckResourceAttr(resourceName, "max_size", "25"), + resource.TestCheckResourceAttr(resourceName, "min_size", "1"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + testAccMatchResourceAttrRegionalARN(otherResourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/2/.+`, rName))), + resource.TestCheckResourceAttr(otherResourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(otherResourceName, "auto_scaling_configuration_revision", "2"), + resource.TestCheckResourceAttr(otherResourceName, "latest", "true"), + resource.TestCheckResourceAttr(otherResourceName, "max_concurrency", "100"), + resource.TestCheckResourceAttr(otherResourceName, "max_size", "25"), + resource.TestCheckResourceAttr(otherResourceName, "min_size", "1"), + resource.TestCheckResourceAttr(otherResourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + { + // Test update of "latest" computed attribute after apply + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_multipleVersions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(otherResourceName), + resource.TestCheckResourceAttr(resourceName, "latest", "false"), + resource.TestCheckResourceAttr(otherResourceName, "latest", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: otherResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_UpdateMultipleVersions(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + otherResourceName := "aws_apprunner_auto_scaling_configuration_version.other" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_multipleVersions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(otherResourceName), + ), + }, + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_updateMultipleVersions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(otherResourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/1/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_configuration_revision", "1"), + resource.TestCheckResourceAttr(resourceName, "latest", "false"), + resource.TestCheckResourceAttr(resourceName, "max_concurrency", "100"), + resource.TestCheckResourceAttr(resourceName, "max_size", "25"), + resource.TestCheckResourceAttr(resourceName, "min_size", "1"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.AutoScalingConfigurationStatusActive), + testAccMatchResourceAttrRegionalARN(otherResourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`autoscalingconfiguration/%s/2/.+`, rName))), + resource.TestCheckResourceAttr(otherResourceName, "auto_scaling_configuration_name", rName), + resource.TestCheckResourceAttr(otherResourceName, "auto_scaling_configuration_revision", "2"), + resource.TestCheckResourceAttr(otherResourceName, "latest", "true"), + resource.TestCheckResourceAttr(otherResourceName, "max_concurrency", "125"), + resource.TestCheckResourceAttr(otherResourceName, "max_size", "20"), + resource.TestCheckResourceAttr(otherResourceName, "min_size", "1"), + resource.TestCheckResourceAttr(otherResourceName, "status", waiter.AutoScalingConfigurationStatusActive), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: otherResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppRunnerAutoScalingConfigurationVersion(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerAutoScalingConfigurationVersion_tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_auto_scaling_configuration_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAppRunnerAutoScalingConfigurationVersionConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppRunnerAutoScalingConfigurationVersionDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_apprunner_auto_scaling_configuration_version" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + input := &apprunner.DescribeAutoScalingConfigurationInput{ + AutoScalingConfigurationArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeAutoScalingConfigurationWithContext(context.Background(), input) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if output != nil && output.AutoScalingConfiguration != nil && aws.StringValue(output.AutoScalingConfiguration.Status) != "inactive" { + return fmt.Errorf("App Runner AutoScaling Configuration (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAppRunnerAutoScalingConfigurationVersionExists(n string) 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 App Runner Service ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + input := &apprunner.DescribeAutoScalingConfigurationInput{ + AutoScalingConfigurationArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeAutoScalingConfigurationWithContext(context.Background(), input) + + if err != nil { + return err + } + + if output == nil || output.AutoScalingConfiguration == nil { + return fmt.Errorf("App Runner AutoScaling Configuration (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q +} +`, rName) +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfig_withNonDefaults(rName string, maxConcurrency, maxSize, minSize int) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q + + max_concurrency = %[2]d + max_size = %[3]d + min_size = %[4]d +} +`, rName, maxConcurrency, maxSize, minSize) +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfig_multipleVersions(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q +} + +resource "aws_apprunner_auto_scaling_configuration_version" "other" { + auto_scaling_configuration_name = aws_apprunner_auto_scaling_configuration_version.test.auto_scaling_configuration_name +} +`, rName) +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfig_updateMultipleVersions(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q +} + +resource "aws_apprunner_auto_scaling_configuration_version" "other" { + auto_scaling_configuration_name = aws_apprunner_auto_scaling_configuration_version.test.auto_scaling_configuration_name + + max_concurrency = 125 + max_size = 20 +} +`, rName) +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfigTags1(rName string, tagKey1 string, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAppRunnerAutoScalingConfigurationVersionConfigTags2(rName string, tagKey1 string, tagValue1 string, tagKey2 string, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/aws/resource_aws_apprunner_connection.go b/aws/resource_aws_apprunner_connection.go new file mode 100644 index 000000000000..f513d894706e --- /dev/null +++ b/aws/resource_aws_apprunner_connection.go @@ -0,0 +1,185 @@ +package aws + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" +) + +func resourceAwsAppRunnerConnection() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppRunnerConnectionCreate, + ReadWithoutTimeout: resourceAwsAppRunnerConnectionRead, + UpdateWithoutTimeout: resourceAwsAppRunnerConnectionUpdate, + DeleteWithoutTimeout: resourceAwsAppRunnerConnectionDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "connection_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "provider_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(apprunner.ProviderType_Values(), false), + ForceNew: true, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), + }, + + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsAppRunnerConnectionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("connection_name").(string) + + input := &apprunner.CreateConnectionInput{ + ConnectionName: aws.String(name), + ProviderType: aws.String(d.Get("provider_type").(string)), + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().ApprunnerTags() + } + + output, err := conn.CreateConnectionWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating App Runner Connection (%s): %w", name, err)) + } + + if output == nil || output.Connection == nil { + return diag.FromErr(fmt.Errorf("error creating App Runner Connection (%s): empty output", name)) + } + + d.SetId(aws.StringValue(output.Connection.ConnectionName)) + + return resourceAwsAppRunnerConnectionRead(ctx, d, meta) +} + +func resourceAwsAppRunnerConnectionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + c, err := finder.ConnectionSummaryByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] App Runner Connection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading App Runner Connection (%s): %w", d.Id(), err)) + } + + if c == nil { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading App Runner Connection (%s): empty output after creation", d.Id())) + } + log.Printf("[WARN] App Runner Connection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + arn := aws.StringValue(c.ConnectionArn) + + d.Set("arn", arn) + d.Set("connection_name", c.ConnectionName) + d.Set("provider_type", c.ProviderType) + d.Set("status", c.Status) + + tags, err := keyvaluetags.ApprunnerListTags(conn, arn) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing tags for App Runner Connection (%s): %w", arn, err)) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) + } + + return nil +} + +func resourceAwsAppRunnerConnectionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := keyvaluetags.ApprunnerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating App Runner Connection (%s) tags: %w", d.Get("arn").(string), err)) + } + } + + return resourceAwsAppRunnerConnectionRead(ctx, d, meta) +} + +func resourceAwsAppRunnerConnectionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + input := &apprunner.DeleteConnectionInput{ + ConnectionArn: aws.String(d.Get("arn").(string)), + } + + _, err := conn.DeleteConnectionWithContext(ctx, input) + + if err != nil { + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error deleting App Runner Connection (%s): %w", d.Id(), err)) + } + + if err := waiter.ConnectionDeleted(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error waiting for App Runner Connection (%s) deletion: %w", d.Id(), err)) + } + + return nil +} diff --git a/aws/resource_aws_apprunner_connection_test.go b/aws/resource_aws_apprunner_connection_test.go new file mode 100644 index 000000000000..538a237f8eaa --- /dev/null +++ b/aws/resource_aws_apprunner_connection_test.go @@ -0,0 +1,274 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/finder" +) + +func init() { + resource.AddTestSweepers("aws_apprunner_connection", &resource.Sweeper{ + Name: "aws_apprunner_connection", + F: testSweepAppRunnerConnections, + Dependencies: []string{"aws_apprunner_service"}, + }) +} + +func testSweepAppRunnerConnections(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).apprunnerconn + sweepResources := make([]*testSweepResource, 0) + ctx := context.Background() + + var errs *multierror.Error + + input := &apprunner.ListConnectionsInput{} + + err = conn.ListConnectionsPagesWithContext(ctx, input, func(page *apprunner.ListConnectionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, c := range page.ConnectionSummaryList { + if c == nil { + continue + } + + name := aws.StringValue(c.ConnectionName) + + log.Printf("[INFO] Deleting App Runner Connection: %s", name) + + r := resourceAwsAppRunnerConnection() + d := r.Data(nil) + d.SetId(name) + d.Set("arn", c.ConnectionArn) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing App Runner Connections: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping App Runner Connections for %s: %w", region, err)) + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping App Runner Connections sweep for %s: %s", region, err) + return nil // In case we have completed some pages, but had errors + } + + return errs.ErrorOrNil() +} + +func TestAccAwsAppRunnerConnection_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerConnectionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerConnection_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerConnectionExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`connection/%s/.+`, rName))), + resource.TestCheckResourceAttr(resourceName, "connection_name", rName), + resource.TestCheckResourceAttr(resourceName, "provider_type", apprunner.ProviderTypeGithub), + resource.TestCheckResourceAttr(resourceName, "status", apprunner.ConnectionStatusPendingHandshake), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerConnection_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerConnectionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerConnection_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerConnectionExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppRunnerConnection(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerConnection_tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_connection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerConnectionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerConnectionConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerConnectionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppRunnerConnectionConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerConnectionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), + ), + }, + { + Config: testAccAppRunnerConnectionConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerConnectionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppRunnerConnectionDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_apprunner_connection" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + connection, err := finder.ConnectionSummaryByName(context.Background(), conn, rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if connection != nil { + return fmt.Errorf("App Runner Connection (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAppRunnerConnectionExists(n string) 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 App Runner Connection ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + connection, err := finder.ConnectionSummaryByName(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + if connection == nil { + return fmt.Errorf("App Runner Connection (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAppRunnerConnection_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_connection" "test" { + connection_name = %q + provider_type = "GITHUB" +} +`, rName) +} + +func testAccAppRunnerConnectionConfigTags1(rName string, tagKey1 string, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_apprunner_connection" "test" { + connection_name = %[1]q + provider_type = "GITHUB" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAppRunnerConnectionConfigTags2(rName string, tagKey1 string, tagValue1 string, tagKey2 string, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_apprunner_connection" "test" { + connection_name = %[1]q + provider_type = "GITHUB" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/aws/resource_aws_apprunner_custom_domain_association.go b/aws/resource_aws_apprunner_custom_domain_association.go new file mode 100644 index 000000000000..a06c30825348 --- /dev/null +++ b/aws/resource_aws_apprunner_custom_domain_association.go @@ -0,0 +1,208 @@ +package aws + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/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" + tfapprunner "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" +) + +func resourceAwsAppRunnerCustomDomainAssociation() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppRunnerCustomDomainAssociationCreate, + ReadWithoutTimeout: resourceAwsAppRunnerCustomDomainAssociationRead, + DeleteWithoutTimeout: resourceAwsAppRunnerCustomDomainAssociationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "certificate_validation_records": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "dns_target": { + Type: schema.TypeString, + Computed: true, + }, + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "enable_www_subdomain": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + "service_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsAppRunnerCustomDomainAssociationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + domainName := d.Get("domain_name").(string) + serviceArn := d.Get("service_arn").(string) + + input := &apprunner.AssociateCustomDomainInput{ + DomainName: aws.String(domainName), + EnableWWWSubdomain: aws.Bool(d.Get("enable_www_subdomain").(bool)), + ServiceArn: aws.String(serviceArn), + } + + output, err := conn.AssociateCustomDomainWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error associating App Runner Custom Domain (%s) for Service (%s): %w", domainName, serviceArn, err)) + } + + if output == nil { + return diag.FromErr(fmt.Errorf("error associating App Runner Custom Domain (%s) for Service (%s): empty output", domainName, serviceArn)) + } + + d.SetId(fmt.Sprintf("%s,%s", aws.StringValue(output.CustomDomain.DomainName), aws.StringValue(output.ServiceArn))) + d.Set("dns_target", output.DNSTarget) + + if err := waiter.CustomDomainAssociationCreated(ctx, conn, domainName, serviceArn); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for App Runner Custom Domain Association (%s) creation: %w", d.Id(), err)) + } + + return resourceAwsAppRunnerCustomDomainAssociationRead(ctx, d, meta) +} + +func resourceAwsAppRunnerCustomDomainAssociationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + domainName, serviceArn, err := tfapprunner.CustomDomainAssociationParseID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + customDomain, err := finder.CustomDomain(ctx, conn, domainName, serviceArn) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] App Runner Custom Domain Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if customDomain == nil { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading App Runner Custom Domain Association (%s): empty output after creation", d.Id())) + } + log.Printf("[WARN] App Runner Custom Domain Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err := d.Set("certificate_validation_records", flattenAppRunnerCustomDomainCertificateValidationRecords(customDomain.CertificateValidationRecords)); err != nil { + return diag.FromErr(fmt.Errorf("error setting certificate_validation_records: %w", err)) + } + + d.Set("domain_name", customDomain.DomainName) + d.Set("enable_www_subdomain", customDomain.EnableWWWSubdomain) + d.Set("service_arn", serviceArn) + d.Set("status", customDomain.Status) + + return nil +} + +func resourceAwsAppRunnerCustomDomainAssociationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + domainName, serviceArn, err := tfapprunner.CustomDomainAssociationParseID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + input := &apprunner.DisassociateCustomDomainInput{ + DomainName: aws.String(domainName), + ServiceArn: aws.String(serviceArn), + } + + _, err = conn.DisassociateCustomDomainWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error disassociating App Runner Custom Domain (%s) for Service (%s): %w", domainName, serviceArn, err)) + } + + if err := waiter.CustomDomainAssociationDeleted(ctx, conn, domainName, serviceArn); err != nil { + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + + return diag.FromErr(fmt.Errorf("error waiting for App Runner Custom Domain Association (%s) deletion: %w", d.Id(), err)) + } + + return nil +} + +func flattenAppRunnerCustomDomainCertificateValidationRecords(records []*apprunner.CertificateValidationRecord) []interface{} { + var results []interface{} + + for _, record := range records { + if record == nil { + continue + } + + m := map[string]interface{}{ + "name": aws.StringValue(record.Name), + "status": aws.StringValue(record.Status), + "type": aws.StringValue(record.Type), + "value": aws.StringValue(record.Value), + } + + results = append(results, m) + } + + return results +} diff --git a/aws/resource_aws_apprunner_custom_domain_association_test.go b/aws/resource_aws_apprunner_custom_domain_association_test.go new file mode 100644 index 000000000000..e3f2cc814b97 --- /dev/null +++ b/aws/resource_aws_apprunner_custom_domain_association_test.go @@ -0,0 +1,171 @@ +package aws + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" + tfapprunner "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" +) + +func TestAccAwsAppRunnerCustomDomainAssociation_basic(t *testing.T) { + domain := os.Getenv("APPRUNNER_CUSTOM_DOMAIN") + if domain == "" { + t.Skip("Environment variable APPRUNNER_CUSTOM_DOMAIN is not set") + } + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_custom_domain_association.test" + serviceResourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerCustomDomainAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerCustomDomainAssociation_basic(rName, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerCustomDomainAssociationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "certificate_validation_records.#", "3"), + resource.TestCheckResourceAttrSet(resourceName, "dns_target"), + resource.TestCheckResourceAttr(resourceName, "domain_name", domain), + resource.TestCheckResourceAttr(resourceName, "enable_www_subdomain", "true"), + resource.TestCheckResourceAttr(resourceName, "status", waiter.CustomDomainAssociationStatusPendingCertificateDnsValidation), + resource.TestCheckResourceAttrPair(resourceName, "service_arn", serviceResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"dns_target"}, + }, + }, + }) +} + +func TestAccAwsAppRunnerCustomDomainAssociation_disappears(t *testing.T) { + domain := os.Getenv("APPRUNNER_CUSTOM_DOMAIN") + if domain == "" { + t.Skip("Environment variable APPRUNNER_CUSTOM_DOMAIN is not set") + } + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_custom_domain_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerCustomDomainAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerCustomDomainAssociation_basic(rName, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerCustomDomainAssociationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppRunnerCustomDomainAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsAppRunnerCustomDomainAssociationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_apprunner_connection" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + domainName, serviceArn, err := tfapprunner.CustomDomainAssociationParseID(rs.Primary.ID) + + if err != nil { + return err + } + + customDomain, err := finder.CustomDomain(context.Background(), conn, domainName, serviceArn) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if customDomain != nil { + return fmt.Errorf("App Runner Custom Domain Association (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAppRunnerCustomDomainAssociationExists(n string) 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 App Runner Custom Domain Association ID is set") + } + + domainName, serviceArn, err := tfapprunner.CustomDomainAssociationParseID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + customDomain, err := finder.CustomDomain(context.Background(), conn, domainName, serviceArn) + + if err != nil { + return err + } + + if customDomain == nil { + return fmt.Errorf("App Runner Custom Domain Association (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAppRunnerCustomDomainAssociation_basic(rName, domain string) string { + return fmt.Sprintf(` +resource "aws_apprunner_service" "test" { + service_name = %[1]q + + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} + +resource "aws_apprunner_custom_domain_association" "test" { + domain_name = %[2]q + service_arn = aws_apprunner_service.test.arn +} +`, rName, domain) +} diff --git a/aws/resource_aws_apprunner_service.go b/aws/resource_aws_apprunner_service.go new file mode 100644 index 000000000000..97ba80a7ea65 --- /dev/null +++ b/aws/resource_aws_apprunner_service.go @@ -0,0 +1,1041 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/apprunner/waiter" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAppRunnerService() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppRunnerServiceCreate, + ReadWithoutTimeout: resourceAwsAppRunnerServiceRead, + UpdateWithoutTimeout: resourceAwsAppRunnerServiceUpdate, + DeleteWithoutTimeout: resourceAwsAppRunnerServiceDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "auto_scaling_configuration_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, + }, + + "encryption_configuration": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kms_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + }, + }, + }, + + "health_check_configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "healthy_threshold": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 20), + }, + "interval": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 20), + }, + "path": { + Type: schema.TypeString, + Optional: true, + Default: "/", + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + Default: apprunner.HealthCheckProtocolTcp, + ForceNew: true, + ValidateFunc: validation.StringInSlice(apprunner.HealthCheckProtocol_Values(), false), + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 2, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 20), + }, + "unhealthy_threshold": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 20), + }, + }, + }, + }, + + "instance_configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cpu": { + Type: schema.TypeString, + Optional: true, + Default: "1024", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`1024|2048|(1|2) vCPU`), ""), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // App Runner API always returns the amount in multiples of 1024 units + return (old == "1024" && new == "1 vCPU") || (old == "2048" && new == "2 vCPU") + }, + }, + "instance_role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "memory": { + Type: schema.TypeString, + Optional: true, + Default: "2048", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`2048|3072|4096|(2|3|4) GB`), ""), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // App Runner API always returns the amount in MB + return (old == "2048" && new == "2 GB") || (old == "3072" && new == "3 GB") || (old == "4096" && new == "4 GB") + }, + }, + }, + }, + }, + + "service_id": { + Type: schema.TypeString, + Computed: true, + }, + + "service_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "service_url": { + Type: schema.TypeString, + Computed: true, + }, + + "source_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "authentication_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access_role_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + "connection_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + }, + }, + }, + "auto_deployments_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "code_repository": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "code_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "code_configuration_values": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "build_command": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + "port": { + Type: schema.TypeString, + Optional: true, + Default: "8080", + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + "runtime": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(apprunner.Runtime_Values(), false), + }, + "runtime_environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + }, + "start_command": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + }, + }, + }, + "configuration_source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(apprunner.ConfigurationSource_Values(), false), + }, + }, + }, + }, + "repository_url": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + "source_code_version": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(apprunner.SourceCodeVersionType_Values(), false), + }, + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + }, + }, + }, + }, + }, + ExactlyOneOf: []string{"source_configuration.0.code_repository", "source_configuration.0.image_repository"}, + }, + "image_repository": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "image_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": { + Type: schema.TypeString, + Optional: true, + Default: "8080", + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + "runtime_environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + }, + "start_command": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 51200), + }, + }, + }, + }, + "image_identifier": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`([0-9]{12}.dkr.ecr.[a-z\-]+-[0-9]{1}.amazonaws.com\/.*)|(^public\.ecr\.aws\/.+\/.+)`), ""), + }, + "image_repository_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(apprunner.ImageRepositoryType_Values(), false), + }, + }, + }, + ExactlyOneOf: []string{"source_configuration.0.image_repository", "source_configuration.0.code_repository"}, + }, + }, + }, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + + "tags_all": tagsSchemaComputed(), + }, + + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsAppRunnerServiceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + serviceName := d.Get("service_name").(string) + + input := &apprunner.CreateServiceInput{ + ServiceName: aws.String(serviceName), + SourceConfiguration: expandAppRunnerServiceSourceConfiguration(d.Get("source_configuration").([]interface{})), + Tags: tags.IgnoreAws().ApprunnerTags(), + } + + if v, ok := d.GetOk("auto_scaling_configuration_arn"); ok { + input.AutoScalingConfigurationArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("encryption_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.EncryptionConfiguration = expandAppRunnerServiceEncryptionConfiguration(v.([]interface{})) + } + + if v, ok := d.GetOk("health_check_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.HealthCheckConfiguration = expandAppRunnerServiceHealthCheckConfiguration(v.([]interface{})) + } + + if v, ok := d.GetOk("instance_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.InstanceConfiguration = expandAppRunnerServiceInstanceConfiguration(v.([]interface{})) + } + + var output *apprunner.CreateServiceOutput + + err := resource.RetryContext(ctx, iamwaiter.PropagationTimeout, func() *resource.RetryError { + var err error + output, err = conn.CreateServiceWithContext(ctx, input) + + if tfawserr.ErrMessageContains(err, apprunner.ErrCodeInvalidRequestException, "Error in assuming instance role") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + output, err = conn.CreateServiceWithContext(ctx, input) + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating App Runner Service (%s): %w", serviceName, err)) + } + + if output == nil || output.Service == nil { + return diag.FromErr(fmt.Errorf("error creating App Runner Service (%s): empty output", serviceName)) + } + + d.SetId(aws.StringValue(output.Service.ServiceArn)) + + if err := waiter.ServiceCreated(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for App Runner Service (%s) creation: %w", d.Id(), err)) + } + + return resourceAwsAppRunnerServiceRead(ctx, d, meta) +} + +func resourceAwsAppRunnerServiceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &apprunner.DescribeServiceInput{ + ServiceArn: aws.String(d.Id()), + } + + output, err := conn.DescribeServiceWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] App Runner Service (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading App Runner Service (%s): %w", d.Id(), err)) + } + + if output == nil || output.Service == nil { + return diag.FromErr(fmt.Errorf("error reading App Runner Service (%s): empty output", d.Id())) + } + + if aws.StringValue(output.Service.Status) == apprunner.ServiceStatusDeleted { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading App Runner Service (%s): %s after creation", d.Id(), aws.StringValue(output.Service.Status))) + } + log.Printf("[WARN] App Runner Service (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + service := output.Service + arn := aws.StringValue(service.ServiceArn) + + var autoScalingConfigArn string + if service.AutoScalingConfigurationSummary != nil { + autoScalingConfigArn = aws.StringValue(service.AutoScalingConfigurationSummary.AutoScalingConfigurationArn) + } + + d.Set("arn", arn) + d.Set("auto_scaling_configuration_arn", autoScalingConfigArn) + d.Set("service_id", service.ServiceId) + d.Set("service_name", service.ServiceName) + d.Set("service_url", service.ServiceUrl) + d.Set("status", service.Status) + if err := d.Set("encryption_configuration", flattenAppRunnerServiceEncryptionConfiguration(service.EncryptionConfiguration)); err != nil { + return diag.FromErr(fmt.Errorf("error setting encryption_configuration: %w", err)) + } + + if err := d.Set("health_check_configuration", flattenAppRunnerServiceHealthCheckConfiguration(service.HealthCheckConfiguration)); err != nil { + return diag.FromErr(fmt.Errorf("error setting health_check_configuration: %w", err)) + } + + if err := d.Set("instance_configuration", flattenAppRunnerServiceInstanceConfiguration(service.InstanceConfiguration)); err != nil { + return diag.FromErr(fmt.Errorf("error setting instance_configuration: %w", err)) + } + + if err := d.Set("source_configuration", flattenAppRunnerServiceSourceConfiguration(service.SourceConfiguration)); err != nil { + return diag.FromErr(fmt.Errorf("error setting source_configuration: %w", err)) + } + + tags, err := keyvaluetags.ApprunnerListTags(conn, arn) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing tags for App Runner Service (%s): %s", arn, err)) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) + } + + return nil +} + +func resourceAwsAppRunnerServiceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + if d.HasChanges( + "auto_scaling_configuration_arn", + "instance_configuration", + "source_configuration", + ) { + input := &apprunner.UpdateServiceInput{ + ServiceArn: aws.String(d.Id()), + } + + if d.HasChange("auto_scaling_configuration_arn") { + input.AutoScalingConfigurationArn = aws.String(d.Get("auto_scaling_configuration_arn").(string)) + } + + if d.HasChange("instance_configuration") { + input.InstanceConfiguration = expandAppRunnerServiceInstanceConfiguration(d.Get("instance_configuration").([]interface{})) + } + + if d.HasChange("source_configuration") { + input.SourceConfiguration = expandAppRunnerServiceSourceConfiguration(d.Get("source_configuration").([]interface{})) + } + + _, err := conn.UpdateServiceWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating App Runner Service (%s): %w", d.Id(), err)) + } + + if err := waiter.ServiceUpdated(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for App Runner Service (%s) to update: %w", d.Id(), err)) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := keyvaluetags.ApprunnerUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating App Runner Service (%s) tags: %s", d.Get("arn").(string), err)) + } + } + + return resourceAwsAppRunnerServiceRead(ctx, d, meta) +} + +func resourceAwsAppRunnerServiceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).apprunnerconn + + input := &apprunner.DeleteServiceInput{ + ServiceArn: aws.String(d.Id()), + } + + _, err := conn.DeleteServiceWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting App Runner Service (%s): %w", d.Id(), err)) + } + + if err := waiter.ServiceDeleted(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + return nil + } + + return diag.FromErr(fmt.Errorf("error waiting for App Runner Service (%s) deletion: %w", d.Id(), err)) + } + + return nil +} + +func expandAppRunnerServiceEncryptionConfiguration(l []interface{}) *apprunner.EncryptionConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.EncryptionConfiguration{} + + if v, ok := tfMap["kms_key"].(string); ok && v != "" { + result.KmsKey = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceHealthCheckConfiguration(l []interface{}) *apprunner.HealthCheckConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.HealthCheckConfiguration{} + + if v, ok := tfMap["healthy_threshold"].(int); ok { + result.HealthyThreshold = aws.Int64(int64(v)) + } + + if v, ok := tfMap["interval"].(int); ok { + result.Interval = aws.Int64(int64(v)) + } + + if v, ok := tfMap["path"].(string); ok { + result.Path = aws.String(v) + } + + if v, ok := tfMap["protocol"].(string); ok { + result.Protocol = aws.String(v) + } + + if v, ok := tfMap["timeout"].(int); ok { + result.Timeout = aws.Int64(int64(v)) + } + + if v, ok := tfMap["unhealthy_threshold"].(int); ok { + result.UnhealthyThreshold = aws.Int64(int64(v)) + } + + return result +} + +func expandAppRunnerServiceInstanceConfiguration(l []interface{}) *apprunner.InstanceConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.InstanceConfiguration{} + + if v, ok := tfMap["cpu"].(string); ok { + result.Cpu = aws.String(v) + } + + if v, ok := tfMap["instance_role_arn"].(string); ok { + result.InstanceRoleArn = aws.String(v) + } + + if v, ok := tfMap["memory"].(string); ok { + result.Memory = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceSourceConfiguration(l []interface{}) *apprunner.SourceConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.SourceConfiguration{} + + if v, ok := tfMap["authentication_configuration"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.AuthenticationConfiguration = expandAppRunnerServiceAuthenticationConfiguration(v) + } + + if v, ok := tfMap["auto_deployments_enabled"].(bool); ok { + result.AutoDeploymentsEnabled = aws.Bool(v) + } + + if v, ok := tfMap["code_repository"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.CodeRepository = expandAppRunnerServiceCodeRepository(v) + } + + if v, ok := tfMap["image_repository"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.ImageRepository = expandAppRunnerServiceImageRepository(v) + } + + return result +} + +func expandAppRunnerServiceAuthenticationConfiguration(l []interface{}) *apprunner.AuthenticationConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.AuthenticationConfiguration{} + + if v, ok := tfMap["access_role_arn"].(string); ok && v != "" { + result.AccessRoleArn = aws.String(v) + } + + if v, ok := tfMap["connection_arn"].(string); ok && v != "" { + result.ConnectionArn = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceImageConfiguration(l []interface{}) *apprunner.ImageConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.ImageConfiguration{} + + if v, ok := tfMap["port"].(string); ok && v != "" { + result.Port = aws.String(v) + } + + if v, ok := tfMap["runtime_environment_variables"].(map[string]interface{}); ok && len(v) > 0 { + result.RuntimeEnvironmentVariables = expandStringMap(v) + } + + if v, ok := tfMap["start_command"].(string); ok && v != "" { + result.StartCommand = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceCodeRepository(l []interface{}) *apprunner.CodeRepository { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.CodeRepository{} + + if v, ok := tfMap["source_code_version"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.SourceCodeVersion = expandAppRunnerServiceSourceCodeVersion(v) + } + + if v, ok := tfMap["code_configuration"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.CodeConfiguration = expandAppRunnerServiceCodeConfiguration(v) + } + + if v, ok := tfMap["repository_url"].(string); ok && v != "" { + result.RepositoryUrl = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceImageRepository(l []interface{}) *apprunner.ImageRepository { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.ImageRepository{} + + if v, ok := tfMap["image_configuration"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.ImageConfiguration = expandAppRunnerServiceImageConfiguration(v) + } + + if v, ok := tfMap["image_identifier"].(string); ok && v != "" { + result.ImageIdentifier = aws.String(v) + } + + if v, ok := tfMap["image_repository_type"].(string); ok && v != "" { + result.ImageRepositoryType = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceCodeConfiguration(l []interface{}) *apprunner.CodeConfiguration { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.CodeConfiguration{} + + if v, ok := tfMap["configuration_source"].(string); ok && v != "" { + result.ConfigurationSource = aws.String(v) + } + + if v, ok := tfMap["code_configuration_values"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.CodeConfigurationValues = expandAppRunnerServiceCodeConfigurationValues(v) + } + + return result +} + +func expandAppRunnerServiceCodeConfigurationValues(l []interface{}) *apprunner.CodeConfigurationValues { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.CodeConfigurationValues{} + + if v, ok := tfMap["build_command"].(string); ok && v != "" { + result.BuildCommand = aws.String(v) + } + + if v, ok := tfMap["port"].(string); ok && v != "" { + result.Port = aws.String(v) + } + + if v, ok := tfMap["runtime"].(string); ok && v != "" { + result.Runtime = aws.String(v) + } + + if v, ok := tfMap["runtime_environment_variables"].(map[string]interface{}); ok && len(v) > 0 { + result.RuntimeEnvironmentVariables = expandStringMap(v) + } + + if v, ok := tfMap["start_command"].(string); ok && v != "" { + result.StartCommand = aws.String(v) + } + + return result +} + +func expandAppRunnerServiceSourceCodeVersion(l []interface{}) *apprunner.SourceCodeVersion { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + + if !ok { + return nil + } + + result := &apprunner.SourceCodeVersion{} + + if v, ok := tfMap["type"].(string); ok && v != "" { + result.Type = aws.String(v) + } + + if v, ok := tfMap["value"].(string); ok && v != "" { + result.Value = aws.String(v) + } + + return result +} + +func flattenAppRunnerServiceEncryptionConfiguration(config *apprunner.EncryptionConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "kms_key": aws.StringValue(config.KmsKey), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceHealthCheckConfiguration(config *apprunner.HealthCheckConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "healthy_threshold": aws.Int64Value(config.HealthyThreshold), + "interval": aws.Int64Value(config.Interval), + "path": aws.StringValue(config.Path), + "protocol": aws.StringValue(config.Protocol), + "timeout": aws.Int64Value(config.Timeout), + "unhealthy_threshold": aws.Int64Value(config.UnhealthyThreshold), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceInstanceConfiguration(config *apprunner.InstanceConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "cpu": aws.StringValue(config.Cpu), + "instance_role_arn": aws.StringValue(config.InstanceRoleArn), + "memory": aws.StringValue(config.Memory), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceCodeRepository(r *apprunner.CodeRepository) []interface{} { + if r == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "code_configuration": flattenAppRunnerServiceCodeConfiguration(r.CodeConfiguration), + "repository_url": aws.StringValue(r.RepositoryUrl), + "source_code_version": flattenAppRunnerServiceSourceCodeVersion(r.SourceCodeVersion), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceCodeConfiguration(config *apprunner.CodeConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "code_configuration_values": flattenAppRunnerServiceCodeConfigurationValues(config.CodeConfigurationValues), + "configuration_source": aws.StringValue(config.ConfigurationSource), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceCodeConfigurationValues(values *apprunner.CodeConfigurationValues) []interface{} { + if values == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "build_command": aws.StringValue(values.BuildCommand), + "port": aws.StringValue(values.Port), + "runtime": aws.StringValue(values.Runtime), + "runtime_environment_variables": aws.StringValueMap(values.RuntimeEnvironmentVariables), + "start_command": aws.StringValue(values.StartCommand), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceSourceCodeVersion(v *apprunner.SourceCodeVersion) []interface{} { + if v == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "type": aws.StringValue(v.Type), + "value": aws.StringValue(v.Value), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceSourceConfiguration(config *apprunner.SourceConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "authentication_configuration": flattenAppRunnerServiceAuthenticationConfiguration(config.AuthenticationConfiguration), + "auto_deployments_enabled": aws.BoolValue(config.AutoDeploymentsEnabled), + "code_repository": flattenAppRunnerServiceCodeRepository(config.CodeRepository), + "image_repository": flattenAppRunnerServiceImageRepository(config.ImageRepository), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceAuthenticationConfiguration(config *apprunner.AuthenticationConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "access_role_arn": aws.StringValue(config.AccessRoleArn), + "connection_arn": aws.StringValue(config.ConnectionArn), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceImageConfiguration(config *apprunner.ImageConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "port": aws.StringValue(config.Port), + "runtime_environment_variables": aws.StringValueMap(config.RuntimeEnvironmentVariables), + "start_command": aws.StringValue(config.StartCommand), + } + + return []interface{}{m} +} + +func flattenAppRunnerServiceImageRepository(r *apprunner.ImageRepository) []interface{} { + if r == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "image_configuration": flattenAppRunnerServiceImageConfiguration(r.ImageConfiguration), + "image_identifier": aws.StringValue(r.ImageIdentifier), + "image_repository_type": aws.StringValue(r.ImageRepositoryType), + } + + return []interface{}{m} +} diff --git a/aws/resource_aws_apprunner_service_test.go b/aws/resource_aws_apprunner_service_test.go new file mode 100644 index 000000000000..f774c7c8254e --- /dev/null +++ b/aws/resource_aws_apprunner_service_test.go @@ -0,0 +1,742 @@ +package aws + +import ( + "context" + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/apprunner" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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" +) + +func init() { + resource.AddTestSweepers("aws_apprunner_service", &resource.Sweeper{ + Name: "aws_apprunner_service", + F: testSweepAppRunnerServices, + }) +} + +func testSweepAppRunnerServices(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).apprunnerconn + sweepResources := make([]*testSweepResource, 0) + ctx := context.Background() + var errs *multierror.Error + + input := &apprunner.ListServicesInput{} + + err = conn.ListServicesPagesWithContext(ctx, input, func(page *apprunner.ListServicesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, service := range page.ServiceSummaryList { + if service == nil { + continue + } + + arn := aws.StringValue(service.ServiceArn) + + log.Printf("[INFO] Deleting App Runner Service: %s", arn) + + r := resourceAwsAppRunnerService() + d := r.Data(nil) + d.SetId(arn) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing App Runner Services: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping App Runner Services for %s: %w", region, err)) + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping App Runner Services sweep for %s: %s", region, err) + return nil // In case we have completed some pages, but had errors + } + + return errs.ErrorOrNil() +} + +func TestAccAwsAppRunnerService_ImageRepository_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "service_name", rName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "apprunner", regexp.MustCompile(fmt.Sprintf(`service/%s/.+`, rName))), + testAccMatchResourceAttrRegionalARN(resourceName, "auto_scaling_configuration_arn", "apprunner", regexp.MustCompile(`autoscalingconfiguration/DefaultConfiguration/1/.+`)), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.protocol", apprunner.HealthCheckProtocolTcp), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.path", "/"), + // Only check the following attribute values for health_check and instance configurations + // are set as their defaults differ in the API documentation and API itself + resource.TestCheckResourceAttrSet(resourceName, "health_check_configuration.0.interval"), + resource.TestCheckResourceAttrSet(resourceName, "health_check_configuration.0.timeout"), + resource.TestCheckResourceAttrSet(resourceName, "health_check_configuration.0.healthy_threshold"), + resource.TestCheckResourceAttrSet(resourceName, "health_check_configuration.0.unhealthy_threshold"), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "instance_configuration.0.cpu"), + resource.TestCheckResourceAttrSet(resourceName, "instance_configuration.0.memory"), + resource.TestCheckResourceAttrSet(resourceName, "service_id"), + resource.TestCheckResourceAttrSet(resourceName, "service_url"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.auto_deployments_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_configuration.0.port", "80"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_identifier", "public.ecr.aws/nginx/nginx:latest"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_repository_type", apprunner.ImageRepositoryTypeEcrPublic), + resource.TestCheckResourceAttr(resourceName, "status", apprunner.ServiceStatusRunning), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerService_ImageRepository_AutoScalingConfiguration(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + autoScalingResourceName := "aws_apprunner_auto_scaling_configuration_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + ), + }, + { + Config: testAccAppRunnerService_imageRepository_autoScalingConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "auto_scaling_configuration_arn", autoScalingResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerService_ImageRepository_EncryptionConfiguration(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + kmsResourceName := "aws_kms_key.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository_encryptionConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "encryption_configuration.0.kms_key", kmsResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test resource recreation; EncryptionConfiguration (or lack thereof) Forces New resource + Config: testAccAppRunnerService_imageRepository(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.#", "0"), + ), + }, + }, + }) +} + +func TestAccAwsAppRunnerService_ImageRepository_HealthCheckConfiguration(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository_healthCheckConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.healthy_threshold", "2"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.interval", "5"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.protocol", apprunner.HealthCheckProtocolTcp), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.timeout", "5"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.unhealthy_threshold", "5"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test resource recreation; HealthConfiguration Forces New resource + Config: testAccAppRunnerService_imageRepository_updateHealthCheckConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.healthy_threshold", "2"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.interval", "5"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.protocol", apprunner.HealthCheckProtocolTcp), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.timeout", "10"), + resource.TestCheckResourceAttr(resourceName, "health_check_configuration.0.unhealthy_threshold", "4"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerService_ImageRepository_InstanceConfiguration(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + roleResourceName := "aws_iam_role.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository_instanceConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.0.cpu", "1024"), + resource.TestCheckResourceAttrPair(resourceName, "instance_configuration.0.instance_role_arn", roleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.0.memory", "3072"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppRunnerService_imageRepository_updateInstanceConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.0.cpu", "2048"), + resource.TestCheckResourceAttrPair(resourceName, "instance_configuration.0.instance_role_arn", roleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.0.memory", "4096"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppRunnerService_imageRepository(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "instance_configuration.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "instance_configuration.0.cpu"), + resource.TestCheckResourceAttrSet(resourceName, "instance_configuration.0.memory"), + ), + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/19469 +func TestAccAwsAppRunnerService_ImageRepository_RuntimeEnvironmentVars(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository_runtimeEnvVars(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_configuration.0.runtime_environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "source_configuration.0.image_repository.0.image_configuration.0.runtime_environment_variables.APP_NAME", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerService_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerService_imageRepository(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppRunnerService(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppRunnerService_tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_apprunner_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAppRunner(t) }, + ErrorCheck: testAccErrorCheck(t, apprunner.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppRunnerServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppRunnerServiceConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppRunnerServiceConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAppRunnerServiceConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppRunnerServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppRunnerServiceDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_apprunner_service" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + input := &apprunner.DescribeServiceInput{ + ServiceArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeServiceWithContext(context.Background(), input) + + if tfawserr.ErrCodeEquals(err, apprunner.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if output != nil && output.Service != nil && aws.StringValue(output.Service.Status) != apprunner.ServiceStatusDeleted { + return fmt.Errorf("App Runner Service (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAppRunnerServiceExists(n string) 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 App Runner Service ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + + input := &apprunner.DescribeServiceInput{ + ServiceArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeServiceWithContext(context.Background(), input) + + if err != nil { + return err + } + + if output == nil || output.Service == nil { + return fmt.Errorf("App Runner Service (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccPreCheckAppRunner(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).apprunnerconn + ctx := context.Background() + + input := &apprunner.ListServicesInput{} + + _, err := conn.ListServicesWithContext(ctx, input) + + if testAccPreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAppRunnerService_imageRepository(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_service" "test" { + service_name = %[1]q + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerService_imageRepository_runtimeEnvVars(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_service" "test" { + service_name = %[1]q + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + runtime_environment_variables = { + APP_NAME = %[1]q + } + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerService_imageRepository_autoScalingConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_auto_scaling_configuration_version" "test" { + auto_scaling_configuration_name = %[1]q +} + +resource "aws_apprunner_service" "test" { + auto_scaling_configuration_arn = aws_apprunner_auto_scaling_configuration_version.test.arn + + service_name = %[1]q + + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerService_imageRepository_encryptionConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + deletion_window_in_days = 7 + description = %[1]q +} + +resource "aws_apprunner_service" "test" { + service_name = %[1]q + + encryption_configuration { + kms_key = aws_kms_key.test.arn + } + + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerService_imageRepository_healthCheckConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_service" "test" { + service_name = %[1]q + + health_check_configuration { + healthy_threshold = 2 + timeout = 5 + } + + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerService_imageRepository_updateHealthCheckConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_apprunner_service" "test" { + service_name = %[1]q + + health_check_configuration { + healthy_threshold = 2 + timeout = 10 + unhealthy_threshold = 4 + } + + source_configuration { + auto_deployments_enabled = false + image_repository { + image_configuration { + port = "80" + } + image_identifier = "public.ecr.aws/nginx/nginx:latest" + image_repository_type = "ECR_PUBLIC" + } + } +} +`, rName) +} + +func testAccAppRunnerIAMRole(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = < 0 { + input.Tags = tags.IgnoreAws().AppstreamTags() + } + + var err error + var output *appstream.CreateFleetOutput + err = resource.RetryContext(ctx, waiter.FleetOperationTimeout, func() *resource.RetryError { + output, err = conn.CreateFleetWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + output, err = conn.CreateFleetWithContext(ctx, input) + } + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Appstream Fleet (%s): %w", d.Id(), err)) + } + + // Start fleet workflow + _, err = conn.StartFleetWithContext(ctx, &appstream.StartFleetInput{ + Name: output.Fleet.Name, + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error starting Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateRunning(ctx, conn, aws.StringValue(output.Fleet.Name)); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be running: %w", d.Id(), err)) + } + + d.SetId(aws.StringValue(output.Fleet.Name)) + + return resourceAwsAppStreamFleetRead(ctx, d, meta) +} + +func resourceAwsAppStreamFleetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + resp, err := conn.DescribeFleetsWithContext(ctx, &appstream.DescribeFleetsInput{Names: []*string{aws.String(d.Id())}}) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appstream Fleet (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %w", d.Id(), err)) + } + + if len(resp.Fleets) == 0 { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %s", d.Id(), "empty response")) + } + + if len(resp.Fleets) > 1 { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %s", d.Id(), "multiple fleets found")) + } + + fleet := resp.Fleets[0] + + d.Set("arn", fleet.Arn) + + if err = d.Set("compute_capacity", flattenComputeCapacity(fleet.ComputeCapacityStatus)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "compute_capacity", d.Id(), err)) + } + + d.Set("created_time", aws.TimeValue(fleet.CreatedTime).Format(time.RFC3339)) + d.Set("description", fleet.Description) + d.Set("display_name", fleet.DisplayName) + d.Set("disconnect_timeout_in_seconds", fleet.DisconnectTimeoutInSeconds) + + if err = d.Set("domain_join_info", flattenDomainInfo(fleet.DomainJoinInfo)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "domain_join_info", d.Id(), err)) + } + + d.Set("idle_disconnect_timeout_in_seconds", fleet.IdleDisconnectTimeoutInSeconds) + d.Set("enable_default_internet_access", fleet.EnableDefaultInternetAccess) + d.Set("fleet_type", fleet.FleetType) + d.Set("iam_role_arn", fleet.IamRoleArn) + d.Set("image_name", fleet.ImageName) + d.Set("image_arn", fleet.ImageArn) + d.Set("instance_type", fleet.InstanceType) + d.Set("max_user_duration_in_seconds", fleet.MaxUserDurationInSeconds) + d.Set("name", fleet.Name) + d.Set("state", fleet.State) + d.Set("stream_view", fleet.StreamView) + + if err = d.Set("vpc_config", flattenVpcConfig(fleet.VpcConfig)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "vpc_config", d.Id(), err)) + } + + tg, err := conn.ListTagsForResource(&appstream.ListTagsForResourceInput{ + ResourceArn: fleet.Arn, + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing stack tags for AppStream Stack (%s): %w", d.Id(), err)) + } + + if tg.Tags == nil { + log.Printf("[DEBUG] AppStream Stack tags (%s) not found", d.Id()) + return nil + } + + tags := keyvaluetags.AppstreamKeyValueTags(tg.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err = d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags", d.Id(), err)) + } + + if err = d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags_all", d.Id(), err)) + } + + return nil +} + +func resourceAwsAppStreamFleetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + input := &appstream.UpdateFleetInput{ + Name: aws.String(d.Id()), + } + shouldStop := false + + if d.HasChanges("description", "domain_join_info", "enable_default_internet_access", "iam_role_arn", "instance_type", "max_user_duration_in_seconds", "stream_view", "vpc_config") { + shouldStop = true + } + + // Stop fleet workflow if needed + if shouldStop { + _, err := conn.StopFleetWithContext(ctx, &appstream.StopFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error stopping Appstream Fleet (%s): %w", d.Id(), err)) + } + if _, err = waiter.FleetStateStopped(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be stopped: %w", d.Id(), err)) + } + } + + if d.HasChange("compute_capacity") { + input.ComputeCapacity = expandComputeCapacity(d.Get("compute_capacity").([]interface{})) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("domain_join_info") { + input.DomainJoinInfo = expandDomainJoinInfo(d.Get("domain_join_info").([]interface{})) + } + + if d.HasChange("disconnect_timeout_in_seconds") { + input.DisconnectTimeoutInSeconds = aws.Int64(int64(d.Get("disconnect_timeout_in_seconds").(int))) + } + + if d.HasChange("enable_default_internet_access") { + input.EnableDefaultInternetAccess = aws.Bool(d.Get("enable_default_internet_access").(bool)) + } + + if d.HasChange("idle_disconnect_timeout_in_seconds") { + input.IdleDisconnectTimeoutInSeconds = aws.Int64(int64(d.Get("idle_disconnect_timeout_in_seconds").(int))) + } + + if d.HasChange("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChange("image_name") { + input.ImageName = aws.String(d.Get("image_name").(string)) + } + + if d.HasChange("image_arn") { + input.ImageArn = aws.String(d.Get("image_arn").(string)) + } + + if d.HasChange("iam_role_arn") { + input.IamRoleArn = aws.String(d.Get("iam_role_arn").(string)) + } + + if d.HasChange("stream_view") { + input.StreamView = aws.String(d.Get("stream_view").(string)) + } + + if d.HasChange("instance_type") { + input.InstanceType = aws.String(d.Get("instance_type").(string)) + } + + if d.HasChange("max_user_duration_in_seconds") { + input.MaxUserDurationInSeconds = aws.Int64(int64(d.Get("max_user_duration_in_seconds").(int))) + } + + if d.HasChange("vpc_config") { + input.VpcConfig = expandVpcConfig(d.Get("vpc_config").([]interface{})) + } + + resp, err := conn.UpdateFleetWithContext(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Appstream Fleet (%s): %w", d.Id(), err)) + } + + if d.HasChange("tags") { + arn := aws.StringValue(resp.Fleet.Arn) + + o, n := d.GetChange("tags") + if err := keyvaluetags.AppstreamUpdateTags(conn, arn, o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating Appstream Fleet tags (%s): %w", d.Id(), err)) + } + } + + // Start fleet workflow if stopped + if shouldStop { + _, err = conn.StartFleetWithContext(ctx, &appstream.StartFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error starting Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateRunning(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be running: %w", d.Id(), err)) + } + } + + return resourceAwsAppStreamFleetRead(ctx, d, meta) +} + +func resourceAwsAppStreamFleetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + // Stop fleet workflow + _, err := conn.StopFleetWithContext(ctx, &appstream.StopFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error stopping Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateStopped(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be stopped: %w", d.Id(), err)) + } + + _, err = conn.DeleteFleetWithContext(ctx, &appstream.DeleteFleetInput{ + Name: aws.String(d.Id()), + }) + + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error deleting Appstream Fleet (%s): %w", d.Id(), err)) + } + return nil +} + +func expandComputeCapacity(tfList []interface{}) *appstream.ComputeCapacity { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.ComputeCapacity{} + + attr := tfList[0].(map[string]interface{}) + if v, ok := attr["desired_instances"]; ok { + apiObject.DesiredInstances = aws.Int64(int64(v.(int))) + } + + return apiObject +} + +func flattenComputeCapacity(apiObject *appstream.ComputeCapacityStatus) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["desired_instances"] = aws.Int64Value(apiObject.Desired) + tfList["available"] = aws.Int64Value(apiObject.Available) + tfList["in_use"] = aws.Int64Value(apiObject.InUse) + tfList["running"] = aws.Int64Value(apiObject.Running) + + return []interface{}{tfList} +} + +func expandDomainJoinInfo(tfList []interface{}) *appstream.DomainJoinInfo { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.DomainJoinInfo{} + + tfMap := tfList[0].(map[string]interface{}) + if v, ok := tfMap["directory_name"]; ok { + apiObject.DirectoryName = aws.String(v.(string)) + } + if v, ok := tfMap["organizational_unit_distinguished_name"]; ok { + apiObject.OrganizationalUnitDistinguishedName = aws.String(v.(string)) + } + + return apiObject +} + +func flattenDomainInfo(apiObject *appstream.DomainJoinInfo) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["directory_name"] = aws.StringValue(apiObject.DirectoryName) + tfList["organizational_unit_distinguished_name"] = aws.StringValue(apiObject.OrganizationalUnitDistinguishedName) + + return []interface{}{tfList} +} + +func expandVpcConfig(tfList []interface{}) *appstream.VpcConfig { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.VpcConfig{} + + tfMap := tfList[0].(map[string]interface{}) + if v, ok := tfMap["security_group_ids"]; ok { + apiObject.SecurityGroupIds = expandStringList(v.([]interface{})) + } + if v, ok := tfMap["subnet_ids"]; ok { + apiObject.SubnetIds = expandStringList(v.([]interface{})) + } + + return apiObject +} + +func flattenVpcConfig(apiObject *appstream.VpcConfig) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["security_group_ids"] = aws.StringValueSlice(apiObject.SecurityGroupIds) + tfList["subnet_ids"] = aws.StringValueSlice(apiObject.SubnetIds) + + return []interface{}{tfList} +} diff --git a/aws/resource_aws_appstream_fleet_test.go b/aws/resource_aws_appstream_fleet_test.go new file mode 100644 index 000000000000..08b4ad32d62c --- /dev/null +++ b/aws/resource_aws_appstream_fleet_test.go @@ -0,0 +1,428 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" +) + +func init() { + RegisterServiceErrorCheckFunc(appstream.EndpointsID, testAccErrorCheckSkipAppStream) +} + +// testAccErrorCheckSkipAppStream skips AppStream tests that have error messages indicating unsupported features +func testAccErrorCheckSkipAppStream(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "ResourceNotFoundException: The image", + ) +} + +func TestAccAwsAppStreamFleet_basic(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfig(rName, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_disappears(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfig(rName, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppStreamFleet(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_completeWithStop(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + instanceTypeUpdate := "stream.standard.medium" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigComplete(rName, description, fleetType, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigComplete(rName, descriptionUpdated, fleetType, instanceTypeUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceTypeUpdate), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_completeWithoutStop(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + displayName := "display name of a test" + displayNameUpdated := "display name of a test updated" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigCompleteWithoutStopping(rName, description, fleetType, instanceType, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "display_name", displayName), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigCompleteWithoutStopping(rName, description, fleetType, instanceType, displayNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "display_name", displayNameUpdated), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_withTags(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + displayName := "display name of a test" + displayNameUpdated := "display name of a test updated" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigWithTags(rName, description, fleetType, instanceType, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "value"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key", "value"), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigWithTags(rName, description, fleetType, instanceType, displayNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "value"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key", "value"), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAppStreamFleetExists(resourceName string, appStreamFleet *appstream.Fleet) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + resp, err := conn.DescribeFleets(&appstream.DescribeFleetsInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if err != nil { + return err + } + + if resp == nil && len(resp.Fleets) == 0 { + return fmt.Errorf("appstream fleet %q does not exist", rs.Primary.ID) + } + + *appStreamFleet = *resp.Fleets[0] + + return nil + } +} + +func testAccCheckAwsAppStreamFleetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appstream_fleet" { + continue + } + + resp, err := conn.DescribeFleets(&appstream.DescribeFleetsInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if resp != nil && len(resp.Fleets) > 0 { + return fmt.Errorf("appstream fleet %q still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAwsAppStreamFleetConfig(name, instanceType string) string { + // "Amazon-AppStream2-Sample-Image-02-04-2019" is not available in GovCloud + return fmt.Sprintf(` +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + instance_type = %[2]q + + compute_capacity { + desired_instances = 1 + } +} +`, name, instanceType) +} + +func testAccAwsAppStreamFleetConfigComplete(name, description, fleetType, instanceType string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType)) +} + +func testAccAwsAppStreamFleetConfigCompleteWithoutStopping(name, description, fleetType, instanceType, displayName string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + display_name = %[5]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType, displayName)) +} + +func testAccAwsAppStreamFleetConfigWithTags(name, description, fleetType, instanceType, displayName string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + display_name = %[5]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + tags = { + Key = "value" + } + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType, displayName)) +} diff --git a/aws/resource_aws_appstream_image_builder.go b/aws/resource_aws_appstream_image_builder.go new file mode 100644 index 000000000000..0c5c06530a1a --- /dev/null +++ b/aws/resource_aws_appstream_image_builder.go @@ -0,0 +1,364 @@ +package aws + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/waiter" +) + +func resourceAwsAppStreamImageBuilder() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppStreamImageBuilderCreate, + ReadWithoutTimeout: resourceAwsAppStreamImageBuilderRead, + UpdateWithoutTimeout: resourceAwsAppStreamImageBuilderUpdate, + DeleteWithoutTimeout: resourceAwsAppStreamImageBuilderDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "access_endpoint": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MinItems: 1, + MaxItems: 4, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.AccessEndpointType_Values(), false), + }, + "vpce_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "appstream_agent_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 100), + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 256), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "domain_join_info": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "directory_name": { + Type: schema.TypeString, + Optional: true, + }, + "organizational_unit_distinguished_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "enable_default_internet_access": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "iam_role_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "image_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"image_arn", "image_name"}, + }, + "image_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"image_name", "image_arn"}, + }, + "instance_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "subnet_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsAppStreamImageBuilderCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("name").(string) + + input := &appstream.CreateImageBuilderInput{ + Name: aws.String(name), + InstanceType: aws.String(d.Get("instance_type").(string)), + } + + if v, ok := d.GetOk("access_endpoint"); ok && v.(*schema.Set).Len() > 0 { + input.AccessEndpoints = expandAccessEndpoints(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("appstream_agent_version"); ok { + input.AppstreamAgentVersion = aws.String(v.(string)) + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("domain_join_info"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DomainJoinInfo = expandDomainJoinInfo(v.([]interface{})) + } + + if v, ok := d.GetOk("enable_default_internet_access"); ok { + input.EnableDefaultInternetAccess = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("image_name"); ok { + input.ImageName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("iam_role_arn"); ok { + input.IamRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("vpc_config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.VpcConfig = expandAppStreamImageBuilderVpcConfig(v.([]interface{})) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AppstreamTags() + } + + output, err := conn.CreateImageBuilderWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Appstream ImageBuilder (%s): %w", name, err)) + } + + d.SetId(aws.StringValue(output.ImageBuilder.Name)) + + if _, err = waiter.ImageBuilderStateRunning(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream ImageBuilder (%s) to be running: %w", d.Id(), err)) + } + + return resourceAwsAppStreamImageBuilderRead(ctx, d, meta) +} + +func resourceAwsAppStreamImageBuilderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + imageBuilder, err := finder.ImageBuilderByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appstream ImageBuilder (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream ImageBuilder (%s): %w", d.Id(), err)) + } + + if imageBuilder == nil { + return diag.FromErr(fmt.Errorf("error reading Appstream ImageBuilder (%s): not found after creation", d.Id())) + } + + arn := aws.StringValue(imageBuilder.Arn) + + d.Set("appstream_agent_version", imageBuilder.AppstreamAgentVersion) + d.Set("arn", arn) + d.Set("created_time", aws.TimeValue(imageBuilder.CreatedTime).Format(time.RFC3339)) + d.Set("description", imageBuilder.Description) + d.Set("display_name", imageBuilder.DisplayName) + d.Set("enable_default_internet_access", imageBuilder.EnableDefaultInternetAccess) + d.Set("image_arn", imageBuilder.ImageArn) + d.Set("iam_role_arn", imageBuilder.IamRoleArn) + d.Set("instance_type", imageBuilder.InstanceType) + + if err = d.Set("access_endpoint", flattenAccessEndpoints(imageBuilder.AccessEndpoints)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "access_endpoints", d.Id(), err)) + } + if err = d.Set("domain_join_info", flattenDomainInfo(imageBuilder.DomainJoinInfo)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "domain_join_info", d.Id(), err)) + } + + if err = d.Set("vpc_config", flattenVpcConfig(imageBuilder.VpcConfig)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "vpc_config", d.Id(), err)) + } + + d.Set("name", imageBuilder.Name) + d.Set("state", imageBuilder.State) + + tags, err := keyvaluetags.AppstreamListTags(conn, arn) + if err != nil { + return diag.FromErr(fmt.Errorf("error listing tags for AppStream ImageBuilder (%s): %w", arn, err)) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) + } + + return nil +} + +func resourceAwsAppStreamImageBuilderUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + if d.HasChange("tags_all") { + conn := meta.(*AWSClient).appstreamconn + + o, n := d.GetChange("tags_all") + + if err := keyvaluetags.AppstreamUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating tags for AppStream ImageBuilder (%s): %w", d.Id(), err)) + } + } + + return resourceAwsAppStreamImageBuilderRead(ctx, d, meta) +} + +func resourceAwsAppStreamImageBuilderDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + _, err := conn.DeleteImageBuilderWithContext(ctx, &appstream.DeleteImageBuilderInput{ + Name: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting Appstream ImageBuilder (%s): %w", d.Id(), err)) + } + + if _, err = waiter.ImageBuilderStateDeleted(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error waiting for Appstream ImageBuilder (%s) to be deleted: %w", d.Id(), err)) + } + + return nil +} + +func expandAppStreamImageBuilderVpcConfig(tfList []interface{}) *appstream.VpcConfig { + if len(tfList) == 0 { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + + if !ok { + return nil + } + + apiObject := &appstream.VpcConfig{} + + if v, ok := tfMap["security_group_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SecurityGroupIds = expandStringSet(v) + } + if v, ok := tfMap["subnet_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SubnetIds = expandStringSet(v) + } + + return apiObject +} diff --git a/aws/resource_aws_appstream_image_builder_test.go b/aws/resource_aws_appstream_image_builder_test.go new file mode 100644 index 000000000000..fd5de3f34a19 --- /dev/null +++ b/aws/resource_aws_appstream_image_builder_test.go @@ -0,0 +1,345 @@ +package aws + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/lister" +) + +func init() { + resource.AddTestSweepers("aws_appstream_image_builder", &resource.Sweeper{ + Name: "aws_appstream_image_builder", + F: testSweepAppStreamImageBuilder, + Dependencies: []string{ + "aws_vpc", + "aws_subnet", + }, + }) +} + +func testSweepAppStreamImageBuilder(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).appstreamconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + + input := &appstream.DescribeImageBuildersInput{} + + err = lister.DescribeImageBuildersPagesWithContext(context.TODO(), conn, input, func(page *appstream.DescribeImageBuildersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, imageBuilder := range page.ImageBuilders { + if imageBuilder == nil { + continue + } + + id := aws.StringValue(imageBuilder.Name) + + r := resourceAwsAppStreamImageBuilder() + d := r.Data(nil) + d.SetId(id) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppStream Image Builders: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppStream Image Builders for %s: %w", region, err)) + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping AppStream Image Builders sweep for %s: %s", region, err) + return nil // In case we have completed some pages, but had errors + } + + return errs.ErrorOrNil() +} + +func TestAccAwsAppStreamImageBuilder_basic(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfig(instanceType, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_disappears(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + instanceType := "stream.standard.medium" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfig(instanceType, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppStreamImageBuilder(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_complete(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + instanceType := "stream.standard.small" + instanceTypeUpdate := "stream.standard.medium" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfigComplete(rName, description, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + { + Config: testAccAwsAppStreamImageBuilderConfigComplete(rName, descriptionUpdated, instanceTypeUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceTypeUpdate), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_Tags(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + instanceType := "stream.standard.small" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfigTags1(instanceType, rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + { + Config: testAccAwsAppStreamImageBuilderConfigTags2(instanceType, rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAwsAppStreamImageBuilderConfigTags1(instanceType, rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppStreamImageBuilderExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + imageBuilder, err := finder.ImageBuilderByName(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + if imageBuilder == nil { + return fmt.Errorf("appstream imageBuilder %q does not exist", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAwsAppStreamImageBuilderDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appstream_image_builder" { + continue + } + + imageBuilder, err := finder.ImageBuilderByName(context.Background(), conn, rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if imageBuilder != nil { + return fmt.Errorf("appstream imageBuilder %q still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAwsAppStreamImageBuilderConfig(instanceType, name string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q +} +`, instanceType, name) +} + +func testAccAwsAppStreamImageBuilderConfigComplete(name, description, instanceType string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "test" { + availability_zone = data.aws_availability_zones.available.names[1] + cidr_block = "10.1.0.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + name = %[1]q + description = %[2]q + enable_default_internet_access = false + instance_type = %[3]q + vpc_config { + subnet_ids = [aws_subnet.test.id] + } +} +`, name, description, instanceType)) +} + +func testAccAwsAppStreamImageBuilderConfigTags1(instanceType, name, key, value string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + } +} +`, instanceType, name, key, value) +} + +func testAccAwsAppStreamImageBuilderConfigTags2(instanceType, name, key1, value1, key2, value2 string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + %[5]q = %[6]q + } +} +`, instanceType, name, key1, value1, key2, value2) +} diff --git a/aws/resource_aws_appstream_stack.go b/aws/resource_aws_appstream_stack.go new file mode 100644 index 000000000000..88f3fc2ecfd4 --- /dev/null +++ b/aws/resource_aws_appstream_stack.go @@ -0,0 +1,714 @@ +package aws + +import ( + "bytes" + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/hashcode" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/waiter" +) + +var ( + flagDiffUserSettings = false +) + +func resourceAwsAppStreamStack() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppStreamStackCreate, + ReadWithoutTimeout: resourceAwsAppStreamStackRead, + UpdateWithoutTimeout: resourceAwsAppStreamStackUpdate, + DeleteWithoutTimeout: resourceAwsAppStreamStackDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "access_endpoints": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MinItems: 1, + MaxItems: 4, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.AccessEndpointType_Values(), false), + }, + "vpce_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + Set: accessEndpointsHash, + }, + "application_settings": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "settings_group": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 256), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "embed_host_domains": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MinItems: 1, + MaxItems: 20, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(0, 128), + }, + Set: schema.HashString, + }, + "feedback_url": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "redirect_url": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "storage_connectors": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "connector_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.StorageConnectorType_Values(), false), + }, + "domains": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 50, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 64), + }, + }, + "resource_identifier": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + }, + }, + Set: storageConnectorsHash, + }, + "user_settings": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MinItems: 1, + DiffSuppressFunc: suppressAppsStreamStackUserSettings, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.Action_Values(), false), + }, + "permission": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.Permission_Values(), false), + }, + }, + }, + Set: userSettingsHash, + }, + "tags": tagsSchemaForceNew(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +func resourceAwsAppStreamStackCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + input := &appstream.CreateStackInput{ + Name: aws.String(d.Get("name").(string)), + } + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + if v, ok := d.GetOk("access_endpoints"); ok { + input.AccessEndpoints = expandAccessEndpoints(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("application_settings"); ok { + input.ApplicationSettings = expandApplicationSettings(v.([]interface{})) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("embed_host_domains"); ok { + input.EmbedHostDomains = expandStringList(v.([]interface{})) + } + + if v, ok := d.GetOk("feedback_url"); ok { + input.FeedbackURL = aws.String(v.(string)) + } + + if v, ok := d.GetOk("redirect_url"); ok { + input.RedirectURL = aws.String(v.(string)) + } + + if v, ok := d.GetOk("storage_connectors"); ok { + input.StorageConnectors = expandStorageConnectors(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("user_settings"); ok { + input.UserSettings = expandUserSettings(v.(*schema.Set).List()) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AppstreamTags() + } + + var err error + var output *appstream.CreateStackOutput + err = resource.RetryContext(ctx, waiter.StackOperationTimeout, func() *resource.RetryError { + output, err = conn.CreateStackWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + output, err = conn.CreateStackWithContext(ctx, input) + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Appstream Stack (%s): %w", d.Id(), err)) + } + + d.SetId(aws.StringValue(output.Stack.Name)) + + return resourceAwsAppStreamStackRead(ctx, d, meta) +} + +func resourceAwsAppStreamStackRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + resp, err := conn.DescribeStacksWithContext(ctx, &appstream.DescribeStacksInput{Names: []*string{aws.String(d.Id())}}) + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appstream Stack (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream Stack (%s): %w", d.Id(), err)) + } + for _, v := range resp.Stacks { + + if err = d.Set("access_endpoints", flattenAccessEndpoints(v.AccessEndpoints)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "access_endpoints", d.Id(), err)) + } + if err = d.Set("application_settings", flattenApplicationSettings(v.ApplicationSettings)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "user_settings", d.Id(), err)) + } + d.Set("arn", v.Arn) + d.Set("created_time", aws.TimeValue(v.CreatedTime).Format(time.RFC3339)) + d.Set("description", v.Description) + d.Set("display_name", v.DisplayName) + if err = d.Set("embed_host_domains", flattenStringList(v.EmbedHostDomains)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "user_settings", d.Id(), err)) + } + d.Set("feedback_url", v.FeedbackURL) + d.Set("name", v.Name) + d.Set("redirect_url", v.RedirectURL) + if err = d.Set("storage_connectors", flattenStorageConnectors(v.StorageConnectors)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "storage_connectors", d.Id(), err)) + } + if err = d.Set("user_settings", flattenUserSettings(v.UserSettings)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "user_settings", d.Id(), err)) + } + + tg, err := conn.ListTagsForResource(&appstream.ListTagsForResourceInput{ + ResourceArn: v.Arn, + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error listing stack tags for AppStream Stack (%s): %w", d.Id(), err)) + } + + tags := keyvaluetags.AppstreamKeyValueTags(tg.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err = d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags", d.Id(), err)) + } + + if err = d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags_all", d.Id(), err)) + } + } + return nil +} + +func resourceAwsAppStreamStackUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + input := &appstream.UpdateStackInput{ + Name: aws.String(d.Id()), + } + + if d.HasChange("access_endpoints") { + input.AccessEndpoints = expandAccessEndpoints(d.Get("access_endpoints").(*schema.Set).List()) + } + + if d.HasChange("application_settings") { + input.ApplicationSettings = expandApplicationSettings(d.Get("application_settings").(*schema.Set).List()) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChange("feedback_url") { + input.FeedbackURL = aws.String(d.Get("feedback_url").(string)) + } + + if d.HasChange("redirect_url") { + input.RedirectURL = aws.String(d.Get("redirect_url").(string)) + } + + if d.HasChange("user_settings") { + input.UserSettings = expandUserSettings(d.Get("user_settings").([]interface{})) + } + + resp, err := conn.UpdateStack(input) + + if err != nil { + diag.FromErr(fmt.Errorf("error updating Appstream Stack (%s): %w", d.Id(), err)) + } + + if d.HasChange("tags") { + arn := aws.StringValue(resp.Stack.Arn) + + o, n := d.GetChange("tags") + if err := keyvaluetags.AppstreamUpdateTags(conn, arn, o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating Appstream Stack tags (%s): %w", d.Id(), err)) + } + } + + return resourceAwsAppStreamStackRead(ctx, d, meta) +} + +func resourceAwsAppStreamStackDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + _, err := conn.DeleteStackWithContext(ctx, &appstream.DeleteStackInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error deleting Appstream Stack (%s): %w", d.Id(), err)) + } + + if _, err = waiter.StackStateDeleted(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + + return diag.FromErr(fmt.Errorf("error waiting for Appstream Stack (%s) to be deleted: %w", d.Id(), err)) + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream Stack (%s): %w", d.Id(), err)) + } + + return nil +} + +func expandAccessEndpoint(tfMap map[string]interface{}) *appstream.AccessEndpoint { + if tfMap == nil { + return nil + } + + apiObject := &appstream.AccessEndpoint{ + EndpointType: aws.String(tfMap["endpoint_type"].(string)), + } + if v, ok := tfMap["vpce_id"]; ok { + apiObject.VpceId = aws.String(v.(string)) + } + + return apiObject +} + +func expandAccessEndpoints(tfList []interface{}) []*appstream.AccessEndpoint { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*appstream.AccessEndpoint + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandAccessEndpoint(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenAccessEndpoint(apiObject *appstream.AccessEndpoint) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + tfMap["endpoint_type"] = aws.StringValue(apiObject.EndpointType) + tfMap["vpce_id"] = aws.StringValue(apiObject.VpceId) + + return tfMap +} + +func flattenAccessEndpoints(apiObjects []*appstream.AccessEndpoint) []map[string]interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []map[string]interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenAccessEndpoint(apiObject)) + } + + return tfList +} + +func expandApplicationSetting(tfMap map[string]interface{}) *appstream.ApplicationSettings { + if tfMap == nil { + return nil + } + + apiObject := &appstream.ApplicationSettings{} + + if v, ok := tfMap["enabled"]; ok { + apiObject.Enabled = aws.Bool(v.(bool)) + } + if v, ok := tfMap["settings_group"]; ok { + apiObject.SettingsGroup = aws.String(v.(string)) + } + + return apiObject +} + +func expandApplicationSettings(tfList []interface{}) *appstream.ApplicationSettings { + if len(tfList) == 0 { + return nil + } + + var apiObject *appstream.ApplicationSettings + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject = expandApplicationSetting(tfMap) + } + + return apiObject +} + +func flattenApplicationSetting(apiObject *appstream.ApplicationSettingsResponse) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + tfMap["enabled"] = aws.BoolValue(apiObject.Enabled) + tfMap["settings_group"] = aws.StringValue(apiObject.SettingsGroup) + + return tfMap +} + +func flattenApplicationSettings(apiObject *appstream.ApplicationSettingsResponse) []interface{} { + if apiObject == nil { + return nil + } + + var tfList []interface{} + + tfList = append(tfList, flattenApplicationSetting(apiObject)) + + return tfList +} + +func expandStorageConnector(tfMap map[string]interface{}) *appstream.StorageConnector { + if tfMap == nil { + return nil + } + + apiObject := &appstream.StorageConnector{ + ConnectorType: aws.String(tfMap["connector_type"].(string)), + } + if v, ok := tfMap["domains"]; ok && len(v.([]interface{})) > 0 { + apiObject.Domains = expandStringList(v.([]interface{})) + } + if v, ok := tfMap["resource_identifier"]; ok && v.(string) != "" { + apiObject.ResourceIdentifier = aws.String(v.(string)) + } + + return apiObject +} + +func expandStorageConnectors(tfList []interface{}) []*appstream.StorageConnector { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*appstream.StorageConnector + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandStorageConnector(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenStorageConnector(apiObject *appstream.StorageConnector) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + tfMap["connector_type"] = aws.StringValue(apiObject.ConnectorType) + tfMap["domains"] = aws.StringValueSlice(apiObject.Domains) + tfMap["resource_identifier"] = aws.StringValue(apiObject.ResourceIdentifier) + + return tfMap +} + +func flattenStorageConnectors(apiObjects []*appstream.StorageConnector) []map[string]interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []map[string]interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenStorageConnector(apiObject)) + } + + return tfList +} + +func expandUserSetting(tfMap map[string]interface{}) *appstream.UserSetting { + if tfMap == nil { + return nil + } + + apiObject := &appstream.UserSetting{ + Action: aws.String(tfMap["action"].(string)), + Permission: aws.String(tfMap["permission"].(string)), + } + + return apiObject +} + +func expandUserSettings(tfList []interface{}) []*appstream.UserSetting { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*appstream.UserSetting + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandUserSetting(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenUserSetting(apiObject *appstream.UserSetting) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + tfMap["action"] = aws.StringValue(apiObject.Action) + tfMap["permission"] = aws.StringValue(apiObject.Permission) + + return tfMap +} + +func flattenUserSettings(apiObjects []*appstream.UserSetting) []map[string]interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []map[string]interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + tfList = append(tfList, flattenUserSetting(apiObject)) + } + + return tfList +} + +func suppressAppsStreamStackUserSettings(k, old, new string, d *schema.ResourceData) bool { + count := len(d.Get("user_settings").(*schema.Set).List()) + defaultCount := len(appstream.Action_Values()) + + if count == defaultCount { + flagDiffUserSettings = false + } + + if count != defaultCount && (fmt.Sprintf("%d", count) == new && fmt.Sprintf("%d", defaultCount) == old) { + flagDiffUserSettings = true + } + + return flagDiffUserSettings +} + +func accessEndpointsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(m["endpoint_type"].(string)) + buf.WriteString(m["vpce_id"].(string)) + return hashcode.String(buf.String()) +} + +func storageConnectorsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(m["connector_type"].(string)) + buf.WriteString(fmt.Sprintf("%+v", m["domains"].([]interface{}))) + buf.WriteString(m["resource_identifier"].(string)) + return hashcode.String(buf.String()) +} + +func userSettingsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(m["action"].(string)) + buf.WriteString(m["permission"].(string)) + return hashcode.String(buf.String()) +} diff --git a/aws/resource_aws_appstream_stack_test.go b/aws/resource_aws_appstream_stack_test.go new file mode 100644 index 000000000000..51e16ae93eb1 --- /dev/null +++ b/aws/resource_aws_appstream_stack_test.go @@ -0,0 +1,297 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" +) + +func TestAccAwsAppStreamStack_basic(t *testing.T) { + var stackOutput appstream.Stack + resourceName := "aws_appstream_stack.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamStackDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamStackConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamStack_disappears(t *testing.T) { + var stackOutput appstream.Stack + resourceName := "aws_appstream_stack.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamStackDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamStackConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppStreamStack(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppStreamStack_complete(t *testing.T) { + var stackOutput appstream.Stack + resourceName := "aws_appstream_stack.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamStackDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamStackConfigComplete(rName, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", description), + ), + }, + { + Config: testAccAwsAppStreamStackConfigComplete(rName, descriptionUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamStack_withTags(t *testing.T) { + var stackOutput appstream.Stack + resourceName := "aws_appstream_stack.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamStackDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamStackConfigComplete(rName, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", description), + ), + }, + { + Config: testAccAwsAppStreamStackConfigWithTags(rName, descriptionUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamStackExists(resourceName, &stackOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "value"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key", "value"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAppStreamStackExists(resourceName string, appStreamStack *appstream.Stack) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + resp, err := conn.DescribeStacks(&appstream.DescribeStacksInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if err != nil { + return fmt.Errorf("problem checking for AppStream Stack existence: %w", err) + } + + if resp == nil && len(resp.Stacks) == 0 { + return fmt.Errorf("appstream stack %q does not exist", rs.Primary.ID) + } + + *appStreamStack = *resp.Stacks[0] + + return nil + } +} + +func testAccCheckAwsAppStreamStackDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appstream_stack" { + continue + } + + resp, err := conn.DescribeStacks(&appstream.DescribeStacksInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("problem while checking AppStream Stack was destroyed: %w", err) + } + + if resp != nil && len(resp.Stacks) > 0 { + return fmt.Errorf("appstream stack %q still exists", rs.Primary.ID) + } + } + + return nil + +} + +func testAccAwsAppStreamStackConfig(name string) string { + return fmt.Sprintf(` +resource "aws_appstream_stack" "test" { + name = %[1]q +} +`, name) +} + +func testAccAwsAppStreamStackConfigComplete(name, description string) string { + return fmt.Sprintf(` +resource "aws_appstream_stack" "test" { + name = %[1]q + description = %[2]q + + storage_connectors { + connector_type = "HOMEFOLDERS" + } + + user_settings { + action = "CLIPBOARD_COPY_FROM_LOCAL_DEVICE" + permission = "ENABLED" + } + + user_settings { + action = "CLIPBOARD_COPY_TO_LOCAL_DEVICE" + permission = "ENABLED" + } + + user_settings { + action = "FILE_UPLOAD" + permission = "ENABLED" + } + + user_settings { + action = "FILE_DOWNLOAD" + permission = "ENABLED" + } + + application_settings { + enabled = true + settings_group = "SettingsGroup" + } +} +`, name, description) +} + +func testAccAwsAppStreamStackConfigWithTags(name, description string) string { + return fmt.Sprintf(` +resource "aws_appstream_stack" "test" { + name = %[1]q + description = %[2]q + + storage_connectors { + connector_type = "HOMEFOLDERS" + } + + user_settings { + action = "CLIPBOARD_COPY_FROM_LOCAL_DEVICE" + permission = "ENABLED" + } + + user_settings { + action = "CLIPBOARD_COPY_TO_LOCAL_DEVICE" + permission = "ENABLED" + } + + user_settings { + action = "FILE_UPLOAD" + permission = "DISABLED" + } + + user_settings { + action = "FILE_DOWNLOAD" + permission = "ENABLED" + } + + user_settings { + action = "PRINTING_TO_LOCAL_DEVICE" + permission = "ENABLED" + } + + user_settings { + action = "DOMAIN_PASSWORD_SIGNIN" + permission = "ENABLED" + } + + application_settings { + enabled = true + settings_group = "SettingsGroup" + } + + tags = { + Key = "value" + } +} +`, name, description) +} diff --git a/aws/resource_aws_appsync_api_key_test.go b/aws/resource_aws_appsync_api_key_test.go index e571323db296..b2594f1e15e2 100644 --- a/aws/resource_aws_appsync_api_key_test.go +++ b/aws/resource_aws_appsync_api_key_test.go @@ -21,6 +21,7 @@ func TestAccAWSAppsyncApiKey_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncApiKeyDestroy, Steps: []resource.TestStep{ @@ -49,6 +50,7 @@ func TestAccAWSAppsyncApiKey_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncApiKeyDestroy, Steps: []resource.TestStep{ @@ -84,6 +86,7 @@ func TestAccAWSAppsyncApiKey_Expires(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncApiKeyDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appsync_datasource_test.go b/aws/resource_aws_appsync_datasource_test.go index a10eb4f6663e..ac28cec1a207 100644 --- a/aws/resource_aws_appsync_datasource_test.go +++ b/aws/resource_aws_appsync_datasource_test.go @@ -18,6 +18,7 @@ func TestAccAwsAppsyncDatasource_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -50,6 +51,7 @@ func TestAccAwsAppsyncDatasource_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -82,6 +84,7 @@ func TestAccAwsAppsyncDatasource_DynamoDBConfig_Region(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -116,6 +119,7 @@ func TestAccAwsAppsyncDatasource_DynamoDBConfig_UseCallerCredentials(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -150,6 +154,7 @@ func TestAccAwsAppsyncDatasource_ElasticsearchConfig_Region(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -184,6 +189,7 @@ func TestAccAwsAppsyncDatasource_HTTPConfig_Endpoint(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -220,6 +226,7 @@ func TestAccAwsAppsyncDatasource_Type(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -249,6 +256,7 @@ func TestAccAwsAppsyncDatasource_Type_DynamoDB(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -279,6 +287,7 @@ func TestAccAwsAppsyncDatasource_Type_Elasticsearch(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -308,6 +317,7 @@ func TestAccAwsAppsyncDatasource_Type_HTTP(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -337,6 +347,7 @@ func TestAccAwsAppsyncDatasource_Type_Lambda(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ @@ -365,6 +376,7 @@ func TestAccAwsAppsyncDatasource_Type_None(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncDatasourceDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appsync_function.go b/aws/resource_aws_appsync_function.go index 42d4566f4e50..51c7045d314d 100644 --- a/aws/resource_aws_appsync_function.go +++ b/aws/resource_aws_appsync_function.go @@ -132,13 +132,13 @@ func resourceAwsAppsyncFunctionRead(d *schema.ResourceData, meta interface{}) er d.Set("api_id", apiID) d.Set("function_id", functionID) - d.Set("data_source", aws.StringValue(resp.FunctionConfiguration.DataSourceName)) - d.Set("description", aws.StringValue(resp.FunctionConfiguration.Description)) - d.Set("arn", aws.StringValue(resp.FunctionConfiguration.FunctionArn)) - d.Set("function_version", aws.StringValue(resp.FunctionConfiguration.FunctionVersion)) - d.Set("name", aws.StringValue(resp.FunctionConfiguration.Name)) - d.Set("request_mapping_template", aws.StringValue(resp.FunctionConfiguration.RequestMappingTemplate)) - d.Set("response_mapping_template", aws.StringValue(resp.FunctionConfiguration.ResponseMappingTemplate)) + d.Set("data_source", resp.FunctionConfiguration.DataSourceName) + d.Set("description", resp.FunctionConfiguration.Description) + d.Set("arn", resp.FunctionConfiguration.FunctionArn) + d.Set("function_version", resp.FunctionConfiguration.FunctionVersion) + d.Set("name", resp.FunctionConfiguration.Name) + d.Set("request_mapping_template", resp.FunctionConfiguration.RequestMappingTemplate) + d.Set("response_mapping_template", resp.FunctionConfiguration.ResponseMappingTemplate) return nil } diff --git a/aws/resource_aws_appsync_function_test.go b/aws/resource_aws_appsync_function_test.go index 3a34bc2f27a2..6ef84803fab1 100644 --- a/aws/resource_aws_appsync_function_test.go +++ b/aws/resource_aws_appsync_function_test.go @@ -21,6 +21,7 @@ func TestAccAwsAppsyncFunction_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncFunctionDestroy, Steps: []resource.TestStep{ @@ -59,6 +60,7 @@ func TestAccAwsAppsyncFunction_description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncFunctionDestroy, Steps: []resource.TestStep{ @@ -93,6 +95,7 @@ func TestAccAwsAppsyncFunction_responseMappingTemplate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncFunctionDestroy, Steps: []resource.TestStep{ @@ -119,6 +122,7 @@ func TestAccAwsAppsyncFunction_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncFunctionDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appsync_graphql_api.go b/aws/resource_aws_appsync_graphql_api.go index af4988538c28..5e2c28455d98 100644 --- a/aws/resource_aws_appsync_graphql_api.go +++ b/aws/resource_aws_appsync_graphql_api.go @@ -196,17 +196,22 @@ func resourceAwsAppsyncGraphqlApi() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "xray_enabled": { Type: schema.TypeBool, Optional: true, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsAppsyncGraphqlApiCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appsyncconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &appsync.CreateGraphqlApiInput{ AuthenticationType: aws.String(d.Get("authentication_type").(string)), @@ -229,8 +234,8 @@ func resourceAwsAppsyncGraphqlApiCreate(d *schema.ResourceData, meta interface{} input.AdditionalAuthenticationProviders = expandAppsyncGraphqlApiAdditionalAuthProviders(v.([]interface{}), meta.(*AWSClient).region) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().AppsyncTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AppsyncTags() } if v, ok := d.GetOk("xray_enabled"); ok { @@ -253,6 +258,7 @@ func resourceAwsAppsyncGraphqlApiCreate(d *schema.ResourceData, meta interface{} func resourceAwsAppsyncGraphqlApiRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appsyncconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &appsync.GetGraphqlApiInput{ @@ -295,11 +301,18 @@ func resourceAwsAppsyncGraphqlApiRead(d *schema.ResourceData, meta interface{}) return fmt.Errorf("error setting uris: %s", err) } - if err := d.Set("tags", keyvaluetags.AppsyncKeyValueTags(resp.GraphqlApi.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.AppsyncKeyValueTags(resp.GraphqlApi.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } - if err := d.Set("xray_enabled", aws.BoolValue(resp.GraphqlApi.XrayEnabled)); err != nil { + if err := d.Set("xray_enabled", resp.GraphqlApi.XrayEnabled); err != nil { return fmt.Errorf("error setting xray_enabled: %s", err) } @@ -309,8 +322,8 @@ func resourceAwsAppsyncGraphqlApiRead(d *schema.ResourceData, meta interface{}) func resourceAwsAppsyncGraphqlApiUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appsyncconn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AppsyncUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating AppSync GraphQL API (%s) tags: %s", d.Get("arn").(string), err) diff --git a/aws/resource_aws_appsync_graphql_api_test.go b/aws/resource_aws_appsync_graphql_api_test.go index d3fc44785f30..dfb5bb443120 100644 --- a/aws/resource_aws_appsync_graphql_api_test.go +++ b/aws/resource_aws_appsync_graphql_api_test.go @@ -71,6 +71,7 @@ func TestAccAWSAppsyncGraphqlApi_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -107,6 +108,7 @@ func TestAccAWSAppsyncGraphqlApi_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -129,6 +131,7 @@ func TestAccAWSAppsyncGraphqlApi_Schema(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -173,6 +176,7 @@ func TestAccAWSAppsyncGraphqlApi_AuthenticationType(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -206,6 +210,7 @@ func TestAccAWSAppsyncGraphqlApi_AuthenticationType_APIKey(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -234,6 +239,7 @@ func TestAccAWSAppsyncGraphqlApi_AuthenticationType_AWSIAM(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -263,6 +269,7 @@ func TestAccAWSAppsyncGraphqlApi_AuthenticationType_AmazonCognitoUserPools(t *te resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -293,6 +300,7 @@ func TestAccAWSAppsyncGraphqlApi_AuthenticationType_OpenIDConnect(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -322,6 +330,7 @@ func TestAccAWSAppsyncGraphqlApi_LogConfig(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -352,6 +361,7 @@ func TestAccAWSAppsyncGraphqlApi_LogConfig_FieldLogLevel(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -402,6 +412,7 @@ func TestAccAWSAppsyncGraphqlApi_LogConfig_ExcludeVerboseContent(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -442,6 +453,7 @@ func TestAccAWSAppsyncGraphqlApi_OpenIDConnectConfig_AuthTTL(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -481,6 +493,7 @@ func TestAccAWSAppsyncGraphqlApi_OpenIDConnectConfig_ClientID(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -520,6 +533,7 @@ func TestAccAWSAppsyncGraphqlApi_OpenIDConnectConfig_IatTTL(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -559,6 +573,7 @@ func TestAccAWSAppsyncGraphqlApi_OpenIDConnectConfig_Issuer(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -597,6 +612,7 @@ func TestAccAWSAppsyncGraphqlApi_Name(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -626,6 +642,7 @@ func TestAccAWSAppsyncGraphqlApi_UserPoolConfig_AwsRegion(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -668,6 +685,7 @@ func TestAccAWSAppsyncGraphqlApi_UserPoolConfig_DefaultAction(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -709,6 +727,7 @@ func TestAccAWSAppsyncGraphqlApi_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -749,6 +768,7 @@ func TestAccAWSAppsyncGraphqlApi_AdditionalAuthentication_APIKey(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -781,6 +801,7 @@ func TestAccAWSAppsyncGraphqlApi_AdditionalAuthentication_AWSIAM(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -814,6 +835,7 @@ func TestAccAWSAppsyncGraphqlApi_AdditionalAuthentication_CognitoUserPools(t *te resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -847,6 +869,7 @@ func TestAccAWSAppsyncGraphqlApi_AdditionalAuthentication_OpenIDConnect(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -881,6 +904,7 @@ func TestAccAWSAppsyncGraphqlApi_AdditionalAuthentication_Multiple(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ @@ -921,6 +945,7 @@ func TestAccAWSAppsyncGraphqlApi_XrayEnabled(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_appsync_resolver.go b/aws/resource_aws_appsync_resolver.go index fd16d129dbf1..63408ce58654 100644 --- a/aws/resource_aws_appsync_resolver.go +++ b/aws/resource_aws_appsync_resolver.go @@ -45,11 +45,11 @@ func resourceAwsAppsyncResolver() *schema.Resource { }, "request_template": { Type: schema.TypeString, - Required: true, + Optional: true, }, "response_template": { Type: schema.TypeString, - Required: true, // documentation bug, the api returns 400 if this is not specified. + Optional: true, }, "kind": { Type: schema.TypeString, @@ -110,12 +110,10 @@ func resourceAwsAppsyncResolverCreate(d *schema.ResourceData, meta interface{}) conn := meta.(*AWSClient).appsyncconn input := &appsync.CreateResolverInput{ - ApiId: aws.String(d.Get("api_id").(string)), - TypeName: aws.String(d.Get("type").(string)), - FieldName: aws.String(d.Get("field").(string)), - RequestMappingTemplate: aws.String(d.Get("request_template").(string)), - ResponseMappingTemplate: aws.String(d.Get("response_template").(string)), - Kind: aws.String(d.Get("kind").(string)), + ApiId: aws.String(d.Get("api_id").(string)), + TypeName: aws.String(d.Get("type").(string)), + FieldName: aws.String(d.Get("field").(string)), + Kind: aws.String(d.Get("kind").(string)), } if v, ok := d.GetOk("data_source"); ok { @@ -129,6 +127,14 @@ func resourceAwsAppsyncResolverCreate(d *schema.ResourceData, meta interface{}) } } + if v, ok := d.GetOk("request_template"); ok { + input.RequestMappingTemplate = aws.String(v.(string)) + } + + if v, ok := d.GetOk("response_template"); ok { + input.ResponseMappingTemplate = aws.String(v.(string)) + } + if v, ok := d.GetOk("caching_config"); ok { input.CachingConfig = expandAppsyncResolverCachingConfig(v.([]interface{})) } @@ -201,12 +207,10 @@ func resourceAwsAppsyncResolverUpdate(d *schema.ResourceData, meta interface{}) conn := meta.(*AWSClient).appsyncconn input := &appsync.UpdateResolverInput{ - ApiId: aws.String(d.Get("api_id").(string)), - FieldName: aws.String(d.Get("field").(string)), - TypeName: aws.String(d.Get("type").(string)), - RequestMappingTemplate: aws.String(d.Get("request_template").(string)), - ResponseMappingTemplate: aws.String(d.Get("response_template").(string)), - Kind: aws.String(d.Get("kind").(string)), + ApiId: aws.String(d.Get("api_id").(string)), + FieldName: aws.String(d.Get("field").(string)), + TypeName: aws.String(d.Get("type").(string)), + Kind: aws.String(d.Get("kind").(string)), } if v, ok := d.GetOk("data_source"); ok { @@ -220,6 +224,14 @@ func resourceAwsAppsyncResolverUpdate(d *schema.ResourceData, meta interface{}) } } + if v, ok := d.GetOk("request_template"); ok { + input.RequestMappingTemplate = aws.String(v.(string)) + } + + if v, ok := d.GetOk("response_template"); ok { + input.ResponseMappingTemplate = aws.String(v.(string)) + } + if v, ok := d.GetOk("caching_config"); ok { input.CachingConfig = expandAppsyncResolverCachingConfig(v.([]interface{})) } @@ -316,7 +328,7 @@ func flattenAppsyncCachingConfig(c *appsync.CachingConfig) []interface{} { return nil } - if len(c.CachingKeys) == 0 && *(c.Ttl) == 0 { + if len(c.CachingKeys) == 0 && aws.Int64Value(c.Ttl) == 0 { return nil } diff --git a/aws/resource_aws_appsync_resolver_test.go b/aws/resource_aws_appsync_resolver_test.go index cff425f1a610..662b43af564d 100644 --- a/aws/resource_aws_appsync_resolver_test.go +++ b/aws/resource_aws_appsync_resolver_test.go @@ -19,6 +19,7 @@ func TestAccAwsAppsyncResolver_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -49,6 +50,7 @@ func TestAccAwsAppsyncResolver_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -72,6 +74,7 @@ func TestAccAwsAppsyncResolver_DataSource(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -98,6 +101,33 @@ func TestAccAwsAppsyncResolver_DataSource(t *testing.T) { }) } +func TestAccAwsAppsyncResolver_DataSource_lambda(t *testing.T) { + var resolver appsync.Resolver + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_DataSource_lambda(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName, &resolver), + resource.TestCheckResourceAttr(resourceName, "data_source", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAwsAppsyncResolver_RequestTemplate(t *testing.T) { var resolver1, resolver2 appsync.Resolver rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) @@ -105,6 +135,7 @@ func TestAccAwsAppsyncResolver_RequestTemplate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -138,6 +169,7 @@ func TestAccAwsAppsyncResolver_ResponseTemplate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -171,6 +203,7 @@ func TestAccAwsAppsyncResolver_multipleResolvers(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -200,6 +233,7 @@ func TestAccAwsAppsyncResolver_PipelineConfig(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -227,6 +261,7 @@ func TestAccAwsAppsyncResolver_CachingConfig(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(appsync.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, appsync.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, Steps: []resource.TestStep{ @@ -455,6 +490,53 @@ EOF `, rName, dataSource) } +func testAccAppsyncResolver_DataSource_lambda(rName string) string { + return testAccAppsyncDatasourceConfig_base_Lambda(rName) + fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %q + + schema = < 0 { - input.Tags = keyvaluetags.New(v).IgnoreAws().AthenaTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AthenaTags() } _, err := conn.CreateWorkGroup(input) @@ -169,6 +172,7 @@ func resourceAwsAthenaWorkgroupCreate(d *schema.ResourceData, meta interface{}) func resourceAwsAthenaWorkgroupRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).athenaconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &athena.GetWorkGroupInput{ @@ -217,8 +221,15 @@ func resourceAwsAthenaWorkgroupRead(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error listing tags for resource (%s): %s", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -275,8 +286,8 @@ func resourceAwsAthenaWorkgroupUpdate(d *schema.ResourceData, meta interface{}) } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.AthenaUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) } @@ -310,6 +321,10 @@ func expandAthenaWorkGroupConfiguration(l []interface{}) *athena.WorkGroupConfig configuration.ResultConfiguration = expandAthenaWorkGroupResultConfiguration(v.([]interface{})) } + if v, ok := m["requester_pays_enabled"]; ok { + configuration.RequesterPaysEnabled = aws.Bool(v.(bool)) + } + return configuration } @@ -340,6 +355,10 @@ func expandAthenaWorkGroupConfigurationUpdates(l []interface{}) *athena.WorkGrou configurationUpdates.ResultConfigurationUpdates = expandAthenaWorkGroupResultConfigurationUpdates(v.([]interface{})) } + if v, ok := m["requester_pays_enabled"]; ok { + configurationUpdates.RequesterPaysEnabled = aws.Bool(v.(bool)) + } + return configurationUpdates } @@ -417,6 +436,7 @@ func flattenAthenaWorkGroupConfiguration(configuration *athena.WorkGroupConfigur "enforce_workgroup_configuration": aws.BoolValue(configuration.EnforceWorkGroupConfiguration), "publish_cloudwatch_metrics_enabled": aws.BoolValue(configuration.PublishCloudWatchMetricsEnabled), "result_configuration": flattenAthenaWorkGroupResultConfiguration(configuration.ResultConfiguration), + "requester_pays_enabled": aws.BoolValue(configuration.RequesterPaysEnabled), } return []interface{}{m} diff --git a/aws/resource_aws_athena_workgroup_test.go b/aws/resource_aws_athena_workgroup_test.go index d6128fd5fd94..efdf8cbe2cbe 100644 --- a/aws/resource_aws_athena_workgroup_test.go +++ b/aws/resource_aws_athena_workgroup_test.go @@ -18,6 +18,7 @@ func TestAccAWSAthenaWorkGroup_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -29,6 +30,7 @@ func TestAccAWSAthenaWorkGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), resource.TestCheckResourceAttr(resourceName, "configuration.0.enforce_workgroup_configuration", "true"), resource.TestCheckResourceAttr(resourceName, "configuration.0.publish_cloudwatch_metrics_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.requester_pays_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "description", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "state", athena.WorkGroupStateEnabled), @@ -52,6 +54,7 @@ func TestAccAWSAthenaWorkGroup_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -74,6 +77,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_BytesScannedCutoffPerQuery(t *testi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -110,6 +114,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_EnforceWorkgroupConfiguration(t *te resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -146,6 +151,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_PublishCloudWatchMetricsEnabled(t * resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -182,6 +188,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_EncryptionConfi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -214,6 +221,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_EncryptionConfi resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -256,6 +264,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_OutputLocation( resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -287,6 +296,45 @@ func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_OutputLocation( }) } +func TestAccAWSAthenaWorkGroup_Configuration_RequesterPaysEnabled(t *testing.T) { + var workgroup1 athena.WorkGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_athena_workgroup.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAthenaWorkGroupConfigConfigurationRequesterPaysEnabled(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAthenaWorkGroupExists(resourceName, &workgroup1), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "athena", fmt.Sprintf("workgroup/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.requester_pays_enabled", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy"}, + }, + { + Config: testAccAthenaWorkGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAthenaWorkGroupExists(resourceName, &workgroup1), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "athena", fmt.Sprintf("workgroup/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.requester_pays_enabled", "false"), + ), + }, + }, + }) +} // + func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_OutputLocation_ForceDestroy(t *testing.T) { var workgroup1, workgroup2 athena.WorkGroup rName := acctest.RandomWithPrefix("tf-acc-test") @@ -296,6 +344,7 @@ func TestAccAWSAthenaWorkGroup_Configuration_ResultConfiguration_OutputLocation_ resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -336,6 +385,7 @@ func TestAccAWSAthenaWorkGroup_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -370,6 +420,7 @@ func TestAccAWSAthenaWorkGroup_State(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -414,6 +465,7 @@ func TestAccAWSAthenaWorkGroup_ForceDestroy(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -442,6 +494,7 @@ func TestAccAWSAthenaWorkGroup_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, athena.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAthenaWorkGroupDestroy, Steps: []resource.TestStep{ @@ -639,6 +692,18 @@ resource "aws_athena_workgroup" "test" { `, rName, bucketName) } +func testAccAthenaWorkGroupConfigConfigurationRequesterPaysEnabled(rName string) string { + return fmt.Sprintf(` +resource "aws_athena_workgroup" "test" { + name = %[1]q + + configuration { + requester_pays_enabled = true + } +} +`, rName) +} + func testAccAthenaWorkGroupConfigConfigurationResultConfigurationOutputLocationForceDestroy(rName string, bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "test" { diff --git a/aws/resource_aws_autoscaling_attachment.go b/aws/resource_aws_autoscaling_attachment.go index f15e1abde087..9648d86f5724 100644 --- a/aws/resource_aws_autoscaling_attachment.go +++ b/aws/resource_aws_autoscaling_attachment.go @@ -93,7 +93,7 @@ func resourceAwsAutoscalingAttachmentRead(d *schema.ResourceData, meta interface if v, ok := d.GetOk("elb"); ok { found := false for _, i := range asg.LoadBalancerNames { - if v.(string) == *i { + if v.(string) == aws.StringValue(i) { d.Set("elb", v.(string)) found = true break @@ -109,7 +109,7 @@ func resourceAwsAutoscalingAttachmentRead(d *schema.ResourceData, meta interface if v, ok := d.GetOk("alb_target_group_arn"); ok { found := false for _, i := range asg.TargetGroupARNs { - if v.(string) == *i { + if v.(string) == aws.StringValue(i) { d.Set("alb_target_group_arn", v.(string)) found = true break diff --git a/aws/resource_aws_autoscaling_attachment_test.go b/aws/resource_aws_autoscaling_attachment_test.go index a954d9576c67..7c5c63b21108 100644 --- a/aws/resource_aws_autoscaling_attachment_test.go +++ b/aws/resource_aws_autoscaling_attachment_test.go @@ -17,6 +17,7 @@ func TestAccAWSAutoscalingAttachment_elb(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutocalingAttachmentDestroy, Steps: []resource.TestStep{ @@ -60,6 +61,7 @@ func TestAccAWSAutoscalingAttachment_albTargetGroup(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutocalingAttachmentDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_autoscaling_group.go b/aws/resource_aws_autoscaling_group.go index 23550037f161..09ffa3a56b66 100644 --- a/aws/resource_aws_autoscaling_group.go +++ b/aws/resource_aws_autoscaling_group.go @@ -25,11 +25,10 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/naming" + tfautoscaling "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscaling" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscaling/waiter" -) - -const ( - autoscalingTagResourceTypeAutoScalingGroup = `auto-scaling-group` + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) func resourceAwsAutoscalingGroup() *schema.Resource { @@ -55,24 +54,26 @@ func resourceAwsAutoscalingGroup() *schema.Resource { ConflictsWith: []string{"name_prefix"}, ValidateFunc: validation.StringLenBetween(0, 255), }, + "name_prefix": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateFunc: validation.StringLenBetween(0, 255-resource.UniqueIDSuffixLength), + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateFunc: validation.StringLenBetween(0, 255-resource.UniqueIDSuffixLength), }, "launch_configuration": { - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"launch_template"}, + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"launch_configuration", "launch_template", "mixed_instances_policy"}, }, "launch_template": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - ConflictsWith: []string{"launch_configuration"}, + Type: schema.TypeList, + MaxItems: 1, + Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { @@ -96,6 +97,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { }, }, }, + ExactlyOneOf: []string{"launch_configuration", "launch_template", "mixed_instances_policy"}, }, "mixed_instances_policy": { @@ -194,6 +196,31 @@ func resourceAwsAutoscalingGroup() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "launch_template_specification": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "launch_template_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "launch_template_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Default: "$Default", + }, + }, + }, + }, "weighted_capacity": { Type: schema.TypeString, Optional: true, @@ -207,6 +234,7 @@ func resourceAwsAutoscalingGroup() *schema.Resource { }, }, }, + ExactlyOneOf: []string{"launch_configuration", "launch_template", "mixed_instances_policy"}, }, "capacity_rebalance": { @@ -526,6 +554,38 @@ func resourceAwsAutoscalingGroup() *schema.Resource { }, }, }, + + "warm_pool": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pool_state": { + Type: schema.TypeString, + Optional: true, + Default: "Stopped", + ValidateFunc: validation.StringInSlice(autoscaling.WarmPoolState_Values(), false), + }, + "min_size": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "max_group_prepared_capacity": { + Type: schema.TypeInt, + Optional: true, + Default: -1, + }, + }, + }, + }, + + "force_delete_warm_pool": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, CustomizeDiff: customdiff.Sequence( @@ -583,17 +643,7 @@ func generatePutLifecycleHookInputs(asgName string, cfgs []interface{}) []autosc func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).autoscalingconn - var asgName string - if v, ok := d.GetOk("name"); ok { - asgName = v.(string) - } else { - if v, ok := d.GetOk("name_prefix"); ok { - asgName = resource.PrefixedUniqueId(v.(string)) - } else { - asgName = resource.PrefixedUniqueId("tf-asg-") - } - d.Set("name", asgName) - } + asgName := naming.Generate(d.Get("name").(string), d.Get("name_prefix").(string)) createOpts := autoscaling.CreateAutoScalingGroupInput{ AutoScalingGroupName: aws.String(asgName), @@ -629,23 +679,12 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) } } - launchConfigurationValue, launchConfigurationOk := d.GetOk("launch_configuration") - launchTemplateValue, launchTemplateOk := d.GetOk("launch_template") - - if createOpts.MixedInstancesPolicy == nil && !launchConfigurationOk && !launchTemplateOk { - return fmt.Errorf("One of `launch_configuration`, `launch_template`, or `mixed_instances_policy` must be set for an Auto Scaling Group") + if v, ok := d.GetOk("launch_configuration"); ok { + createOpts.LaunchConfigurationName = aws.String(v.(string)) } - if launchConfigurationOk { - createOpts.LaunchConfigurationName = aws.String(launchConfigurationValue.(string)) - } - - if launchTemplateOk { - var err error - createOpts.LaunchTemplate, err = expandLaunchTemplateSpecification(launchTemplateValue.([]interface{})) - if err != nil { - return err - } + if v, ok := d.GetOk("launch_template"); ok { + createOpts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{})) } // Availability Zones are optional if VPC Zone Identifier(s) are specified @@ -653,14 +692,12 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) createOpts.AvailabilityZones = expandStringSet(v.(*schema.Set)) } - resourceID := d.Get("name").(string) - if v, ok := d.GetOk("tag"); ok { - createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, resourceID, autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() + createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, asgName, tfautoscaling.TagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() } if v, ok := d.GetOk("tags"); ok { - createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, resourceID, autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() + createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, asgName, tfautoscaling.TagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() } if v, ok := d.GetOk("capacity_rebalance"); ok { @@ -710,7 +747,7 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) log.Printf("[DEBUG] Auto Scaling Group create configuration: %#v", createOpts) // Retry for IAM eventual consistency - err := resource.Retry(1*time.Minute, func() *resource.RetryError { + err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { _, err := conn.CreateAutoScalingGroup(&createOpts) // ValidationError: You must use a valid fully-formed launch template. Value (tf-acc-test-6643732652421074386) for parameter iamInstanceProfile.name is invalid. Invalid IAM Instance Profile name @@ -731,7 +768,7 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error creating Auto Scaling Group: %s", err) } - d.SetId(d.Get("name").(string)) + d.SetId(asgName) log.Printf("[INFO] Auto Scaling Group ID: %s", d.Id()) if twoPhases { @@ -765,6 +802,17 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) } } + if _, ok := d.GetOk("warm_pool"); ok { + _, err := conn.PutWarmPool(createPutWarmPoolInput(d.Id(), d.Get("warm_pool").([]interface{}))) + + if err != nil { + return fmt.Errorf("error creating Warm Pool for Auto Scaling Group (%s), error: %s", d.Id(), err) + } + + log.Printf("[INFO] Successfully created Warm pool") + + } + return resourceAwsAutoscalingGroupRead(d, meta) } @@ -822,6 +870,7 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e } d.Set("name", g.AutoScalingGroupName) + d.Set("name_prefix", naming.NamePrefixFromName(aws.StringValue(g.AutoScalingGroupName))) d.Set("placement_group", g.PlacementGroup) d.Set("protect_from_scale_in", g.NewInstancesProtectedFromScaleIn) d.Set("service_linked_role_arn", g.ServiceLinkedRoleARN) @@ -837,23 +886,23 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e // Deprecated: In a future major version, this should always set all tags except those ignored. // Remove d.GetOk() and Only() handling. if v, tagOk = d.GetOk("tag"); tagOk { - proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) - if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfMap()); err != nil { + if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfMap()); err != nil { return fmt.Errorf("error setting tag: %w", err) } } if v, tagsOk = d.GetOk("tags"); tagsOk { - proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) - if err := d.Set("tags", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfStringMap()); err != nil { + if err := d.Set("tags", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfStringMap()); err != nil { return fmt.Errorf("error setting tags: %w", err) } } if !tagOk && !tagsOk { - if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).AutoscalingListOfMap()); err != nil { + if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).AutoscalingListOfMap()); err != nil { return fmt.Errorf("error setting tag: %w", err) } } @@ -881,6 +930,10 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e } } + if err := d.Set("warm_pool", flattenWarmPoolConfiguration(g.WarmPoolConfiguration)); err != nil { + return fmt.Errorf("error setting warm_pool for Auto Scaling Group (%s), error: %s", d.Id(), err) + } + return nil } @@ -995,7 +1048,7 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("launch_template") { if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 { - opts.LaunchTemplate, _ = expandLaunchTemplateSpecification(v.([]interface{})) + opts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{})) } shouldRefreshInstances = true } @@ -1060,15 +1113,15 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) oTagRaw, nTagRaw := d.GetChange("tag") oTagsRaw, nTagsRaw := d.GetChange("tags") - oTag := keyvaluetags.AutoscalingKeyValueTags(oTagRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) - oTags := keyvaluetags.AutoscalingKeyValueTags(oTagsRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + oTag := keyvaluetags.AutoscalingKeyValueTags(oTagRaw, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) + oTags := keyvaluetags.AutoscalingKeyValueTags(oTagsRaw, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) oldTags := oTag.Merge(oTags).AutoscalingTags() - nTag := keyvaluetags.AutoscalingKeyValueTags(nTagRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) - nTags := keyvaluetags.AutoscalingKeyValueTags(nTagsRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + nTag := keyvaluetags.AutoscalingKeyValueTags(nTagRaw, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) + nTags := keyvaluetags.AutoscalingKeyValueTags(nTagsRaw, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup) newTags := nTag.Merge(nTags).AutoscalingTags() - if err := keyvaluetags.AutoscalingUpdateTags(conn, d.Id(), autoscalingTagResourceTypeAutoScalingGroup, oldTags, newTags); err != nil { + if err := keyvaluetags.AutoscalingUpdateTags(conn, d.Id(), tfautoscaling.TagResourceTypeAutoScalingGroup, oldTags, newTags); err != nil { return fmt.Errorf("error updating tags for Auto Scaling Group (%s): %w", d.Id(), err) } } @@ -1244,6 +1297,32 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("warm_pool") { + w := d.Get("warm_pool").([]interface{}) + + // No warm pool exists in new config. Delete it. + if len(w) == 0 || w[0] == nil { + g, err := getAwsAutoscalingGroup(d.Id(), conn) + if err != nil { + return err + } + + if err := resourceAutoScalingGroupWarmPoolDelete(g, d, meta); err != nil { + return fmt.Errorf("error deleting Warm pool for Auto Scaling Group %s: %s", d.Id(), err) + } + + log.Printf("[INFO] Successfully removed Warm pool") + } else { + _, err := conn.PutWarmPool(createPutWarmPoolInput(d.Id(), d.Get("warm_pool").([]interface{}))) + + if err != nil { + return fmt.Errorf("error updating Warm Pool for Auto Scaling Group (%s), error: %s", d.Id(), err) + } + + log.Printf("[INFO] Successfully updated Warm pool") + } + } + if shouldWaitForCapacity { if err := waitForASGCapacity(d, meta, capacitySatisfiedUpdate); err != nil { return fmt.Errorf("error waiting for Auto Scaling Group Capacity: %w", err) @@ -1279,7 +1358,13 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) log.Printf("[WARN] Auto Scaling Group (%s) not found, removing from state", d.Id()) return nil } - if len(g.Instances) > 0 || *g.DesiredCapacity > 0 { + + // Try deleting Warm pool first. + if err := resourceAutoScalingGroupWarmPoolDelete(g, d, meta); err != nil { + return fmt.Errorf("error deleting Warm pool for Auto Scaling Group %s: %s", d.Id(), err) + } + + if len(g.Instances) > 0 || aws.Int64Value(g.DesiredCapacity) > 0 { if err := resourceAwsAutoscalingGroupDrain(d, meta); err != nil { return err } @@ -1343,6 +1428,166 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) return nil } +func resourceAutoScalingGroupWarmPoolDelete(g *autoscaling.Group, d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + if g.WarmPoolConfiguration == nil { + // No warm pool configured. Skipping deletion. + return nil + } + + log.Printf("[INFO] Auto Scaling Group has a Warm Pool. First deleting it.") + + if err := resourceAwsAutoscalingGroupWarmPoolDrain(d, meta); err != nil { + return err + } + + // Delete Warm Pool if it is not pending delete. + if g.WarmPoolConfiguration.Status == nil || aws.StringValue(g.WarmPoolConfiguration.Status) != "PendingDelete" { + deleteopts := autoscaling.DeleteWarmPoolInput{ + AutoScalingGroupName: aws.String(d.Id()), + ForceDelete: aws.Bool(d.Get("force_delete").(bool) || d.Get("force_delete_warm_pool").(bool)), + } + + err := resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + _, err := conn.DeleteWarmPool(&deleteopts) + if err != nil { + if callerr, ok := err.(awserr.Error); ok { + switch callerr.Code() { + case "ResourceInUse", "ScalingActivityInProgress": + // These are retryable + return resource.RetryableError(callerr) + } + } + // Didn't recognize the error, so shouldn't retry. + return resource.NonRetryableError(err) + } + // Successful delete + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.DeleteWarmPool(&deleteopts) + } + if err != nil { + return fmt.Errorf("error deleting Warm Pool: %s", err) + } + } + + // Wait for Warm pool to be gone. + if err := waitForWarmPoolDeletion(conn, d); err != nil { + return fmt.Errorf("error waiting for Warm Pool deletion: %s", err) + } + + log.Printf("[INFO] Successfully removed Warm pool") + + return nil +} + +func waitForWarmPoolDeletion(conn *autoscaling.AutoScaling, d *schema.ResourceData) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"", autoscaling.WarmPoolStatusPendingDelete}, + Target: []string{"deleted"}, + Refresh: asgWarmPoolStateRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + NotFoundChecks: 1, + } + + log.Printf("[DEBUG] Waiting for Auto Scaling Group (%s) Warm Pool deletion", d.Id()) + _, err := stateConf.WaitForState() + + if isResourceNotFoundError(err) { + return nil + } + + return err +} + +func asgWarmPoolStateRefreshFunc(conn *autoscaling.AutoScaling, asgName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + g, err := getAwsAutoscalingGroup(asgName, conn) + + if err != nil { + return nil, "", fmt.Errorf("error describing Auto Scaling Group (%s): %s", asgName, err) + } + + if g == nil || g.WarmPoolConfiguration == nil { + return nil, "deleted", nil + } + return asgName, aws.StringValue(g.WarmPoolConfiguration.Status), nil + } +} + +func getAwsAutoscalingGroupWarmPool(asgName string, conn *autoscaling.AutoScaling) (*autoscaling.DescribeWarmPoolOutput, error) { + describeOpts := autoscaling.DescribeWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + } + + log.Printf("[DEBUG] Warm Pool describe configuration input: %#v", describeOpts) + describeWarmPoolOutput, err := conn.DescribeWarmPool(&describeOpts) + if err != nil { + autoscalingerr, ok := err.(awserr.Error) + if ok && autoscalingerr.Code() == "InvalidGroup.NotFound" { + return nil, nil + } + + return nil, fmt.Errorf("error retrieving Warm Pool: %s", err) + } + + return describeWarmPoolOutput, nil +} + +func resourceAwsAutoscalingGroupWarmPoolDrain(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + if d.Get("force_delete").(bool) || d.Get("force_delete_warm_pool").(bool) { + log.Printf("[DEBUG] Skipping Warm pool drain, force delete was set.") + return nil + } + + // First, set the max group prepared capacity and min size to zero for the pool to drain + log.Printf("[DEBUG] Reducing Warm pool capacity to zero") + opts := autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(d.Id()), + MaxGroupPreparedCapacity: aws.Int64(0), + MinSize: aws.Int64(0), + } + if _, err := conn.PutWarmPool(&opts); err != nil { + return fmt.Errorf("error setting capacity to zero to drain: %s", err) + } + + // Next, wait for the Warm Pool to drain + log.Printf("[DEBUG] Waiting for warm pool to have zero instances") + var p *autoscaling.DescribeWarmPoolOutput + err := resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + p, err := getAwsAutoscalingGroupWarmPool(d.Id(), conn) + if err != nil { + return resource.NonRetryableError(err) + } + + if len(p.Instances) == 0 { + return nil + } + + return resource.RetryableError( + fmt.Errorf("Warm pool still has %d instances", len(p.Instances))) + }) + + if isResourceTimeoutError(err) { + p, err = getAwsAutoscalingGroupWarmPool(d.Id(), conn) + if err != nil { + return fmt.Errorf("error getting Warm Pool info when draining Auto Scaling Group %s: %s", d.Id(), err) + } + if p != nil && len(p.Instances) > 0 { + return fmt.Errorf("Warm pool still has %d instances", len(p.Instances)) + } + } + if err != nil { + return fmt.Errorf("error draining Warm pool: %s", err) + } + return nil +} + // TODO: make this a finder // TODO: this should return a NotFoundError if not found func getAwsAutoscalingGroup(asgName string, conn *autoscaling.AutoScaling) (*autoscaling.Group, error) { @@ -1363,7 +1608,11 @@ func getAwsAutoscalingGroup(asgName string, conn *autoscaling.AutoScaling) (*aut // Search for the Auto Scaling Group for idx, asc := range describeGroups.AutoScalingGroups { - if *asc.AutoScalingGroupName == asgName { + if asc == nil { + continue + } + + if aws.StringValue(asc.AutoScalingGroupName) == asgName { return describeGroups.AutoScalingGroups[idx], nil } } @@ -1546,17 +1795,17 @@ func getELBInstanceStates(g *autoscaling.Group, meta interface{}) (map[string]ma elbconn := meta.(*AWSClient).elbconn for _, lbName := range g.LoadBalancerNames { - lbInstanceStates[*lbName] = make(map[string]string) + lbInstanceStates[aws.StringValue(lbName)] = make(map[string]string) opts := &elb.DescribeInstanceHealthInput{LoadBalancerName: lbName} r, err := elbconn.DescribeInstanceHealth(opts) if err != nil { return nil, err } for _, is := range r.InstanceStates { - if is.InstanceId == nil || is.State == nil { + if is == nil || is.InstanceId == nil || is.State == nil { continue } - lbInstanceStates[*lbName][*is.InstanceId] = *is.State + lbInstanceStates[aws.StringValue(lbName)][aws.StringValue(is.InstanceId)] = aws.StringValue(is.State) } } @@ -1575,17 +1824,17 @@ func getTargetGroupInstanceStates(g *autoscaling.Group, meta interface{}) (map[s elbv2conn := meta.(*AWSClient).elbv2conn for _, targetGroupARN := range g.TargetGroupARNs { - targetInstanceStates[*targetGroupARN] = make(map[string]string) + targetInstanceStates[aws.StringValue(targetGroupARN)] = make(map[string]string) opts := &elbv2.DescribeTargetHealthInput{TargetGroupArn: targetGroupARN} r, err := elbv2conn.DescribeTargetHealth(opts) if err != nil { return nil, err } for _, desc := range r.TargetHealthDescriptions { - if desc.Target == nil || desc.Target.Id == nil || desc.TargetHealth == nil || desc.TargetHealth.State == nil { + if desc == nil || desc.Target == nil || desc.Target.Id == nil || desc.TargetHealth == nil || desc.TargetHealth.State == nil { continue } - targetInstanceStates[*targetGroupARN][*desc.Target.Id] = *desc.TargetHealth.State + targetInstanceStates[aws.StringValue(targetGroupARN)][aws.StringValue(desc.Target.Id)] = aws.StringValue(desc.TargetHealth.State) } } @@ -1636,7 +1885,7 @@ func expandAutoScalingInstancesDistribution(l []interface{}) *autoscaling.Instan return instancesDistribution } -func expandAutoScalingLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplate { +func expandMixedInstancesLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplate { if len(l) == 0 || l[0] == nil { return nil } @@ -1644,7 +1893,7 @@ func expandAutoScalingLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplat m := l[0].(map[string]interface{}) launchTemplate := &autoscaling.LaunchTemplate{ - LaunchTemplateSpecification: expandAutoScalingLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})), + LaunchTemplateSpecification: expandMixedInstancesLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})), } if v, ok := m["override"]; ok { @@ -1678,6 +1927,10 @@ func expandAutoScalingLaunchTemplateOverride(m map[string]interface{}) *autoscal launchTemplateOverrides.InstanceType = aws.String(v.(string)) } + if v, ok := m["launch_template_specification"]; ok && v.([]interface{}) != nil { + launchTemplateOverrides.LaunchTemplateSpecification = expandMixedInstancesLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})) + } + if v, ok := m["weighted_capacity"]; ok && v.(string) != "" { launchTemplateOverrides.WeightedCapacity = aws.String(v.(string)) } @@ -1685,7 +1938,7 @@ func expandAutoScalingLaunchTemplateOverride(m map[string]interface{}) *autoscal return launchTemplateOverrides } -func expandAutoScalingLaunchTemplateSpecification(l []interface{}) *autoscaling.LaunchTemplateSpecification { +func expandMixedInstancesLaunchTemplateSpecification(l []interface{}) *autoscaling.LaunchTemplateSpecification { launchTemplateSpecification := &autoscaling.LaunchTemplateSpecification{} if len(l) == 0 || l[0] == nil { @@ -1720,7 +1973,7 @@ func expandAutoScalingMixedInstancesPolicy(l []interface{}) *autoscaling.MixedIn m := l[0].(map[string]interface{}) mixedInstancesPolicy := &autoscaling.MixedInstancesPolicy{ - LaunchTemplate: expandAutoScalingLaunchTemplate(m["launch_template"].([]interface{})), + LaunchTemplate: expandMixedInstancesLaunchTemplate(m["launch_template"].([]interface{})), } if v, ok := m["instances_distribution"]; ok { @@ -1769,8 +2022,9 @@ func flattenAutoScalingLaunchTemplateOverrides(launchTemplateOverrides []*autosc continue } m := map[string]interface{}{ - "instance_type": aws.StringValue(launchTemplateOverride.InstanceType), - "weighted_capacity": aws.StringValue(launchTemplateOverride.WeightedCapacity), + "instance_type": aws.StringValue(launchTemplateOverride.InstanceType), + "launch_template_specification": flattenAutoScalingLaunchTemplateSpecification(launchTemplateOverride.LaunchTemplateSpecification), + "weighted_capacity": aws.StringValue(launchTemplateOverride.WeightedCapacity), } l[i] = m } @@ -1805,6 +2059,25 @@ func flattenAutoScalingMixedInstancesPolicy(mixedInstancesPolicy *autoscaling.Mi return []interface{}{m} } +func flattenWarmPoolConfiguration(warmPoolConfiguration *autoscaling.WarmPoolConfiguration) []interface{} { + if warmPoolConfiguration == nil { + return []interface{}{} + } + + maxGroupPreparedCapacity := int64(-1) + if warmPoolConfiguration.MaxGroupPreparedCapacity != nil { + maxGroupPreparedCapacity = aws.Int64Value(warmPoolConfiguration.MaxGroupPreparedCapacity) + } + + m := map[string]interface{}{ + "pool_state": aws.StringValue(warmPoolConfiguration.PoolState), + "min_size": aws.Int64Value(warmPoolConfiguration.MinSize), + "max_group_prepared_capacity": maxGroupPreparedCapacity, + } + + return []interface{}{m} +} + func waitUntilAutoscalingGroupLoadBalancersAdded(conn *autoscaling.AutoScaling, asgName string) error { input := &autoscaling.DescribeLoadBalancersInput{ AutoScalingGroupName: aws.String(asgName), @@ -1877,6 +2150,32 @@ func waitUntilAutoscalingGroupLoadBalancersRemoved(conn *autoscaling.AutoScaling return nil } +func createPutWarmPoolInput(asgName string, l []interface{}) *autoscaling.PutWarmPoolInput { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + input := autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + } + + if v, ok := m["pool_state"]; ok && v.(string) != "" { + input.PoolState = aws.String(v.(string)) + } + + if v, ok := m["min_size"]; ok && v.(int) > -1 { + input.MinSize = aws.Int64(int64(v.(int))) + } + + if v, ok := m["max_group_prepared_capacity"]; ok && v.(int) > -2 { + input.MaxGroupPreparedCapacity = aws.Int64(int64(v.(int))) + } + + return &input +} + func createAutoScalingGroupInstanceRefreshInput(asgName string, l []interface{}) *autoscaling.StartInstanceRefreshInput { if len(l) == 0 || l[0] == nil { return nil @@ -1989,3 +2288,54 @@ func validateAutoScalingGroupInstanceRefreshTriggerFields(i interface{}, path ct return diag.Errorf("'%s' is not a recognized parameter name for aws_autoscaling_group", v) } + +func expandLaunchTemplateSpecification(specs []interface{}) *autoscaling.LaunchTemplateSpecification { + if len(specs) < 1 { + return nil + } + + spec := specs[0].(map[string]interface{}) + + idValue, idOk := spec["id"] + nameValue, nameOk := spec["name"] + + result := &autoscaling.LaunchTemplateSpecification{} + + // DescribeAutoScalingGroups returns both name and id but LaunchTemplateSpecification + // allows only one of them to be set + if idOk && idValue != "" { + result.LaunchTemplateId = aws.String(idValue.(string)) + } else if nameOk && nameValue != "" { + result.LaunchTemplateName = aws.String(nameValue.(string)) + } + + if v, ok := spec["version"]; ok && v != "" { + result.Version = aws.String(v.(string)) + } + + return result +} + +func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecification) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{} + result := make([]map[string]interface{}, 0) + + // id and name are always returned by DescribeAutoscalingGroups + attrs["id"] = aws.StringValue(lt.LaunchTemplateId) + attrs["name"] = aws.StringValue(lt.LaunchTemplateName) + + // version is returned only if it was previously set + if lt.Version != nil { + attrs["version"] = aws.StringValue(lt.Version) + } else { + attrs["version"] = nil + } + + result = append(result, attrs) + + return result +} diff --git a/aws/resource_aws_autoscaling_group_tag.go b/aws/resource_aws_autoscaling_group_tag.go new file mode 100644 index 000000000000..e24851fdc735 --- /dev/null +++ b/aws/resource_aws_autoscaling_group_tag.go @@ -0,0 +1,133 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfautoscaling "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscaling" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAutoscalingGroupTag() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingGroupTagCreate, + Read: resourceAwsAutoscalingGroupTagRead, + Update: resourceAwsAutoscalingGroupTagUpdate, + Delete: resourceAwsAutoscalingGroupTagDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "autoscaling_group_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "tag": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + "propagate_at_launch": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + }, + } +} + +func resourceAwsAutoscalingGroupTagCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + identifier := d.Get("autoscaling_group_name").(string) + tags := d.Get("tag").([]interface{}) + key := tags[0].(map[string]interface{})["key"].(string) + + if err := keyvaluetags.AutoscalingUpdateTags(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, nil, tags); err != nil { + return fmt.Errorf("error creating AutoScaling Group (%s) tag (%s): %w", identifier, key, err) + } + + d.SetId(tagresource.SetResourceID(identifier, key)) + + return resourceAwsAutoscalingGroupTagRead(d, meta) +} + +func resourceAwsAutoscalingGroupTagRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + value, err := keyvaluetags.AutoscalingGetTag(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, key) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] AutoScaling Group (%s) tag (%s), removing from state", identifier, key) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading AutoScaling Group (%s) tag (%s): %w", identifier, key, err) + } + + d.Set("autoscaling_group_name", identifier) + + if err := d.Set("tag", []map[string]interface{}{{ + "key": key, + "value": value.Value, + "propagate_at_launch": value.AdditionalBoolFields["PropagateAtLaunch"], + }}); err != nil { + return fmt.Errorf("error setting tag: %w", err) + } + + return nil +} + +func resourceAwsAutoscalingGroupTagUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.AutoscalingUpdateTags(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, nil, d.Get("tag")); err != nil { + return fmt.Errorf("error updating AutoScaling Group (%s) tag (%s): %w", identifier, key, err) + } + + return resourceAwsAutoscalingGroupTagRead(d, meta) +} + +func resourceAwsAutoscalingGroupTagDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + identifier, key, err := tagresource.GetResourceID(d.Id()) + + if err != nil { + return err + } + + if err := keyvaluetags.AutoscalingUpdateTags(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, d.Get("tag"), nil); err != nil { + return fmt.Errorf("error deleting AutoScaling Group (%s) tag (%s): %w", identifier, key, err) + } + + return nil +} diff --git a/aws/resource_aws_autoscaling_group_tag_test.go b/aws/resource_aws_autoscaling_group_tag_test.go new file mode 100644 index 000000000000..64f492236261 --- /dev/null +++ b/aws/resource_aws_autoscaling_group_tag_test.go @@ -0,0 +1,194 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfautoscaling "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/autoscaling" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestAccAWSAutoscalingGroupTag_basic(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingGroupTag_disappears(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAutoscalingGroupTag(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingGroupTag_Value(t *testing.T) { + resourceName := "aws_autoscaling_group_tag.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAutoscalingGroupTagDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAutoscalingGroupTagConfig("key1", "value1updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAutoscalingGroupTagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tag.0.key", "key1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "value1updated"), + ), + }, + }, + }) +} + +func testAccCheckAutoscalingGroupTagDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_group_tag" { + continue + } + + identifier, key, err := tagresource.GetResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = keyvaluetags.AutoscalingGetTag(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, key) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("AutoScaling Group (%s) tag (%s) still exists", identifier, key) + } + + return nil +} + +func testAccCheckAutoscalingGroupTagExists(n string) 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("%s: missing resource ID", n) + } + + identifier, key, err := tagresource.GetResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + _, err = keyvaluetags.AutoscalingGetTag(conn, identifier, tfautoscaling.TagResourceTypeAutoScalingGroup, key) + + if err != nil { + return err + } + + return nil + } +} + +func testAccAutoscalingGroupTagConfig(key string, value string) string { + return composeConfig( + testAccAvailableAZsNoOptInDefaultExcludeConfig(), + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name_prefix = "terraform-test-" + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = "t2.nano" +} + +resource "aws_autoscaling_group" "test" { + lifecycle { + ignore_changes = [tag] + } + + availability_zones = [data.aws_availability_zones.available.names[0]] + + min_size = 0 + max_size = 0 + + launch_template { + id = aws_launch_template.test.id + version = "$Latest" + } +} + +resource "aws_autoscaling_group_tag" "test" { + autoscaling_group_name = aws_autoscaling_group.test.name + + tag { + key = %[1]q + value = %[2]q + + propagate_at_launch = true + } +} +`, key, value)) +} diff --git a/aws/resource_aws_autoscaling_group_test.go b/aws/resource_aws_autoscaling_group_test.go index 41aa98811408..62897a65e9fb 100644 --- a/aws/resource_aws_autoscaling_group_test.go +++ b/aws/resource_aws_autoscaling_group_test.go @@ -18,15 +18,24 @@ import ( "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/terraform-providers/terraform-provider-aws/aws/internal/naming" ) func init() { + RegisterServiceErrorCheckFunc(autoscaling.EndpointsID, testAccErrorCheckSkipAutoScaling) + resource.AddTestSweepers("aws_autoscaling_group", &resource.Sweeper{ Name: "aws_autoscaling_group", F: testSweepAutoscalingGroups, }) } +func testAccErrorCheckSkipAutoScaling(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "gp3 is invalid", + ) +} + func testSweepAutoscalingGroups(region string) error { client, err := sharedClientForRegion(region) if err != nil { @@ -87,6 +96,7 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -136,7 +146,6 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -171,21 +180,22 @@ func TestAccAWSAutoScalingGroup_basic(t *testing.T) { }) } -func TestAccAWSAutoScalingGroup_namePrefix(t *testing.T) { - nameRegexp := regexp.MustCompile("^tf-test-") +func TestAccAWSAutoScalingGroup_Name_Generated(t *testing.T) { + var group autoscaling.Group + resourceName := "aws_autoscaling_group.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAutoScalingGroupConfig_namePrefix(), + Config: testAccAWSAutoScalingGroupConfigNameGenerated(), Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "aws_autoscaling_group.test", "name", nameRegexp), - resource.TestCheckResourceAttrSet( - "aws_autoscaling_group.test", "arn"), + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + naming.TestCheckResourceAttrNameGenerated(resourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", "terraform-"), ), }, { @@ -195,7 +205,6 @@ func TestAccAWSAutoScalingGroup_namePrefix(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -206,31 +215,31 @@ func TestAccAWSAutoScalingGroup_namePrefix(t *testing.T) { }) } -func TestAccAWSAutoScalingGroup_autoGeneratedName(t *testing.T) { - asgNameRegexp := regexp.MustCompile("^tf-asg-") +func TestAccAWSAutoScalingGroup_NamePrefix(t *testing.T) { + var group autoscaling.Group + resourceName := "aws_autoscaling_group.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSAutoScalingGroupConfig_autoGeneratedName(), + Config: testAccAWSAutoScalingGroupConfigNamePrefix("tf-acc-test-prefix-"), Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "aws_autoscaling_group.bar", "name", asgNameRegexp), - resource.TestCheckResourceAttrSet( - "aws_autoscaling_group.bar", "arn"), + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + naming.TestCheckResourceAttrNameFromPrefix(resourceName, "name", "tf-acc-test-prefix-"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", "tf-acc-test-prefix-"), ), }, { - ResourceName: "aws_autoscaling_group.bar", + ResourceName: "aws_autoscaling_group.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -244,6 +253,7 @@ func TestAccAWSAutoScalingGroup_autoGeneratedName(t *testing.T) { func TestAccAWSAutoScalingGroup_terminationPolicies(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -261,7 +271,6 @@ func TestAccAWSAutoScalingGroup_terminationPolicies(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -284,7 +293,6 @@ func TestAccAWSAutoScalingGroup_terminationPolicies(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -318,6 +326,7 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -346,7 +355,6 @@ func TestAccAWSAutoScalingGroup_tags(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -381,6 +389,7 @@ func TestAccAWSAutoScalingGroup_VpcUpdates(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -402,7 +411,6 @@ func TestAccAWSAutoScalingGroup_VpcUpdates(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -430,6 +438,7 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -447,7 +456,6 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -464,6 +472,7 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer_ToTargetGroup(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -482,7 +491,6 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer_ToTargetGroup(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -504,7 +512,6 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer_ToTargetGroup(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -526,7 +533,6 @@ func TestAccAWSAutoScalingGroup_WithLoadBalancer_ToTargetGroup(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -543,6 +549,7 @@ func TestAccAWSAutoScalingGroup_withPlacementGroup(t *testing.T) { randName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -560,7 +567,6 @@ func TestAccAWSAutoScalingGroup_withPlacementGroup(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -577,6 +583,7 @@ func TestAccAWSAutoScalingGroup_enablingMetrics(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -595,7 +602,6 @@ func TestAccAWSAutoScalingGroup_enablingMetrics(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -620,6 +626,7 @@ func TestAccAWSAutoScalingGroup_suspendingProcesses(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -646,7 +653,6 @@ func TestAccAWSAutoScalingGroup_suspendingProcesses(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -670,6 +676,7 @@ func TestAccAWSAutoScalingGroup_withMetrics(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -688,7 +695,6 @@ func TestAccAWSAutoScalingGroup_withMetrics(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -712,6 +718,7 @@ func TestAccAWSAutoScalingGroup_serviceLinkedRoleARN(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -730,7 +737,6 @@ func TestAccAWSAutoScalingGroup_serviceLinkedRoleARN(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -746,6 +752,7 @@ func TestAccAWSAutoScalingGroup_MaxInstanceLifetime(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -764,7 +771,6 @@ func TestAccAWSAutoScalingGroup_MaxInstanceLifetime(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -812,6 +818,7 @@ func TestAccAWSAutoScalingGroup_ALB_TargetGroups(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -843,7 +850,6 @@ func TestAccAWSAutoScalingGroup_ALB_TargetGroups(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -872,6 +878,7 @@ func TestAccAWSAutoScalingGroup_TargetGroupArns(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -889,7 +896,6 @@ func TestAccAWSAutoScalingGroup_TargetGroupArns(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -921,6 +927,7 @@ func TestAccAWSAutoScalingGroup_initialLifecycleHook(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -944,7 +951,6 @@ func TestAccAWSAutoScalingGroup_initialLifecycleHook(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -963,6 +969,7 @@ func TestAccAWSAutoScalingGroup_ALB_TargetGroups_ELBCapacity(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -981,7 +988,6 @@ func TestAccAWSAutoScalingGroup_ALB_TargetGroups_ELBCapacity(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -998,6 +1004,7 @@ func TestAccAWSAutoScalingGroup_InstanceRefresh_Basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1049,6 +1056,7 @@ func TestAccAWSAutoScalingGroup_InstanceRefresh_Start(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1089,6 +1097,7 @@ func TestAccAWSAutoScalingGroup_InstanceRefresh_Triggers(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1115,6 +1124,54 @@ func TestAccAWSAutoScalingGroup_InstanceRefresh_Triggers(t *testing.T) { }) } +func TestAccAWSAutoScalingGroup_WarmPool(t *testing.T) { + var group autoscaling.Group + resourceName := "aws_autoscaling_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAutoScalingGroupConfig_WarmPool_Empty(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + resource.TestCheckResourceAttr(resourceName, "warm_pool.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "warm_pool", + "force_delete", + "wait_for_capacity_timeout", + }, + }, + { + Config: testAccAwsAutoScalingGroupConfig_WarmPool_Full(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + resource.TestCheckResourceAttr(resourceName, "warm_pool.#", "1"), + resource.TestCheckResourceAttr(resourceName, "warm_pool.0.pool_state", "Stopped"), + resource.TestCheckResourceAttr(resourceName, "warm_pool.0.min_size", "0"), + resource.TestCheckResourceAttr(resourceName, "warm_pool.0.max_group_prepared_capacity", "2"), + ), + }, + { + Config: testAccAwsAutoScalingGroupConfig_WarmPool_Remove(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + resource.TestCheckNoResourceAttr(resourceName, "warm_pool.#"), + ), + }, + }, + }) +} + func testAccCheckAWSAutoScalingGroupExists(n string, group *autoscaling.Group) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1383,6 +1440,7 @@ func TestAccAWSAutoScalingGroup_classicVpcZoneIdentifier(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1400,7 +1458,6 @@ func TestAccAWSAutoScalingGroup_classicVpcZoneIdentifier(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1416,6 +1473,7 @@ func TestAccAWSAutoScalingGroup_launchTemplate(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1434,7 +1492,6 @@ func TestAccAWSAutoScalingGroup_launchTemplate(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1450,6 +1507,7 @@ func TestAccAWSAutoScalingGroup_launchTemplate_update(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1468,7 +1526,6 @@ func TestAccAWSAutoScalingGroup_launchTemplate_update(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1529,6 +1586,7 @@ func TestAccAWSAutoScalingGroup_LaunchTemplate_IAMInstanceProfile(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1545,7 +1603,6 @@ func TestAccAWSAutoScalingGroup_LaunchTemplate_IAMInstanceProfile(t *testing.T) ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1564,6 +1621,7 @@ func TestAccAWSAutoScalingGroup_LoadBalancers(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1581,7 +1639,6 @@ func TestAccAWSAutoScalingGroup_LoadBalancers(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1613,6 +1670,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1638,7 +1696,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1656,6 +1713,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_CapacityRebalance(t *testin resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1682,7 +1740,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_CapacityRebalance(t *testin ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1700,6 +1757,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1719,7 +1777,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1737,6 +1794,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1756,7 +1814,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1793,6 +1850,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_Updat resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1812,7 +1870,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_Updat ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1835,7 +1892,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_Updat ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1853,6 +1909,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1872,7 +1929,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDem ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1899,6 +1955,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotA resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1918,7 +1975,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotA ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1936,6 +1992,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotI resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -1955,7 +2012,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotI ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -1982,6 +2038,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotM resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2001,7 +2058,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotM ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2037,6 +2093,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTempla resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2057,7 +2114,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTempla ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2075,6 +2131,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTempla resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2095,7 +2152,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTempla ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2123,6 +2179,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_Ins resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2144,7 +2201,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_Ins ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2166,6 +2222,49 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_Ins }) } +func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_InstanceType_With_LaunchTemplateSpecification(t *testing.T) { + var group autoscaling.Group + resourceName := "aws_autoscaling_group.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_LaunchTemplate_Override_InstanceType_With_LaunchTemplateSpecification(rName, rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAutoScalingGroupExists(resourceName, &group), + resource.TestCheckResourceAttr(resourceName, "mixed_instances_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "mixed_instances_policy.0.launch_template.#", "1"), + resource.TestCheckResourceAttr(resourceName, "mixed_instances_policy.0.launch_template.0.override.#", "2"), + resource.TestCheckResourceAttr(resourceName, "mixed_instances_policy.0.launch_template.0.override.0.instance_type", "t2.micro"), + resource.TestCheckNoResourceAttr(resourceName, "mixed_instances_policy.0.launch_template.0.override.0.launch_template_specification.#"), + resource.TestCheckResourceAttr(resourceName, "mixed_instances_policy.0.launch_template.0.override.1.instance_type", "t4g.micro"), + resource.TestCheckResourceAttrPair(resourceName, "mixed_instances_policy.0.launch_template.0.override.1.launch_template_specification.0.launch_template_id", "aws_launch_template.testarm", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_delete", + "initial_lifecycle_hook", + "name_prefix", + "tag", + "tags", + "wait_for_capacity_timeout", + "wait_for_elb_capacity", + }, + }, + }, + }) +} + func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_WeightedCapacity(t *testing.T) { var group autoscaling.Group resourceName := "aws_autoscaling_group.test" @@ -2173,6 +2272,7 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_Wei resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2196,7 +2296,6 @@ func TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_Wei ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2214,6 +2313,7 @@ func TestAccAWSAutoScalingGroup_launchTempPartitionNum(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoScalingGroupDestroy, Steps: []resource.TestStep{ @@ -2230,7 +2330,6 @@ func TestAccAWSAutoScalingGroup_launchTempPartitionNum(t *testing.T) { ImportStateVerifyIgnore: []string{ "force_delete", "initial_lifecycle_hook", - "name_prefix", "tag", "tags", "wait_for_capacity_timeout", @@ -2241,60 +2340,43 @@ func TestAccAWSAutoScalingGroup_launchTempPartitionNum(t *testing.T) { }) } -func testAccAWSAutoScalingGroupConfig_autoGeneratedName() string { - return composeConfig(testAccAvailableAZsNoOptInDefaultExcludeConfig(), ` -data "aws_ami" "test_ami" { - most_recent = true - owners = ["amazon"] - - filter { - name = "name" - values = ["amzn-ami-hvm-*-x86_64-gp2"] - } -} - -resource "aws_launch_configuration" "foobar" { - image_id = data.aws_ami.test_ami.id +func testAccAWSAutoScalingGroupConfigNameGenerated() string { + return composeConfig( + testAccAvailableAZsNoOptInDefaultExcludeConfig(), + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + ` +resource "aws_launch_configuration" "test" { + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id instance_type = "t2.micro" } -resource "aws_autoscaling_group" "bar" { +resource "aws_autoscaling_group" "test" { availability_zones = [data.aws_availability_zones.available.names[0]] - desired_capacity = 0 max_size = 0 min_size = 0 - launch_configuration = aws_launch_configuration.foobar.name + launch_configuration = aws_launch_configuration.test.name } `) } -func testAccAWSAutoScalingGroupConfig_namePrefix() string { - return composeConfig(testAccAvailableAZsNoOptInDefaultExcludeConfig(), - ` -data "aws_ami" "test_ami" { - most_recent = true - owners = ["amazon"] - - filter { - name = "name" - values = ["amzn-ami-hvm-*-x86_64-gp2"] - } -} - +func testAccAWSAutoScalingGroupConfigNamePrefix(namePrefix string) string { + return composeConfig( + testAccAvailableAZsNoOptInDefaultExcludeConfig(), + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + fmt.Sprintf(` resource "aws_launch_configuration" "test" { - image_id = data.aws_ami.test_ami.id + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id instance_type = "t2.micro" } resource "aws_autoscaling_group" "test" { availability_zones = [data.aws_availability_zones.available.names[0]] - desired_capacity = 0 max_size = 0 min_size = 0 - name_prefix = "tf-test-" + name_prefix = %[1]q launch_configuration = aws_launch_configuration.test.name } -`) +`, namePrefix)) } func testAccAWSAutoScalingGroupConfig_terminationPoliciesEmpty() string { @@ -3938,6 +4020,26 @@ resource "aws_launch_template" "test" { `, rName) } +func testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_Arm_Base(rName string) string { + return fmt.Sprintf(` +data "aws_ami" "testarm" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-arm64-gp2"] + } +} + +resource "aws_launch_template" "testarm" { + image_id = data.aws_ami.testarm.id + instance_type = "t4g.micro" + name = %q +} +`, rName) +} + func testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy(rName string) string { return testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_Base(rName) + fmt.Sprintf(` @@ -4276,6 +4378,39 @@ resource "aws_autoscaling_group" "test" { `, rName, instanceType) } +func testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_LaunchTemplate_Override_InstanceType_With_LaunchTemplateSpecification(rName, rName2 string) string { + return testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_Base(rName) + + testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_Arm_Base(rName2) + + fmt.Sprintf(` + +resource "aws_autoscaling_group" "test" { + availability_zones = [data.aws_availability_zones.available.names[0]] + desired_capacity = 0 + max_size = 0 + min_size = 0 + name = %q + + mixed_instances_policy { + launch_template { + launch_template_specification { + launch_template_id = aws_launch_template.test.id + } + + override { + instance_type = "t2.micro" + } + override { + instance_type = "t4g.micro" + launch_template_specification { + launch_template_id = aws_launch_template.testarm.id + } + } + } + } +} +`, rName) +} + func testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_LaunchTemplate_Override_WeightedCapacity(rName string) string { return testAccAWSAutoScalingGroupConfig_MixedInstancesPolicy_Base(rName) + fmt.Sprintf(` @@ -4561,6 +4696,78 @@ resource "aws_launch_configuration" "test" { ` } +func testAccAwsAutoScalingGroupConfig_WarmPool_Base() string { + return ` +data "aws_ami" "test" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn-ami-hvm-*-x86_64-gp2"] + } +} + +data "aws_availability_zones" "current" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_launch_configuration" "test" { + image_id = data.aws_ami.test.id + instance_type = "t3.nano" +} +` +} + +func testAccAwsAutoScalingGroupConfig_WarmPool_Empty() string { + return testAccAwsAutoScalingGroupConfig_WarmPool_Base() + ` +resource "aws_autoscaling_group" "test" { + availability_zones = [data.aws_availability_zones.current.names[0]] + max_size = 5 + min_size = 1 + desired_capacity = 1 + launch_configuration = aws_launch_configuration.test.name + + warm_pool {} +} +` +} + +func testAccAwsAutoScalingGroupConfig_WarmPool_Full() string { + return testAccAwsAutoScalingGroupConfig_WarmPool_Base() + ` +resource "aws_autoscaling_group" "test" { + availability_zones = [data.aws_availability_zones.current.names[0]] + max_size = 5 + min_size = 1 + desired_capacity = 1 + launch_configuration = aws_launch_configuration.test.name + + warm_pool { + pool_state = "Stopped" + min_size = 0 + max_group_prepared_capacity = 2 + } +} +` +} + +func testAccAwsAutoScalingGroupConfig_WarmPool_Remove() string { + return testAccAwsAutoScalingGroupConfig_WarmPool_Base() + ` +resource "aws_autoscaling_group" "test" { + availability_zones = [data.aws_availability_zones.current.names[0]] + max_size = 5 + min_size = 1 + desired_capacity = 1 + launch_configuration = aws_launch_configuration.test.name +} +` +} + func testAccCheckAutoScalingInstanceRefreshCount(group *autoscaling.Group, expected int) resource.TestCheckFunc { return func(state *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).autoscalingconn @@ -4744,3 +4951,143 @@ func TestCreateAutoScalingGroupInstanceRefreshInput(t *testing.T) { }) } } + +func TestPutWarmPoolInput(t *testing.T) { + const asgName = "test-asg" + testCases := []struct { + name string + input []interface{} + expected *autoscaling.PutWarmPoolInput + }{ + { + name: "empty interface", + input: []interface{}{}, + expected: nil, + }, + { + name: "nil", + input: nil, + expected: nil, + }, + { + name: "only pool state", + input: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + }}, + expected: &autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + PoolState: aws.String("Stopped"), + }, + }, + { + name: "0 min size", + input: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "min_size": 0, + "max_group_prepared_capacity": 5, + }}, + expected: &autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + PoolState: aws.String("Stopped"), + MinSize: aws.Int64(0), + MaxGroupPreparedCapacity: aws.Int64(5), + }, + }, + { + name: "-1 max prepared size", + input: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "max_group_prepared_capacity": -1, + }}, + expected: &autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + PoolState: aws.String("Stopped"), + MaxGroupPreparedCapacity: aws.Int64(-1), + }, + }, + { + name: "all values", + input: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "min_size": 3, + "max_group_prepared_capacity": 2, + }}, + expected: &autoscaling.PutWarmPoolInput{ + AutoScalingGroupName: aws.String(asgName), + PoolState: aws.String("Stopped"), + MinSize: aws.Int64(3), + MaxGroupPreparedCapacity: aws.Int64(2), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := createPutWarmPoolInput(asgName, testCase.input) + + if !reflect.DeepEqual(got, testCase.expected) { + t.Errorf("got %s, expected %s", got, testCase.expected) + } + }) + } +} + +func TestFlattenWarmPoolConfiguration(t *testing.T) { + testCases := []struct { + name string + input *autoscaling.WarmPoolConfiguration + expected []interface{} + }{ + { + name: "empty interface", + input: nil, + expected: []interface{}{}, + }, + { + name: "only pool state", + input: &autoscaling.WarmPoolConfiguration{ + PoolState: aws.String("Stopped"), + }, + expected: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "min_size": int64(0), + "max_group_prepared_capacity": int64(-1), + }}, + }, + { + name: "only max group prepared capacity", + input: &autoscaling.WarmPoolConfiguration{ + PoolState: aws.String("Stopped"), + MaxGroupPreparedCapacity: aws.Int64(2), + }, + expected: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "min_size": int64(0), + "max_group_prepared_capacity": int64(2), + }}, + }, + { + name: "all values", + input: &autoscaling.WarmPoolConfiguration{ + PoolState: aws.String("Stopped"), + MinSize: aws.Int64(3), + MaxGroupPreparedCapacity: aws.Int64(5), + }, + expected: []interface{}{map[string]interface{}{ + "pool_state": "Stopped", + "min_size": int64(3), + "max_group_prepared_capacity": int64(5), + }}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := flattenWarmPoolConfiguration(testCase.input) + + if !reflect.DeepEqual(got, testCase.expected) { + t.Errorf("got %s, expected %s", got, testCase.expected) + } + }) + } +} diff --git a/aws/resource_aws_autoscaling_lifecycle_hook.go b/aws/resource_aws_autoscaling_lifecycle_hook.go index 1e9ac0cf2a29..9249ef9b5b4d 100644 --- a/aws/resource_aws_autoscaling_lifecycle_hook.go +++ b/aws/resource_aws_autoscaling_lifecycle_hook.go @@ -195,7 +195,11 @@ func getAwsAutoscalingLifecycleHook(d *schema.ResourceData, meta interface{}) (* // find lifecycle hooks name := d.Get("name") for idx, sp := range resp.LifecycleHooks { - if *sp.LifecycleHookName == name { + if sp == nil { + continue + } + + if aws.StringValue(sp.LifecycleHookName) == name { return resp.LifecycleHooks[idx], nil } } diff --git a/aws/resource_aws_autoscaling_lifecycle_hook_test.go b/aws/resource_aws_autoscaling_lifecycle_hook_test.go index bbd99ceb5423..384167f7c220 100644 --- a/aws/resource_aws_autoscaling_lifecycle_hook_test.go +++ b/aws/resource_aws_autoscaling_lifecycle_hook_test.go @@ -16,6 +16,7 @@ func TestAccAWSAutoscalingLifecycleHook_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingLifecycleHookDestroy, Steps: []resource.TestStep{ @@ -44,6 +45,7 @@ func TestAccAWSAutoscalingLifecycleHook_omitDefaultResult(t *testing.T) { rInt := acctest.RandInt() resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingLifecycleHookDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_autoscaling_notification.go b/aws/resource_aws_autoscaling_notification.go index 2868fb2ce510..5e706df6ec24 100644 --- a/aws/resource_aws_autoscaling_notification.go +++ b/aws/resource_aws_autoscaling_notification.go @@ -83,9 +83,13 @@ func resourceAwsAutoscalingNotificationRead(d *schema.ResourceData, meta interfa } for _, n := range resp.NotificationConfigurations { - if *n.TopicARN == topic { - gRaw[*n.AutoScalingGroupName] = true - nRaw[*n.NotificationType] = true + if n == nil { + continue + } + + if aws.StringValue(n.TopicARN) == topic { + gRaw[aws.StringValue(n.AutoScalingGroupName)] = true + nRaw[aws.StringValue(n.NotificationType)] = true } } return true // return false to stop paging diff --git a/aws/resource_aws_autoscaling_notification_test.go b/aws/resource_aws_autoscaling_notification_test.go index d1a462a0d108..cc716434969f 100644 --- a/aws/resource_aws_autoscaling_notification_test.go +++ b/aws/resource_aws_autoscaling_notification_test.go @@ -19,6 +19,7 @@ func TestAccAWSASGNotification_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckASGNDestroy, Steps: []resource.TestStep{ @@ -40,6 +41,7 @@ func TestAccAWSASGNotification_update(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckASGNDestroy, Steps: []resource.TestStep{ @@ -69,6 +71,7 @@ func TestAccAWSASGNotification_Pagination(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckASGNDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_autoscaling_policy.go b/aws/resource_aws_autoscaling_policy.go index b1fbcc3b1b7d..443ccecb2082 100644 --- a/aws/resource_aws_autoscaling_policy.go +++ b/aws/resource_aws_autoscaling_policy.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log" + "strconv" "strings" "github.com/aws/aws-sdk-go/aws" @@ -11,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" ) @@ -43,16 +45,6 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { Required: true, ForceNew: true, }, - "policy_type": { - Type: schema.TypeString, - Optional: true, - Default: "SimpleScaling", // preserve AWS's default to make validation easier. - ValidateFunc: validation.StringInSlice([]string{ - "SimpleScaling", - "StepScaling", - "TargetTrackingScaling", - }, false), - }, "cooldown": { Type: schema.TypeInt, Optional: true, @@ -71,6 +63,136 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { Optional: true, ValidateFunc: validation.IntAtLeast(1), }, + "policy_type": { + Type: schema.TypeString, + Optional: true, + Default: "SimpleScaling", // preserve AWS's default to make validation easier. + ValidateFunc: validation.StringInSlice([]string{ + "SimpleScaling", + "StepScaling", + "TargetTrackingScaling", + "PredictiveScaling", + }, false), + }, + "predictive_scaling_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_specification": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_metric_pair_specification": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_metric_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "ASGCPUUtilization", + "ASGNetworkIn", + "ASGNetworkOut", + "ALBRequestCount", + }, false), + }, + "resource_label": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "predefined_scaling_metric_specification": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_metric_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "ASGAverageCPUUtilization", + "ASGAverageNetworkIn", + "ASGAverageNetworkOut", + "ALBRequestCountPerTarget", + }, false), + }, + "resource_label": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "predefined_load_metric_specification": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "predefined_metric_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "ASGTotalCPUUtilization", + "ASGTotalNetworkIn", + "ASGTotalNetworkOut", + "ALBTargetGroupRequestCount", + }, false), + }, + "resource_label": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "target_value": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "max_capacity_breach_behavior": { + Type: schema.TypeString, + Optional: true, + Default: "HonorMaxCapacity", + ValidateFunc: validation.StringInSlice([]string{ + "HonorMaxCapacity", + "IncreaseMaxCapacity", + }, false), + }, + "max_capacity_buffer": { + Type: nullable.TypeNullableInt, + Optional: true, + ValidateFunc: nullable.ValidateTypeStringNullableIntBetween(0, 100), + }, + "mode": { + Type: schema.TypeString, + Optional: true, + Default: "ForecastOnly", + ValidateFunc: validation.StringInSlice([]string{ + "ForecastOnly", + "ForecastAndScale", + }, false), + }, + "scheduling_buffer_time": { + Type: nullable.TypeNullableInt, + Optional: true, + ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0), + }, + }, + }, + }, "scaling_adjustment": { Type: schema.TypeInt, Optional: true, @@ -226,6 +348,9 @@ func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) d.Set("arn", p.PolicyARN) d.Set("name", p.PolicyName) d.Set("scaling_adjustment", p.ScalingAdjustment) + if err := d.Set("predictive_scaling_configuration", flattenPredictiveScalingConfig(p.PredictiveScalingConfiguration)); err != nil { + return fmt.Errorf("error setting predictive_scaling_configuration: %s", err) + } if err := d.Set("step_adjustment", flattenStepAdjustments(p.StepAdjustments)); err != nil { return fmt.Errorf("error setting step_adjustment: %s", err) } @@ -309,6 +434,10 @@ func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) (autoscaling params.AdjustmentType = aws.String(v.(string)) } + if predictiveScalingConfigFlat := d.Get("predictive_scaling_configuration").([]interface{}); len(predictiveScalingConfigFlat) > 0 { + params.PredictiveScalingConfiguration = expandPredictiveScalingConfig(predictiveScalingConfigFlat) + } + // This parameter is supported if the policy type is SimpleScaling. if v, ok := d.GetOkExists("cooldown"); ok { // 0 is allowed as placeholder even if policyType is not supported @@ -404,7 +533,11 @@ func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autosca // find scaling policy name := d.Get("name") for idx, sp := range resp.ScalingPolicies { - if *sp.PolicyName == name { + if sp == nil { + continue + } + + if aws.StringValue(sp.PolicyName) == name { return resp.ScalingPolicies[idx], nil } } @@ -478,37 +611,106 @@ func expandTargetTrackingConfiguration(configs []interface{}) *autoscaling.Targe return result } +func expandPredictiveScalingConfig(predictiveScalingConfigSlice []interface{}) *autoscaling.PredictiveScalingConfiguration { + if predictiveScalingConfigSlice == nil || len(predictiveScalingConfigSlice) < 1 { + return nil + } + predictiveScalingConfigFlat := predictiveScalingConfigSlice[0].(map[string]interface{}) + predictiveScalingConfig := &autoscaling.PredictiveScalingConfiguration{ + MetricSpecifications: expandPredictiveScalingMetricSpecifications(predictiveScalingConfigFlat["metric_specification"].([]interface{})), + MaxCapacityBreachBehavior: aws.String(predictiveScalingConfigFlat["max_capacity_breach_behavior"].(string)), + Mode: aws.String(predictiveScalingConfigFlat["mode"].(string)), + } + if v, null, _ := nullable.Int(predictiveScalingConfigFlat["max_capacity_buffer"].(string)).Value(); !null { + predictiveScalingConfig.MaxCapacityBuffer = aws.Int64(v) + } + if v, null, _ := nullable.Int(predictiveScalingConfigFlat["scheduling_buffer_time"].(string)).Value(); !null { + predictiveScalingConfig.SchedulingBufferTime = aws.Int64(v) + } + return predictiveScalingConfig +} + +func expandPredictiveScalingMetricSpecifications(metricSpecificationsSlice []interface{}) []*autoscaling.PredictiveScalingMetricSpecification { + if metricSpecificationsSlice == nil || len(metricSpecificationsSlice) < 1 { + return nil + } + metricSpecificationsFlat := metricSpecificationsSlice[0].(map[string]interface{}) + metricSpecification := &autoscaling.PredictiveScalingMetricSpecification{ + PredefinedLoadMetricSpecification: expandPredefinedLoadMetricSpecification(metricSpecificationsFlat["predefined_load_metric_specification"].([]interface{})), + PredefinedMetricPairSpecification: expandPredefinedMetricPairSpecification(metricSpecificationsFlat["predefined_metric_pair_specification"].([]interface{})), + PredefinedScalingMetricSpecification: expandPredefinedScalingMetricSpecification(metricSpecificationsFlat["predefined_scaling_metric_specification"].([]interface{})), + TargetValue: aws.Float64(float64(metricSpecificationsFlat["target_value"].(int))), + } + return []*autoscaling.PredictiveScalingMetricSpecification{metricSpecification} +} + +func expandPredefinedLoadMetricSpecification(predefinedLoadMetricSpecificationSlice []interface{}) *autoscaling.PredictiveScalingPredefinedLoadMetric { + if predefinedLoadMetricSpecificationSlice == nil || len(predefinedLoadMetricSpecificationSlice) < 1 { + return nil + } + predefinedLoadMetricSpecificationFlat := predefinedLoadMetricSpecificationSlice[0].(map[string]interface{}) + predefinedLoadMetricSpecification := &autoscaling.PredictiveScalingPredefinedLoadMetric{ + PredefinedMetricType: aws.String(predefinedLoadMetricSpecificationFlat["predefined_metric_type"].(string)), + ResourceLabel: aws.String(predefinedLoadMetricSpecificationFlat["resource_label"].(string)), + } + return predefinedLoadMetricSpecification +} + +func expandPredefinedMetricPairSpecification(predefinedMetricPairSpecificationSlice []interface{}) *autoscaling.PredictiveScalingPredefinedMetricPair { + if predefinedMetricPairSpecificationSlice == nil || len(predefinedMetricPairSpecificationSlice) < 1 { + return nil + } + predefinedMetricPairSpecificationFlat := predefinedMetricPairSpecificationSlice[0].(map[string]interface{}) + predefinedMetricPairSpecification := &autoscaling.PredictiveScalingPredefinedMetricPair{ + PredefinedMetricType: aws.String(predefinedMetricPairSpecificationFlat["predefined_metric_type"].(string)), + ResourceLabel: aws.String(predefinedMetricPairSpecificationFlat["resource_label"].(string)), + } + return predefinedMetricPairSpecification +} + +func expandPredefinedScalingMetricSpecification(predefinedScalingMetricSpecificationSlice []interface{}) *autoscaling.PredictiveScalingPredefinedScalingMetric { + if predefinedScalingMetricSpecificationSlice == nil || len(predefinedScalingMetricSpecificationSlice) < 1 { + return nil + } + predefinedScalingMetricSpecificationFlat := predefinedScalingMetricSpecificationSlice[0].(map[string]interface{}) + predefinedScalingMetricSpecification := &autoscaling.PredictiveScalingPredefinedScalingMetric{ + PredefinedMetricType: aws.String(predefinedScalingMetricSpecificationFlat["predefined_metric_type"].(string)), + ResourceLabel: aws.String(predefinedScalingMetricSpecificationFlat["resource_label"].(string)), + } + return predefinedScalingMetricSpecification +} + func flattenTargetTrackingConfiguration(config *autoscaling.TargetTrackingConfiguration) []interface{} { if config == nil { return []interface{}{} } result := map[string]interface{}{} - result["disable_scale_in"] = *config.DisableScaleIn - result["target_value"] = *config.TargetValue + result["disable_scale_in"] = aws.BoolValue(config.DisableScaleIn) + result["target_value"] = aws.Float64Value(config.TargetValue) if config.PredefinedMetricSpecification != nil { spec := map[string]interface{}{} - spec["predefined_metric_type"] = *config.PredefinedMetricSpecification.PredefinedMetricType + spec["predefined_metric_type"] = aws.StringValue(config.PredefinedMetricSpecification.PredefinedMetricType) if config.PredefinedMetricSpecification.ResourceLabel != nil { - spec["resource_label"] = *config.PredefinedMetricSpecification.ResourceLabel + spec["resource_label"] = aws.StringValue(config.PredefinedMetricSpecification.ResourceLabel) } result["predefined_metric_specification"] = []map[string]interface{}{spec} } if config.CustomizedMetricSpecification != nil { spec := map[string]interface{}{} - spec["metric_name"] = *config.CustomizedMetricSpecification.MetricName - spec["namespace"] = *config.CustomizedMetricSpecification.Namespace - spec["statistic"] = *config.CustomizedMetricSpecification.Statistic + spec["metric_name"] = aws.StringValue(config.CustomizedMetricSpecification.MetricName) + spec["namespace"] = aws.StringValue(config.CustomizedMetricSpecification.Namespace) + spec["statistic"] = aws.StringValue(config.CustomizedMetricSpecification.Statistic) if config.CustomizedMetricSpecification.Unit != nil { - spec["unit"] = *config.CustomizedMetricSpecification.Unit + spec["unit"] = aws.StringValue(config.CustomizedMetricSpecification.Unit) } if config.CustomizedMetricSpecification.Dimensions != nil { dimSpec := make([]interface{}, len(config.CustomizedMetricSpecification.Dimensions)) for i := range dimSpec { dim := map[string]interface{}{} rawDim := config.CustomizedMetricSpecification.Dimensions[i] - dim["name"] = *rawDim.Name - dim["value"] = *rawDim.Value + dim["name"] = aws.StringValue(rawDim.Name) + dim["value"] = aws.StringValue(rawDim.Value) dimSpec[i] = dim } spec["metric_dimension"] = dimSpec @@ -517,3 +719,76 @@ func flattenTargetTrackingConfiguration(config *autoscaling.TargetTrackingConfig } return []interface{}{result} } + +func flattenPredictiveScalingConfig(predictiveScalingConfig *autoscaling.PredictiveScalingConfiguration) []map[string]interface{} { + predictiveScalingConfigFlat := map[string]interface{}{} + if predictiveScalingConfig == nil { + return nil + } + if predictiveScalingConfig.MetricSpecifications != nil && len(predictiveScalingConfig.MetricSpecifications) > 0 { + predictiveScalingConfigFlat["metric_specification"] = flattenPredictiveScalingMetricSpecifications(predictiveScalingConfig.MetricSpecifications) + } + if predictiveScalingConfig.Mode != nil { + predictiveScalingConfigFlat["mode"] = aws.StringValue(predictiveScalingConfig.Mode) + } + if predictiveScalingConfig.SchedulingBufferTime != nil { + predictiveScalingConfigFlat["scheduling_buffer_time"] = strconv.FormatInt(aws.Int64Value(predictiveScalingConfig.SchedulingBufferTime), 10) + } + if predictiveScalingConfig.MaxCapacityBreachBehavior != nil { + predictiveScalingConfigFlat["max_capacity_breach_behavior"] = aws.StringValue(predictiveScalingConfig.MaxCapacityBreachBehavior) + } + if predictiveScalingConfig.MaxCapacityBuffer != nil { + predictiveScalingConfigFlat["max_capacity_buffer"] = strconv.FormatInt(aws.Int64Value(predictiveScalingConfig.MaxCapacityBuffer), 10) + } + return []map[string]interface{}{predictiveScalingConfigFlat} +} + +func flattenPredictiveScalingMetricSpecifications(metricSpecification []*autoscaling.PredictiveScalingMetricSpecification) []map[string]interface{} { + metricSpecificationFlat := map[string]interface{}{} + if metricSpecification == nil || len(metricSpecification) < 1 { + return []map[string]interface{}{metricSpecificationFlat} + } + if metricSpecification[0].TargetValue != nil { + metricSpecificationFlat["target_value"] = aws.Float64Value(metricSpecification[0].TargetValue) + } + if metricSpecification[0].PredefinedLoadMetricSpecification != nil { + metricSpecificationFlat["predefined_load_metric_specification"] = flattenPredefinedLoadMetricSpecification(metricSpecification[0].PredefinedLoadMetricSpecification) + } + if metricSpecification[0].PredefinedMetricPairSpecification != nil { + metricSpecificationFlat["predefined_metric_pair_specification"] = flattenPredefinedMetricPairSpecification(metricSpecification[0].PredefinedMetricPairSpecification) + } + if metricSpecification[0].PredefinedScalingMetricSpecification != nil { + metricSpecificationFlat["predefined_scaling_metric_specification"] = flattenPredefinedScalingMetricSpecification(metricSpecification[0].PredefinedScalingMetricSpecification) + } + return []map[string]interface{}{metricSpecificationFlat} +} + +func flattenPredefinedScalingMetricSpecification(predefinedScalingMetricSpecification *autoscaling.PredictiveScalingPredefinedScalingMetric) []map[string]interface{} { + predefinedScalingMetricSpecificationFlat := map[string]interface{}{} + if predefinedScalingMetricSpecification == nil { + return []map[string]interface{}{predefinedScalingMetricSpecificationFlat} + } + predefinedScalingMetricSpecificationFlat["predefined_metric_type"] = aws.StringValue(predefinedScalingMetricSpecification.PredefinedMetricType) + predefinedScalingMetricSpecificationFlat["resource_label"] = aws.StringValue(predefinedScalingMetricSpecification.ResourceLabel) + return []map[string]interface{}{predefinedScalingMetricSpecificationFlat} +} + +func flattenPredefinedLoadMetricSpecification(predefinedLoadMetricSpecification *autoscaling.PredictiveScalingPredefinedLoadMetric) []map[string]interface{} { + predefinedLoadMetricSpecificationFlat := map[string]interface{}{} + if predefinedLoadMetricSpecification == nil { + return []map[string]interface{}{predefinedLoadMetricSpecificationFlat} + } + predefinedLoadMetricSpecificationFlat["predefined_metric_type"] = aws.StringValue(predefinedLoadMetricSpecification.PredefinedMetricType) + predefinedLoadMetricSpecificationFlat["resource_label"] = aws.StringValue(predefinedLoadMetricSpecification.ResourceLabel) + return []map[string]interface{}{predefinedLoadMetricSpecificationFlat} +} + +func flattenPredefinedMetricPairSpecification(predefinedMetricPairSpecification *autoscaling.PredictiveScalingPredefinedMetricPair) []map[string]interface{} { + predefinedMetricPairSpecificationFlat := map[string]interface{}{} + if predefinedMetricPairSpecification == nil { + return []map[string]interface{}{predefinedMetricPairSpecificationFlat} + } + predefinedMetricPairSpecificationFlat["predefined_metric_type"] = aws.StringValue(predefinedMetricPairSpecification.PredefinedMetricType) + predefinedMetricPairSpecificationFlat["resource_label"] = aws.StringValue(predefinedMetricPairSpecification.ResourceLabel) + return []map[string]interface{}{predefinedMetricPairSpecificationFlat} +} diff --git a/aws/resource_aws_autoscaling_policy_test.go b/aws/resource_aws_autoscaling_policy_test.go index 306b26b3b1af..61294e13d46c 100644 --- a/aws/resource_aws_autoscaling_policy_test.go +++ b/aws/resource_aws_autoscaling_policy_test.go @@ -24,6 +24,7 @@ func TestAccAWSAutoscalingPolicy_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -102,6 +103,134 @@ func TestAccAWSAutoscalingPolicy_basic(t *testing.T) { }) } +func TestAccAWSAutoscalingPolicy_predictiveScaling(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resourceSimpleName := "aws_autoscaling_policy.test" + + name := acctest.RandomWithPrefix("terraform-testacc-asp") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAutoscalingPolicyConfig_predictiveScaling(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(resourceSimpleName, &policy), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.mode", "ForecastAndScale"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.scheduling_buffer_time", "10"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_breach_behavior", "IncreaseMaxCapacity"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_buffer", "0"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.target_value", "32"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.predefined_metric_type", "ASGAverageCPUUtilization"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.resource_label", "testLabel"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.predefined_metric_type", "ASGTotalCPUUtilization"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.resource_label", "testLabel"), + ), + }, + { + ResourceName: resourceSimpleName, + ImportState: true, + ImportStateIdFunc: testAccAWSAutoscalingPolicyImportStateIdFunc(resourceSimpleName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingPolicy_predictiveScalingRemoved(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resourceSimpleName := "aws_autoscaling_policy.test" + + name := acctest.RandomWithPrefix("terraform-testacc-asp") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAutoscalingPolicyConfig_predictiveScaling(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(resourceSimpleName, &policy), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.#", "1"), + ), + }, + { + Config: testAccAWSAutoscalingPolicyConfig_predictiveScalingRemoved(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(resourceSimpleName, &policy), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.#", "0"), + ), + }, + { + ResourceName: resourceSimpleName, + ImportState: true, + ImportStateIdFunc: testAccAWSAutoscalingPolicyImportStateIdFunc(resourceSimpleName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSAutoscalingPolicy_predictiveScalingUpdated(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resourceSimpleName := "aws_autoscaling_policy.test" + + name := acctest.RandomWithPrefix("terraform-testacc-asp") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAutoscalingPolicyConfig_predictiveScaling(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(resourceSimpleName, &policy), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.mode", "ForecastAndScale"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.scheduling_buffer_time", "10"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_breach_behavior", "IncreaseMaxCapacity"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_buffer", "0"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.target_value", "32"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.predefined_metric_type", "ASGAverageCPUUtilization"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.resource_label", "testLabel"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.predefined_metric_type", "ASGTotalCPUUtilization"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.resource_label", "testLabel"), + ), + }, + { + Config: testaccawsautoscalingpolicyconfigPredictivescalingUpdated(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists(resourceSimpleName, &policy), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.mode", "ForecastOnly"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.scheduling_buffer_time", ""), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_buffer", ""), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.max_capacity_breach_behavior", "HonorMaxCapacity"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.target_value", "32"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.predefined_metric_type", "ASGAverageNetworkIn"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_scaling_metric_specification.0.resource_label", "testLabel"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.predefined_metric_type", "ASGTotalNetworkIn"), + resource.TestCheckResourceAttr(resourceSimpleName, "predictive_scaling_configuration.0.metric_specification.0.predefined_load_metric_specification.0.resource_label", "testLabel"), + ), + }, + { + ResourceName: resourceSimpleName, + ImportState: true, + ImportStateIdFunc: testAccAWSAutoscalingPolicyImportStateIdFunc(resourceSimpleName), + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAWSAutoscalingPolicy_disappears(t *testing.T) { var policy autoscaling.ScalingPolicy @@ -111,6 +240,7 @@ func TestAccAWSAutoscalingPolicy_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -172,6 +302,7 @@ func TestAccAWSAutoscalingPolicy_SimpleScalingStepAdjustment(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -200,6 +331,7 @@ func TestAccAWSAutoscalingPolicy_TargetTrack_Predefined(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -226,6 +358,7 @@ func TestAccAWSAutoscalingPolicy_TargetTrack_Custom(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -254,6 +387,7 @@ func TestAccAWSAutoscalingPolicy_zerovalue(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, Steps: []resource.TestStep{ @@ -386,7 +520,7 @@ resource "aws_autoscaling_group" "test" { } func testAccAWSAutoscalingPolicyConfig_basic(name string) string { - return testAccAWSAutoscalingPolicyConfig_base(name) + fmt.Sprintf(` + return composeConfig(testAccAWSAutoscalingPolicyConfig_base(name), fmt.Sprintf(` resource "aws_autoscaling_policy" "foobar_simple" { name = "%s-foobar_simple" adjustment_type = "ChangeInCapacity" @@ -424,7 +558,72 @@ resource "aws_autoscaling_policy" "foobar_target_tracking" { target_value = 40.0 } } -`, name, name, name) +`, name, name, name)) +} + +func testAccAWSAutoscalingPolicyConfig_predictiveScaling(name string) string { + return composeConfig(testAccAWSAutoscalingPolicyConfig_base(name), fmt.Sprintf(` +resource "aws_autoscaling_policy" "test" { + name = "%[1]s-policy_predictive" + policy_type = "PredictiveScaling" + autoscaling_group_name = aws_autoscaling_group.test.name + predictive_scaling_configuration { + metric_specification { + target_value = 32 + predefined_scaling_metric_specification { + predefined_metric_type = "ASGAverageCPUUtilization" + resource_label = "testLabel" + } + predefined_load_metric_specification { + predefined_metric_type = "ASGTotalCPUUtilization" + resource_label = "testLabel" + } + } + mode = "ForecastAndScale" + scheduling_buffer_time = 10 + max_capacity_breach_behavior = "IncreaseMaxCapacity" + max_capacity_buffer = 0 + } +} +`, name)) +} + +func testAccAWSAutoscalingPolicyConfig_predictiveScalingRemoved(name string) string { + return composeConfig(testAccAWSAutoscalingPolicyConfig_base(name), fmt.Sprintf(` +resource "aws_autoscaling_policy" "test" { + name = "%[1]s-foobar_simple" + adjustment_type = "ChangeInCapacity" + cooldown = 300 + policy_type = "SimpleScaling" + scaling_adjustment = 2 + autoscaling_group_name = aws_autoscaling_group.test.name +} +`, name)) +} + +func testaccawsautoscalingpolicyconfigPredictivescalingUpdated(name string) string { + return testAccAWSAutoscalingPolicyConfig_base(name) + fmt.Sprintf(` +resource "aws_autoscaling_policy" "test" { + name = "%[1]s-policy_predictive" + policy_type = "PredictiveScaling" + autoscaling_group_name = aws_autoscaling_group.test.name + predictive_scaling_configuration { + metric_specification { + target_value = 32 + predefined_scaling_metric_specification { + predefined_metric_type = "ASGAverageNetworkIn" + resource_label = "testLabel" + } + predefined_load_metric_specification { + predefined_metric_type = "ASGTotalNetworkIn" + resource_label = "testLabel" + } + } + mode = "ForecastOnly" + max_capacity_breach_behavior = "HonorMaxCapacity" + } +} +`, name) } func testAccAWSAutoscalingPolicyConfig_basicUpdate(name string) string { diff --git a/aws/resource_aws_autoscaling_schedule.go b/aws/resource_aws_autoscaling_schedule.go index d57fb39a02dd..fcfc6a03636a 100644 --- a/aws/resource_aws_autoscaling_schedule.go +++ b/aws/resource_aws_autoscaling_schedule.go @@ -51,6 +51,11 @@ func resourceAwsAutoscalingSchedule() *schema.Resource { Computed: true, ValidateFunc: validateASGScheduleTimestamp, }, + "time_zone": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "recurrence": { Type: schema.TypeString, Optional: true, @@ -119,6 +124,10 @@ func resourceAwsAutoscalingScheduleCreate(d *schema.ResourceData, meta interface params.EndTime = aws.Time(t) } + if attr, ok := d.GetOk("time_zone"); ok { + params.TimeZone = aws.String(attr.(string)) + } + if attr, ok := d.GetOk("recurrence"); ok { params.Recurrence = aws.String(attr.(string)) } @@ -194,6 +203,10 @@ func resourceAwsAutoscalingScheduleRead(d *schema.ResourceData, meta interface{} d.Set("end_time", sa.EndTime.Format(awsAutoscalingScheduleTimeLayout)) } + if sa.TimeZone != nil { + d.Set("time_zone", sa.TimeZone) + } + return nil } @@ -236,7 +249,7 @@ func resourceAwsASGScheduledActionRetrieve(d *schema.ResourceData, meta interfac } if len(actions.ScheduledUpdateGroupActions) != 1 || - *actions.ScheduledUpdateGroupActions[0].ScheduledActionName != d.Id() { + aws.StringValue(actions.ScheduledUpdateGroupActions[0].ScheduledActionName) != d.Id() { return nil, false, nil } diff --git a/aws/resource_aws_autoscaling_schedule_test.go b/aws/resource_aws_autoscaling_schedule_test.go index 998491428b5c..d0172ba519eb 100644 --- a/aws/resource_aws_autoscaling_schedule_test.go +++ b/aws/resource_aws_autoscaling_schedule_test.go @@ -25,6 +25,7 @@ func TestAccAWSAutoscalingSchedule_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingScheduleDestroy, Steps: []resource.TestStep{ @@ -59,6 +60,7 @@ func TestAccAWSAutoscalingSchedule_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingScheduleDestroy, Steps: []resource.TestStep{ @@ -97,6 +99,7 @@ func TestAccAWSAutoscalingSchedule_recurrence(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingScheduleDestroy, Steps: []resource.TestStep{ @@ -130,6 +133,7 @@ func TestAccAWSAutoscalingSchedule_zeroValues(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingScheduleDestroy, Steps: []resource.TestStep{ @@ -162,6 +166,7 @@ func TestAccAWSAutoscalingSchedule_negativeOne(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscaling.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSAutoscalingScheduleDestroy, Steps: []resource.TestStep{ @@ -354,6 +359,7 @@ resource "aws_autoscaling_schedule" "foobar" { max_size = 1 desired_capacity = 0 recurrence = "0 8 * * *" + time_zone = "Pacific/Tahiti" autoscaling_group_name = aws_autoscaling_group.foobar.name } `, r, r) diff --git a/aws/resource_aws_autoscalingplans_scaling_plan.go b/aws/resource_aws_autoscalingplans_scaling_plan.go index df99d0517f26..eb8f1c70fcbc 100644 --- a/aws/resource_aws_autoscalingplans_scaling_plan.go +++ b/aws/resource_aws_autoscalingplans_scaling_plan.go @@ -367,7 +367,7 @@ func resourceAwsAutoScalingPlansScalingPlanRead(d *schema.ResourceData, meta int if err != nil { return fmt.Errorf("error setting application_source: %w", err) } - d.Set("scaling_plan_version", int(aws.Int64Value(scalingPlan.ScalingPlanVersion))) + d.Set("scaling_plan_version", scalingPlan.ScalingPlanVersion) return nil } diff --git a/aws/resource_aws_autoscalingplans_scaling_plan_test.go b/aws/resource_aws_autoscalingplans_scaling_plan_test.go index 2c24fd1ffbd5..c03b02e7b3f1 100644 --- a/aws/resource_aws_autoscalingplans_scaling_plan_test.go +++ b/aws/resource_aws_autoscalingplans_scaling_plan_test.go @@ -78,6 +78,7 @@ func TestAccAwsAutoScalingPlansScalingPlan_basicDynamicScaling(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscalingplans.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, Steps: []resource.TestStep{ @@ -129,6 +130,7 @@ func TestAccAwsAutoScalingPlansScalingPlan_basicPredictiveScaling(t *testing.T) testAccPreCheck(t) testAccPreCheckIamServiceLinkedRole(t, "/aws-service-role/autoscaling-plans") }, + ErrorCheck: testAccErrorCheck(t, autoscalingplans.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, Steps: []resource.TestStep{ @@ -184,6 +186,7 @@ func TestAccAwsAutoScalingPlansScalingPlan_basicUpdate(t *testing.T) { testAccPreCheck(t) testAccPreCheckIamServiceLinkedRole(t, "/aws-service-role/autoscaling-plans") }, + ErrorCheck: testAccErrorCheck(t, autoscalingplans.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, Steps: []resource.TestStep{ @@ -261,6 +264,7 @@ func TestAccAwsAutoScalingPlansScalingPlan_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, autoscalingplans.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAutoScalingPlansScalingPlanDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_backup_global_settings.go b/aws/resource_aws_backup_global_settings.go index 58ffd4e7fbb1..84d6ec45b40d 100644 --- a/aws/resource_aws_backup_global_settings.go +++ b/aws/resource_aws_backup_global_settings.go @@ -32,7 +32,7 @@ func resourceAwsBackupGlobalSettingsUpdate(d *schema.ResourceData, meta interfac conn := meta.(*AWSClient).backupconn input := &backup.UpdateGlobalSettingsInput{ - GlobalSettings: stringMapToPointers(d.Get("global_settings").(map[string]interface{})), + GlobalSettings: expandStringMap(d.Get("global_settings").(map[string]interface{})), } _, err := conn.UpdateGlobalSettings(input) diff --git a/aws/resource_aws_backup_global_settings_test.go b/aws/resource_aws_backup_global_settings_test.go index bca27e840865..ad15741fe5e0 100644 --- a/aws/resource_aws_backup_global_settings_test.go +++ b/aws/resource_aws_backup_global_settings_test.go @@ -18,6 +18,7 @@ func TestAccAwsBackupGlobalSettings_basic(t *testing.T) { testAccPreCheck(t) testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_backup_plan.go b/aws/resource_aws_backup_plan.go index 89f4515e96a9..50cb8f9bc59a 100644 --- a/aws/resource_aws_backup_plan.go +++ b/aws/resource_aws_backup_plan.go @@ -57,6 +57,11 @@ func resourceAwsBackupPlan() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "enable_continuous_backup": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "start_window": { Type: schema.TypeInt, Optional: true, @@ -126,12 +131,12 @@ func resourceAwsBackupPlan() *schema.Resource { Schema: map[string]*schema.Schema{ "backup_options": { Type: schema.TypeMap, - Optional: true, + Required: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "resource_type": { Type: schema.TypeString, - Optional: true, + Required: true, ValidateFunc: validation.StringInSlice([]string{ "EC2", }, false), @@ -147,13 +152,18 @@ func resourceAwsBackupPlan() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsBackupPlanCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &backup.CreateBackupPlanInput{ BackupPlan: &backup.PlanInput{ @@ -161,7 +171,7 @@ func resourceAwsBackupPlanCreate(d *schema.ResourceData, meta interface{}) error Rules: expandBackupPlanRules(d.Get("rule").(*schema.Set)), AdvancedBackupSettings: expandBackupPlanAdvancedBackupSettings(d.Get("advanced_backup_setting").(*schema.Set)), }, - BackupPlanTags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().BackupTags(), + BackupPlanTags: tags.IgnoreAws().BackupTags(), } log.Printf("[DEBUG] Creating Backup Plan: %#v", input) @@ -177,6 +187,7 @@ func resourceAwsBackupPlanCreate(d *schema.ResourceData, meta interface{}) error func resourceAwsBackupPlanRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig resp, err := conn.GetBackupPlan(&backup.GetBackupPlanInput{ @@ -209,10 +220,17 @@ func resourceAwsBackupPlanRead(d *schema.ResourceData, meta interface{}) error { if err != nil { return fmt.Errorf("error listing tags for Backup Plan (%s): %w", d.Id(), err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } @@ -236,8 +254,8 @@ func resourceAwsBackupPlanUpdate(d *schema.ResourceData, meta interface{}) error } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.BackupUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags for Backup Plan (%s): %w", d.Id(), err) } @@ -301,6 +319,9 @@ func expandBackupPlanRules(vRules *schema.Set) []*backup.RuleInput { if vSchedule, ok := mRule["schedule"].(string); ok && vSchedule != "" { rule.ScheduleExpression = aws.String(vSchedule) } + if vEnableContinuousBackup, ok := mRule["enable_continuous_backup"].(bool); ok { + rule.EnableContinuousBackup = aws.Bool(vEnableContinuousBackup) + } if vStartWindow, ok := mRule["start_window"].(int); ok { rule.StartWindowMinutes = aws.Int64(int64(vStartWindow)) } @@ -335,12 +356,18 @@ func expandBackupPlanAdvancedBackupSettings(vAdvancedBackupSettings *schema.Set) mAdvancedBackupSetting := vAdvancedBackupSetting.(map[string]interface{}) if v, ok := mAdvancedBackupSetting["backup_options"].(map[string]interface{}); ok && v != nil { - advancedBackupSetting.BackupOptions = stringMapToPointers(v) + advancedBackupSetting.BackupOptions = expandStringMap(v) } if v, ok := mAdvancedBackupSetting["resource_type"].(string); ok && v != "" { advancedBackupSetting.ResourceType = aws.String(v) } + // https://github.com/hashicorp/terraform-plugin-sdk/issues/588 + // Map in Set may add empty element. Ignore it. + if advancedBackupSetting.ResourceType == nil { + continue + } + advancedBackupSettings = append(advancedBackupSettings, advancedBackupSetting) } @@ -387,12 +414,13 @@ func flattenBackupPlanRules(rules []*backup.Rule) *schema.Set { for _, rule := range rules { mRule := map[string]interface{}{ - "rule_name": aws.StringValue(rule.RuleName), - "target_vault_name": aws.StringValue(rule.TargetBackupVaultName), - "schedule": aws.StringValue(rule.ScheduleExpression), - "start_window": int(aws.Int64Value(rule.StartWindowMinutes)), - "completion_window": int(aws.Int64Value(rule.CompletionWindowMinutes)), - "recovery_point_tags": keyvaluetags.BackupKeyValueTags(rule.RecoveryPointTags).IgnoreAws().Map(), + "rule_name": aws.StringValue(rule.RuleName), + "target_vault_name": aws.StringValue(rule.TargetBackupVaultName), + "schedule": aws.StringValue(rule.ScheduleExpression), + "enable_continuous_backup": aws.BoolValue(rule.EnableContinuousBackup), + "start_window": int(aws.Int64Value(rule.StartWindowMinutes)), + "completion_window": int(aws.Int64Value(rule.CompletionWindowMinutes)), + "recovery_point_tags": keyvaluetags.BackupKeyValueTags(rule.RecoveryPointTags).IgnoreAws().Map(), } if lifecycle := rule.Lifecycle; lifecycle != nil { @@ -475,6 +503,9 @@ func backupBackupPlanHash(vRule interface{}) int { if v, ok := mRule["schedule"].(string); ok { buf.WriteString(fmt.Sprintf("%s-", v)) } + if v, ok := mRule["enable_continuous_backup"].(bool); ok { + buf.WriteString(fmt.Sprintf("%t-", v)) + } if v, ok := mRule["start_window"].(int); ok { buf.WriteString(fmt.Sprintf("%d-", v)) } diff --git a/aws/resource_aws_backup_plan_test.go b/aws/resource_aws_backup_plan_test.go index 30aa7c16796d..c06cc04ceb61 100644 --- a/aws/resource_aws_backup_plan_test.go +++ b/aws/resource_aws_backup_plan_test.go @@ -20,6 +20,7 @@ func TestAccAwsBackupPlan_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -56,6 +57,7 @@ func TestAccAwsBackupPlan_withTags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -111,6 +113,7 @@ func TestAccAwsBackupPlan_withRules(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -193,6 +196,7 @@ func TestAccAwsBackupPlan_withLifecycle(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -264,6 +268,7 @@ func TestAccAwsBackupPlan_withRecoveryPointTags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -336,6 +341,7 @@ func TestAccAwsBackupPlan_Rule_CopyAction_SameRegion(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -398,6 +404,7 @@ func TestAccAwsBackupPlan_Rule_CopyAction_NoLifecycle(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -458,6 +465,7 @@ func TestAccAwsBackupPlan_Rule_CopyAction_Multiple(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -497,6 +505,7 @@ func TestAccAwsBackupPlan_Rule_CopyAction_CrossRegion(t *testing.T) { testAccPreCheckAWSBackup(t) testAccMultipleRegionPreCheck(t, 2) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), ProviderFactories: testAccProviderFactoriesAlternate(&providers), CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -532,6 +541,7 @@ func TestAccAwsBackupPlan_AdvancedBackupSetting(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -553,6 +563,58 @@ func TestAccAwsBackupPlan_AdvancedBackupSetting(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccAwsBackupPlanConfigAdvancedBackupSettingUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists(resourceName, &plan), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "advanced_backup_setting.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "advanced_backup_setting.*", map[string]string{ + "backup_options.%": "1", + "backup_options.WindowsVSS": "disabled", + "resource_type": "EC2", + }), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_EnableContinuousBackup(t *testing.T) { + var plan backup.GetBackupPlanOutput + resourceName := "aws_backup_plan.test" + rName := fmt.Sprintf("tf-testacc-backup-%s", acctest.RandString(14)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsBackupPlanConfigEnableContinuousBackup(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists(resourceName, &plan), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "backup", regexp.MustCompile(`backup-plan:.+`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "rule_name": rName, + "target_vault_name": rName, + "schedule": "cron(0 12 * * ? *)", + "enable_continuous_backup": "true", + "lifecycle.#": "1", + "lifecycle.0.delete_after": "35", + }), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "version"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -564,6 +626,7 @@ func TestAccAwsBackupPlan_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupPlanDestroy, Steps: []resource.TestStep{ @@ -1010,7 +1073,7 @@ resource "aws_backup_plan" "test" { func testAccAwsBackupPlanConfigAdvancedBackupSetting(rName string) string { return fmt.Sprintf(` resource "aws_backup_vault" "test" { - name = "%[1]s-1" + name = %[1]q } resource "aws_backup_plan" "test" { @@ -1031,8 +1094,63 @@ resource "aws_backup_plan" "test" { backup_options = { WindowsVSS = "enabled" } + resource_type = "EC2" } } `, rName) } + +func testAccAwsBackupPlanConfigAdvancedBackupSettingUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_backup_vault" "test" { + name = %[1]q +} + +resource "aws_backup_plan" "test" { + name = %[1]q + + rule { + rule_name = %[1]q + target_vault_name = aws_backup_vault.test.name + schedule = "cron(0 12 * * ? *)" + + lifecycle { + cold_storage_after = 30 + delete_after = 180 + } + } + + advanced_backup_setting { + backup_options = { + WindowsVSS = "disabled" + } + + resource_type = "EC2" + } +} +`, rName) +} + +func testAccAwsBackupPlanConfigEnableContinuousBackup(rName string) string { + return fmt.Sprintf(` +resource "aws_backup_vault" "test" { + name = %[1]q +} + +resource "aws_backup_plan" "test" { + name = %[1]q + + rule { + rule_name = %[1]q + target_vault_name = aws_backup_vault.test.name + schedule = "cron(0 12 * * ? *)" + enable_continuous_backup = true + + lifecycle { + delete_after = 35 + } + } +} +`, rName) +} diff --git a/aws/resource_aws_backup_region_settings_test.go b/aws/resource_aws_backup_region_settings_test.go index c6a2cda56f1f..9ccee4284aed 100644 --- a/aws/resource_aws_backup_region_settings_test.go +++ b/aws/resource_aws_backup_region_settings_test.go @@ -21,6 +21,7 @@ func TestAccAwsBackupRegionSettings_basic(t *testing.T) { testAccPartitionHasServicePreCheck(fsx.EndpointsID, t) testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: nil, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_backup_selection.go b/aws/resource_aws_backup_selection.go index 483ddf0d8bd6..eac3f1691630 100644 --- a/aws/resource_aws_backup_selection.go +++ b/aws/resource_aws_backup_selection.go @@ -5,13 +5,16 @@ import ( "log" "regexp" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/backup" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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/terraform-providers/terraform-provider-aws/aws/internal/service/backup/waiter" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsBackupSelection() *schema.Resource { @@ -98,7 +101,7 @@ func resourceAwsBackupSelectionCreate(d *schema.ResourceData, meta interface{}) // Retry for IAM eventual consistency var output *backup.CreateBackupSelectionOutput - err := resource.Retry(1*time.Minute, func() *resource.RetryError { + err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { var err error output, err = conn.CreateBackupSelection(input) @@ -144,16 +147,50 @@ func resourceAwsBackupSelectionRead(d *schema.ResourceData, meta interface{}) er SelectionId: aws.String(d.Id()), } - resp, err := conn.GetBackupSelection(input) - if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") || - isAWSErr(err, backup.ErrCodeInvalidParameterValueException, "Cannot find Backup plan") { + var resp *backup.GetBackupSelectionOutput + + err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { + var err error + + resp, err = conn.GetBackupSelection(input) + + if d.IsNewResource() && tfawserr.ErrCodeEquals(err, backup.ErrCodeResourceNotFoundException) { + return resource.RetryableError(err) + } + + if d.IsNewResource() && tfawserr.ErrMessageContains(err, backup.ErrCodeInvalidParameterValueException, "Cannot find Backup plan") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + resp, err = conn.GetBackupSelection(input) + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, backup.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Backup Selection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrMessageContains(err, backup.ErrCodeInvalidParameterValueException, "Cannot find Backup plan") { log.Printf("[WARN] Backup Selection (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading Backup Selection: %s", err) + return fmt.Errorf("error reading Backup Selection (%s): %w", d.Id(), err) + } + + if resp == nil { + return fmt.Errorf("error reading Backup Selection (%s): empty response", d.Id()) } d.Set("plan_id", resp.BackupPlanId) diff --git a/aws/resource_aws_backup_selection_test.go b/aws/resource_aws_backup_selection_test.go index 9d6c72f5a2c3..9fba2e814e93 100644 --- a/aws/resource_aws_backup_selection_test.go +++ b/aws/resource_aws_backup_selection_test.go @@ -18,6 +18,7 @@ func TestAccAwsBackupSelection_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ @@ -44,6 +45,7 @@ func TestAccAwsBackupSelection_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ @@ -67,6 +69,7 @@ func TestAccAwsBackupSelection_disappears_BackupPlan(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ @@ -90,6 +93,7 @@ func TestAccAwsBackupSelection_withTags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ @@ -117,6 +121,7 @@ func TestAccAwsBackupSelection_withResources(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ @@ -144,6 +149,7 @@ func TestAccAwsBackupSelection_updateTag(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupSelectionDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_backup_vault.go b/aws/resource_aws_backup_vault.go index 1f4a4fa31f2d..ec0018f6da80 100644 --- a/aws/resource_aws_backup_vault.go +++ b/aws/resource_aws_backup_vault.go @@ -29,7 +29,8 @@ func resourceAwsBackupVault() *schema.Resource { ForceNew: true, ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]{1,50}$`), "must consist of lowercase letters, numbers, and hyphens."), }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "kms_key_arn": { Type: schema.TypeString, Optional: true, @@ -46,15 +47,19 @@ func resourceAwsBackupVault() *schema.Resource { Computed: true, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsBackupVaultCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &backup.CreateBackupVaultInput{ BackupVaultName: aws.String(d.Get("name").(string)), - BackupVaultTags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().BackupTags(), + BackupVaultTags: tags.IgnoreAws().BackupTags(), } if v, ok := d.GetOk("kms_key_arn"); ok { @@ -73,6 +78,7 @@ func resourceAwsBackupVaultCreate(d *schema.ResourceData, meta interface{}) erro func resourceAwsBackupVaultRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &backup.DescribeBackupVaultInput{ @@ -103,8 +109,15 @@ func resourceAwsBackupVaultRead(d *schema.ResourceData, meta interface{}) error if err != nil { return fmt.Errorf("error listing tags for Backup Vault (%s): %s", d.Id(), err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -113,8 +126,8 @@ func resourceAwsBackupVaultRead(d *schema.ResourceData, meta interface{}) error func resourceAwsBackupVaultUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.BackupUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags for Backup Vault (%s): %s", d.Id(), err) } @@ -126,13 +139,13 @@ func resourceAwsBackupVaultUpdate(d *schema.ResourceData, meta interface{}) erro func resourceAwsBackupVaultDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn - input := &backup.DeleteBackupVaultInput{ - BackupVaultName: aws.String(d.Get("name").(string)), - } + log.Printf("[DEBUG] Deleting Backup Vault: %s", d.Id()) + _, err := conn.DeleteBackupVault(&backup.DeleteBackupVaultInput{ + BackupVaultName: aws.String(d.Id()), + }) - _, err := conn.DeleteBackupVault(input) if err != nil { - return fmt.Errorf("error deleting Backup Vault (%s): %s", d.Id(), err) + return fmt.Errorf("error deleting Backup Vault (%s): %w", d.Id(), err) } return nil diff --git a/aws/resource_aws_backup_vault_notifications_test.go b/aws/resource_aws_backup_vault_notifications_test.go index 78500e920ee4..8ccc610791cb 100644 --- a/aws/resource_aws_backup_vault_notifications_test.go +++ b/aws/resource_aws_backup_vault_notifications_test.go @@ -22,52 +22,51 @@ func init() { func testSweepBackupVaultNotifications(region string) error { client, err := sharedClientForRegion(region) + if err != nil { return fmt.Errorf("Error getting client: %w", err) } + conn := client.(*AWSClient).backupconn - var sweeperErrs *multierror.Error + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error input := &backup.ListBackupVaultsInput{} - for { - output, err := conn.ListBackupVaults(input) - if err != nil { - if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping Backup Vault Notifications sweep for %s: %s", region, err) - return nil - } - sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Backup Vault Notifications: %w", err)) - return sweeperErrs.ErrorOrNil() + err = conn.ListBackupVaultsPages(input, func(page *backup.ListBackupVaultsOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - if len(output.BackupVaultList) == 0 { - log.Print("[DEBUG] No Backup Vault Notifications to sweep") - return nil - } - - for _, rule := range output.BackupVaultList { - name := aws.StringValue(rule.BackupVaultName) - - log.Printf("[INFO] Deleting Backup Vault Notifications %s", name) - _, err := conn.DeleteBackupVaultNotifications(&backup.DeleteBackupVaultNotificationsInput{ - BackupVaultName: aws.String(name), - }) - if err != nil { - sweeperErr := fmt.Errorf("error deleting Backup Vault Notifications %s: %w", name, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + for _, vault := range page.BackupVaultList { + if vault == nil { continue } - } - if output.NextToken == nil { - break + r := resourceAwsBackupVaultNotifications() + d := r.Data(nil) + d.SetId(aws.StringValue(vault.BackupVaultName)) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) } - input.NextToken = output.NextToken + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing Backup Vaults for %s: %w", region, err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping Backup Vault Notifications for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping Backup Vault Notifications sweep for %s: %s", region, errs) + return nil } - return sweeperErrs.ErrorOrNil() + return errs.ErrorOrNil() } func TestAccAwsBackupVaultNotification_basic(t *testing.T) { @@ -77,6 +76,7 @@ func TestAccAwsBackupVaultNotification_basic(t *testing.T) { resourceName := "aws_backup_vault_notifications.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultNotificationDestroy, Steps: []resource.TestStep{ @@ -103,6 +103,7 @@ func TestAccAwsBackupVaultNotification_disappears(t *testing.T) { resourceName := "aws_backup_vault_notifications.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultNotificationDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_backup_vault_policy.go b/aws/resource_aws_backup_vault_policy.go index b382caf340a7..670e124fc446 100644 --- a/aws/resource_aws_backup_vault_policy.go +++ b/aws/resource_aws_backup_vault_policy.go @@ -6,8 +6,12 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/backup" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfbackup "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/backup" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/backup/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsBackupVaultPolicy() *schema.Resource { @@ -21,6 +25,10 @@ func resourceAwsBackupVaultPolicy() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "backup_vault_arn": { + Type: schema.TypeString, + Computed: true, + }, "backup_vault_name": { Type: schema.TypeString, Required: true, @@ -32,10 +40,6 @@ func resourceAwsBackupVaultPolicy() *schema.Resource { ValidateFunc: validation.StringIsJSON, DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, }, - "backup_vault_arn": { - Type: schema.TypeString, - Computed: true, - }, }, } } @@ -43,17 +47,19 @@ func resourceAwsBackupVaultPolicy() *schema.Resource { func resourceAwsBackupVaultPolicyPut(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn + name := d.Get("backup_vault_name").(string) input := &backup.PutBackupVaultAccessPolicyInput{ - BackupVaultName: aws.String(d.Get("backup_vault_name").(string)), + BackupVaultName: aws.String(name), Policy: aws.String(d.Get("policy").(string)), } _, err := conn.PutBackupVaultAccessPolicy(input) + if err != nil { - return fmt.Errorf("error creating Backup Vault Policy (%s): %w", d.Id(), err) + return fmt.Errorf("error creating Backup Vault Policy (%s): %w", name, err) } - d.SetId(d.Get("backup_vault_name").(string)) + d.SetId(name) return resourceAwsBackupVaultPolicyRead(d, meta) } @@ -61,13 +67,10 @@ func resourceAwsBackupVaultPolicyPut(d *schema.ResourceData, meta interface{}) e func resourceAwsBackupVaultPolicyRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn - input := &backup.GetBackupVaultAccessPolicyInput{ - BackupVaultName: aws.String(d.Id()), - } + output, err := finder.BackupVaultAccessPolicyByName(conn, d.Id()) - resp, err := conn.GetBackupVaultAccessPolicy(input) - if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] Backup Vault Policy %s not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Backup Vault Policy (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -75,9 +78,10 @@ func resourceAwsBackupVaultPolicyRead(d *schema.ResourceData, meta interface{}) if err != nil { return fmt.Errorf("error reading Backup Vault Policy (%s): %w", d.Id(), err) } - d.Set("backup_vault_name", resp.BackupVaultName) - d.Set("policy", resp.Policy) - d.Set("backup_vault_arn", resp.BackupVaultArn) + + d.Set("backup_vault_arn", output.BackupVaultArn) + d.Set("backup_vault_name", output.BackupVaultName) + d.Set("policy", output.Policy) return nil } @@ -85,15 +89,16 @@ func resourceAwsBackupVaultPolicyRead(d *schema.ResourceData, meta interface{}) func resourceAwsBackupVaultPolicyDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).backupconn - input := &backup.DeleteBackupVaultAccessPolicyInput{ + log.Printf("[DEBUG] Deleting Backup Vault Policy (%s)", d.Id()) + _, err := conn.DeleteBackupVaultAccessPolicy(&backup.DeleteBackupVaultAccessPolicyInput{ BackupVaultName: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, backup.ErrCodeResourceNotFoundException) || tfawserr.ErrCodeEquals(err, tfbackup.ErrCodeAccessDeniedException) { + return nil } - _, err := conn.DeleteBackupVaultAccessPolicy(input) if err != nil { - if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { - return nil - } return fmt.Errorf("error deleting Backup Vault Policy (%s): %w", d.Id(), err) } diff --git a/aws/resource_aws_backup_vault_policy_test.go b/aws/resource_aws_backup_vault_policy_test.go index 9903b751b4ed..b574690a616b 100644 --- a/aws/resource_aws_backup_vault_policy_test.go +++ b/aws/resource_aws_backup_vault_policy_test.go @@ -8,10 +8,12 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/backup" - "github.com/hashicorp/go-multierror" + multierror "github.com/hashicorp/go-multierror" "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/terraform-providers/terraform-provider-aws/aws/internal/service/backup/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -27,45 +29,37 @@ func testSweepBackupVaultPolicies(region string) error { return fmt.Errorf("Error getting client: %w", err) } conn := client.(*AWSClient).backupconn - var sweeperErrs *multierror.Error - input := &backup.ListBackupVaultsInput{} + var sweeperErrs *multierror.Error + sweepResources := make([]*testSweepResource, 0) - for { - output, err := conn.ListBackupVaults(input) - if err != nil { - if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping Backup Vault Policies sweep for %s: %s", region, err) - return nil - } - sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Backup Vault Policies: %w", err)) - return sweeperErrs.ErrorOrNil() + err = conn.ListBackupVaultsPages(input, func(page *backup.ListBackupVaultsOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - if len(output.BackupVaultList) == 0 { - log.Print("[DEBUG] No Backup Vault Policies to sweep") - return nil - } + for _, vault := range page.BackupVaultList { + r := resourceAwsBackupVaultPolicy() + d := r.Data(nil) + d.SetId(aws.StringValue(vault.BackupVaultName)) - for _, rule := range output.BackupVaultList { - name := aws.StringValue(rule.BackupVaultName) - - log.Printf("[INFO] Deleting Backup Vault Policies %s", name) - _, err := conn.DeleteBackupVaultAccessPolicy(&backup.DeleteBackupVaultAccessPolicyInput{ - BackupVaultName: aws.String(name), - }) - if err != nil { - sweeperErr := fmt.Errorf("error deleting Backup Vault Policies %s: %w", name, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue - } + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) } - if output.NextToken == nil { - break - } - input.NextToken = output.NextToken + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Backup Vault Policies sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Backup Vaults for %s: %w", region, err)) + } + + if err := testSweepResourceOrchestrator(sweepResources); err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping Backup Vault Policies for %s: %w", region, err)) } return sweeperErrs.ErrorOrNil() @@ -73,11 +67,12 @@ func testSweepBackupVaultPolicies(region string) error { func TestAccAwsBackupVaultPolicy_basic(t *testing.T) { var vault backup.GetBackupVaultAccessPolicyOutput - rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_backup_vault_policy.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultPolicyDestroy, Steps: []resource.TestStep{ @@ -106,11 +101,12 @@ func TestAccAwsBackupVaultPolicy_basic(t *testing.T) { func TestAccAwsBackupVaultPolicy_disappears(t *testing.T) { var vault backup.GetBackupVaultAccessPolicyOutput - rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_backup_vault_policy.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultPolicyDestroy, Steps: []resource.TestStep{ @@ -126,24 +122,49 @@ func TestAccAwsBackupVaultPolicy_disappears(t *testing.T) { }) } +func TestAccAwsBackupVaultPolicy_disappears_vault(t *testing.T) { + var vault backup.GetBackupVaultAccessPolicyOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_backup_vault_policy.test" + vaultResourceName := "aws_backup_vault.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupVaultPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupVaultPolicyConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupVaultPolicyExists(resourceName, &vault), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBackupVault(), vaultResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAwsBackupVaultPolicyDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).backupconn + for _, rs := range s.RootModule().Resources { if rs.Type != "aws_backup_vault_policy" { continue } - input := &backup.GetBackupVaultAccessPolicyInput{ - BackupVaultName: aws.String(rs.Primary.ID), - } + _, err := finder.BackupVaultAccessPolicyByName(conn, rs.Primary.ID) - resp, err := conn.GetBackupVaultAccessPolicy(input) + if tfresource.NotFound(err) { + continue + } - if err == nil { - if aws.StringValue(resp.BackupVaultName) == rs.Primary.ID { - return fmt.Errorf("Backup Plan Policies '%s' was not deleted properly", rs.Primary.ID) - } + if err != nil { + return err } + + return fmt.Errorf("Backup Vault Policy %s still exists", rs.Primary.ID) } return nil @@ -156,16 +177,19 @@ func testAccCheckAwsBackupVaultPolicyExists(name string, vault *backup.GetBackup return fmt.Errorf("Not found: %s", name) } - conn := testAccProvider.Meta().(*AWSClient).backupconn - params := &backup.GetBackupVaultAccessPolicyInput{ - BackupVaultName: aws.String(rs.Primary.ID), + if rs.Primary.ID == "" { + return fmt.Errorf("No Backup Vault Policy ID is set") } - resp, err := conn.GetBackupVaultAccessPolicy(params) + + conn := testAccProvider.Meta().(*AWSClient).backupconn + + output, err := finder.BackupVaultAccessPolicyByName(conn, rs.Primary.ID) + if err != nil { return err } - *vault = *resp + *vault = *output return nil } diff --git a/aws/resource_aws_backup_vault_test.go b/aws/resource_aws_backup_vault_test.go index 011cf0b9f582..5d2ad43ed483 100644 --- a/aws/resource_aws_backup_vault_test.go +++ b/aws/resource_aws_backup_vault_test.go @@ -2,15 +2,117 @@ package aws import ( "fmt" + "log" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/backup" + multierror "github.com/hashicorp/go-multierror" "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" ) +func init() { + resource.AddTestSweepers("aws_backup_vault", &resource.Sweeper{ + Name: "aws_backup_vault", + F: testSweepBackupVaults, + Dependencies: []string{ + "aws_backup_vault_notifications", + "aws_backup_vault_policy", + }, + }) +} + +func testSweepBackupVaults(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("Error getting client: %w", err) + } + conn := client.(*AWSClient).backupconn + input := &backup.ListBackupVaultsInput{} + var sweeperErrs *multierror.Error + sweepResources := make([]*testSweepResource, 0) + + err = conn.ListBackupVaultsPages(input, func(page *backup.ListBackupVaultsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, vault := range page.BackupVaultList { + failedToDeleteRecoveryPoint := false + name := aws.StringValue(vault.BackupVaultName) + input := &backup.ListRecoveryPointsByBackupVaultInput{ + BackupVaultName: aws.String(name), + } + + err := conn.ListRecoveryPointsByBackupVaultPages(input, func(page *backup.ListRecoveryPointsByBackupVaultOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, recoveryPoint := range page.RecoveryPoints { + arn := aws.StringValue(recoveryPoint.RecoveryPointArn) + + log.Printf("[INFO] Deleting Recovery Point (%s) in Backup Vault (%s)", arn, name) + _, err := conn.DeleteRecoveryPoint(&backup.DeleteRecoveryPointInput{ + BackupVaultName: aws.String(name), + RecoveryPointArn: aws.String(arn), + }) + + if err != nil { + log.Printf("[WARN] Failed to delete Recovery Point (%s) in Backup Vault (%s): %s", arn, name, err) + failedToDeleteRecoveryPoint = true + } + } + + return !lastPage + }) + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Reovery Points in Backup Vault (%s) for %s: %w", name, region, err)) + } + + // Ignore Default and Automatic EFS Backup Vaults in region (cannot be deleted) + if name == "Default" || name == "aws/efs/automatic-backup-vault" { + log.Printf("[INFO] Skipping Backup Vault: %s", name) + continue + } + + // Backup Vault deletion only supported when empty + // Reference: https://docs.aws.amazon.com/aws-backup/latest/devguide/API_DeleteBackupVault.html + if failedToDeleteRecoveryPoint { + log.Printf("[INFO] Skipping Backup Vault (%s): not empty", name) + continue + } + + r := resourceAwsBackupVault() + d := r.Data(nil) + d.SetId(name) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Backup Vaults sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Backup Vaults for %s: %w", region, err)) + } + + if err := testSweepResourceOrchestrator(sweepResources); err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping Backup Vaults for %s: %w", region, err)) + } + + return sweeperErrs.ErrorOrNil() +} + func TestAccAwsBackupVault_basic(t *testing.T) { var vault backup.DescribeBackupVaultOutput @@ -18,6 +120,7 @@ func TestAccAwsBackupVault_basic(t *testing.T) { resourceName := "aws_backup_vault.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultDestroy, Steps: []resource.TestStep{ @@ -43,6 +146,7 @@ func TestAccAwsBackupVault_withKmsKey(t *testing.T) { resourceName := "aws_backup_vault.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultDestroy, Steps: []resource.TestStep{ @@ -69,6 +173,7 @@ func TestAccAwsBackupVault_withTags(t *testing.T) { resourceName := "aws_backup_vault.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultDestroy, Steps: []resource.TestStep{ @@ -117,6 +222,7 @@ func TestAccAwsBackupVault_disappears(t *testing.T) { resourceName := "aws_backup_vault.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + ErrorCheck: testAccErrorCheck(t, backup.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAwsBackupVaultDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_batch_compute_environment.go b/aws/resource_aws_batch_compute_environment.go index 74945d95a12c..19f1be3bc81c 100644 --- a/aws/resource_aws_batch_compute_environment.go +++ b/aws/resource_aws_batch_compute_environment.go @@ -1,16 +1,21 @@ package aws import ( + "context" "fmt" "log" - "time" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/batch" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/naming" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/batch/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/batch/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsBatchComputeEnvironment() *schema.Resource { @@ -19,13 +24,15 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { Read: resourceAwsBatchComputeEnvironmentRead, Update: resourceAwsBatchComputeEnvironmentUpdate, Delete: resourceAwsBatchComputeEnvironmentDelete, - Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.Set("compute_environment_name", d.Id()) - return []*schema.ResourceData{d}, nil - }, + State: schema.ImportStatePassthrough, }, + + CustomizeDiff: customdiff.Sequence( + resourceAwsBatchComputeEnvironmentCustomizeDiff, + SetTagsDiff, + ), + Schema: map[string]*schema.Schema{ "compute_environment_name": { Type: schema.TypeString, @@ -38,6 +45,7 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { "compute_environment_name_prefix": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, ConflictsWith: []string{"compute_environment_name"}, ValidateFunc: validateBatchPrefix, @@ -45,6 +53,7 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { "compute_resources": { Type: schema.TypeList, Optional: true, + ForceNew: true, MinItems: 0, MaxItems: 1, Elem: &schema.Resource{ @@ -53,10 +62,10 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - batch.CRAllocationStrategyBestFit, - batch.CRAllocationStrategyBestFitProgressive, - batch.CRAllocationStrategySpotCapacityOptimized}, true), + StateFunc: func(val interface{}) string { + return strings.ToUpper(val.(string)) + }, + ValidateFunc: validation.StringInSlice(batch.CRAllocationStrategy_Values(), true), }, "bid_percentage": { Type: schema.TypeInt, @@ -80,13 +89,13 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { }, "instance_role": { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, ValidateFunc: validateArn, }, "instance_type": { Type: schema.TypeSet, - Required: true, + Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -100,14 +109,14 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { "launch_template_id": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{"compute_resources.0.launch_template.0.launch_template_name"}, ForceNew: true, + ConflictsWith: []string{"compute_resources.0.launch_template.0.launch_template_name"}, }, "launch_template_name": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{"compute_resources.0.launch_template.0.launch_template_id"}, ForceNew: true, + ConflictsWith: []string{"compute_resources.0.launch_template.0.launch_template_id"}, }, "version": { Type: schema.TypeString, @@ -123,12 +132,11 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { }, "min_vcpus": { Type: schema.TypeInt, - Required: true, + Optional: true, }, "security_group_ids": { Type: schema.TypeSet, Required: true, - ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "spot_iam_fleet_role": { @@ -140,36 +148,46 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { "subnets": { Type: schema.TypeSet, Required: true, - ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "tags": tagsSchemaForceNew(), "type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{batch.CRTypeEc2, batch.CRTypeSpot}, true), + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: func(val interface{}) string { + return strings.ToUpper(val.(string)) + }, + ValidateFunc: validation.StringInSlice(batch.CRType_Values(), true), }, }, }, }, "service_role": { Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, ValidateFunc: validateArn, }, "state": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{batch.CEStateEnabled, batch.CEStateDisabled}, true), + Type: schema.TypeString, + Optional: true, + StateFunc: func(val interface{}) string { + return strings.ToUpper(val.(string)) + }, + ValidateFunc: validation.StringInSlice(batch.CEState_Values(), true), Default: batch.CEStateEnabled, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{batch.CETypeManaged, batch.CETypeUnmanaged}, true), + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: func(val interface{}) string { + return strings.ToUpper(val.(string)) + }, + ValidateFunc: validation.StringInSlice(batch.CEType_Values(), true), }, "arn": { Type: schema.TypeString, @@ -193,126 +211,45 @@ func resourceAwsBatchComputeEnvironment() *schema.Resource { func resourceAwsBatchComputeEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) - // Build the compute environment name. - var computeEnvironmentName string - if v, ok := d.GetOk("compute_environment_name"); ok { - computeEnvironmentName = v.(string) - } else if v, ok := d.GetOk("compute_environment_name_prefix"); ok { - computeEnvironmentName = resource.PrefixedUniqueId(v.(string)) - } else { - computeEnvironmentName = resource.UniqueId() - } - d.Set("compute_environment_name", computeEnvironmentName) - - serviceRole := d.Get("service_role").(string) + computeEnvironmentName := naming.Generate(d.Get("compute_environment_name").(string), d.Get("compute_environment_name_prefix").(string)) computeEnvironmentType := d.Get("type").(string) input := &batch.CreateComputeEnvironmentInput{ ComputeEnvironmentName: aws.String(computeEnvironmentName), - ServiceRole: aws.String(serviceRole), + ServiceRole: aws.String(d.Get("service_role").(string)), Type: aws.String(computeEnvironmentType), } - if v, ok := d.GetOk("state"); ok { - input.State = aws.String(v.(string)) + // TODO Check in CustomizeDiff that UNMANAGED compute environment has no compute_resources. + // TODO This would be a breaking change. + if computeEnvironmentType := strings.ToUpper(computeEnvironmentType); computeEnvironmentType == batch.CETypeManaged { + if v, ok := d.GetOk("compute_resources"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.ComputeResources = expandBatchComputeResource(v.([]interface{})[0].(map[string]interface{})) + } } - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - input.Tags = keyvaluetags.New(v).IgnoreAws().BatchTags() + if v, ok := d.GetOk("state"); ok { + input.State = aws.String(v.(string)) } - if computeEnvironmentType == batch.CETypeManaged { - computeResources := d.Get("compute_resources").([]interface{}) - if len(computeResources) == 0 { - return fmt.Errorf("One compute environment is expected, but no compute environments are set") - } - computeResource := computeResources[0].(map[string]interface{}) - - instanceRole := computeResource["instance_role"].(string) - maxvCpus := int64(computeResource["max_vcpus"].(int)) - minvCpus := int64(computeResource["min_vcpus"].(int)) - computeResourceType := computeResource["type"].(string) - - var instanceTypes []*string - for _, v := range computeResource["instance_type"].(*schema.Set).List() { - instanceTypes = append(instanceTypes, aws.String(v.(string))) - } - - var securityGroupIds []*string - for _, v := range computeResource["security_group_ids"].(*schema.Set).List() { - securityGroupIds = append(securityGroupIds, aws.String(v.(string))) - } - - var subnets []*string - for _, v := range computeResource["subnets"].(*schema.Set).List() { - subnets = append(subnets, aws.String(v.(string))) - } - - input.ComputeResources = &batch.ComputeResource{ - InstanceRole: aws.String(instanceRole), - InstanceTypes: instanceTypes, - MaxvCpus: aws.Int64(maxvCpus), - MinvCpus: aws.Int64(minvCpus), - SecurityGroupIds: securityGroupIds, - Subnets: subnets, - Type: aws.String(computeResourceType), - } - - if v, ok := computeResource["allocation_strategy"]; ok { - input.ComputeResources.AllocationStrategy = aws.String(v.(string)) - } - if v, ok := computeResource["bid_percentage"]; ok { - input.ComputeResources.BidPercentage = aws.Int64(int64(v.(int))) - } - if v, ok := computeResource["desired_vcpus"]; ok && v.(int) > 0 { - input.ComputeResources.DesiredvCpus = aws.Int64(int64(v.(int))) - } - if v, ok := computeResource["ec2_key_pair"]; ok { - input.ComputeResources.Ec2KeyPair = aws.String(v.(string)) - } - if v, ok := computeResource["image_id"]; ok { - input.ComputeResources.ImageId = aws.String(v.(string)) - } - if v, ok := computeResource["spot_iam_fleet_role"]; ok { - input.ComputeResources.SpotIamFleetRole = aws.String(v.(string)) - } - if v, ok := computeResource["tags"]; ok { - input.ComputeResources.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().BatchTags() - } - - if raw, ok := computeResource["launch_template"]; ok && len(raw.([]interface{})) > 0 { - input.ComputeResources.LaunchTemplate = &batch.LaunchTemplateSpecification{} - launchTemplate := raw.([]interface{})[0].(map[string]interface{}) - if v, ok := launchTemplate["launch_template_id"]; ok { - input.ComputeResources.LaunchTemplate.LaunchTemplateId = aws.String(v.(string)) - } - if v, ok := launchTemplate["launch_template_name"]; ok { - input.ComputeResources.LaunchTemplate.LaunchTemplateName = aws.String(v.(string)) - } - if v, ok := launchTemplate["version"]; ok { - input.ComputeResources.LaunchTemplate.Version = aws.String(v.(string)) - } - } + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().BatchTags() } - log.Printf("[DEBUG] Create compute environment %s.\n", input) + log.Printf("[DEBUG] Creating Batch Compute Environment: %s", input) + output, err := conn.CreateComputeEnvironment(input) - if _, err := conn.CreateComputeEnvironment(input); err != nil { - return err + if err != nil { + return fmt.Errorf("error creating Batch Compute Environment (%s): %w", computeEnvironmentName, err) } - d.SetId(computeEnvironmentName) + d.SetId(aws.StringValue(output.ComputeEnvironmentName)) - stateConf := &resource.StateChangeConf{ - Pending: []string{batch.CEStatusCreating}, - Target: []string{batch.CEStatusValid}, - Refresh: resourceAwsBatchComputeEnvironmentStatusRefreshFunc(computeEnvironmentName, conn), - Timeout: d.Timeout(schema.TimeoutCreate), - MinTimeout: 5 * time.Second, - } - if _, err := stateConf.WaitForState(); err != nil { - return err + if _, err := waiter.ComputeEnvironmentCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Batch Compute Environment (%s) create: %w", d.Id(), err) } return resourceAwsBatchComputeEnvironmentRead(d, meta) @@ -320,100 +257,53 @@ func resourceAwsBatchComputeEnvironmentCreate(d *schema.ResourceData, meta inter func resourceAwsBatchComputeEnvironmentRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - computeEnvironmentName := d.Get("compute_environment_name").(string) - - input := &batch.DescribeComputeEnvironmentsInput{ - ComputeEnvironments: []*string{ - aws.String(computeEnvironmentName), - }, - } + computeEnvironment, err := finder.ComputeEnvironmentDetailByName(conn, d.Id()) - log.Printf("[DEBUG] Read compute environment %s.\n", input) - - result, err := conn.DescribeComputeEnvironments(input) - - if err != nil { - return fmt.Errorf("error reading Batch Compute Environment (%s): %w", d.Id(), err) - } - - if len(result.ComputeEnvironments) == 0 { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] Batch Compute Environment (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - computeEnvironment := result.ComputeEnvironments[0] - - d.Set("service_role", computeEnvironment.ServiceRole) - d.Set("state", computeEnvironment.State) - - if err := d.Set("tags", keyvaluetags.BatchKeyValueTags(computeEnvironment.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + if err != nil { + return fmt.Errorf("error reading Batch Compute Environment (%s): %w", d.Id(), err) } - d.Set("type", computeEnvironment.Type) - - if aws.StringValue(computeEnvironment.Type) == batch.CETypeManaged { - if err := d.Set("compute_resources", flattenBatchComputeResources(computeEnvironment.ComputeResources)); err != nil { - return fmt.Errorf("error setting compute_resources: %s", err) - } - } + computeEnvironmentType := aws.StringValue(computeEnvironment.Type) d.Set("arn", computeEnvironment.ComputeEnvironmentArn) + d.Set("compute_environment_name", computeEnvironment.ComputeEnvironmentName) + d.Set("compute_environment_name_prefix", naming.NamePrefixFromName(aws.StringValue(computeEnvironment.ComputeEnvironmentName))) d.Set("ecs_cluster_arn", computeEnvironment.EcsClusterArn) + d.Set("service_role", computeEnvironment.ServiceRole) + d.Set("state", computeEnvironment.State) d.Set("status", computeEnvironment.Status) d.Set("status_reason", computeEnvironment.StatusReason) + d.Set("type", computeEnvironmentType) - return nil -} - -func flattenBatchComputeResources(computeResource *batch.ComputeResource) []map[string]interface{} { - result := make([]map[string]interface{}, 0) - m := make(map[string]interface{}) - - m["allocation_strategy"] = aws.StringValue(computeResource.AllocationStrategy) - m["bid_percentage"] = int(aws.Int64Value(computeResource.BidPercentage)) - m["desired_vcpus"] = int(aws.Int64Value(computeResource.DesiredvCpus)) - m["ec2_key_pair"] = aws.StringValue(computeResource.Ec2KeyPair) - m["image_id"] = aws.StringValue(computeResource.ImageId) - m["instance_role"] = aws.StringValue(computeResource.InstanceRole) - m["instance_type"] = flattenStringSet(computeResource.InstanceTypes) - m["max_vcpus"] = int(aws.Int64Value(computeResource.MaxvCpus)) - m["min_vcpus"] = int(aws.Int64Value(computeResource.MinvCpus)) - m["security_group_ids"] = flattenStringSet(computeResource.SecurityGroupIds) - m["spot_iam_fleet_role"] = aws.StringValue(computeResource.SpotIamFleetRole) - m["subnets"] = flattenStringSet(computeResource.Subnets) - m["tags"] = keyvaluetags.BatchKeyValueTags(computeResource.Tags).IgnoreAws().Map() - m["type"] = aws.StringValue(computeResource.Type) - - if launchTemplate := computeResource.LaunchTemplate; launchTemplate != nil { - lt := make(map[string]interface{}) - lt["launch_template_id"] = aws.StringValue(launchTemplate.LaunchTemplateId) - lt["launch_template_name"] = aws.StringValue(launchTemplate.LaunchTemplateName) - lt["version"] = aws.StringValue(launchTemplate.Version) - m["launch_template"] = []map[string]interface{}{lt} - } - - result = append(result, m) - return result -} + // TODO See above on how to remove check on type. + if computeEnvironmentType == batch.CETypeManaged { + if computeEnvironment.ComputeResources != nil { + if err := d.Set("compute_resources", []interface{}{flattenBatchComputeResource(computeEnvironment.ComputeResources)}); err != nil { + return fmt.Errorf("error setting compute_resources: %w", err) + } + } else { + d.Set("compute_resources", nil) + } + } -func resourceAwsBatchComputeEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*AWSClient).batchconn - computeEnvironmentName := d.Get("compute_environment_name").(string) + tags := keyvaluetags.BatchKeyValueTags(computeEnvironment.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) - log.Printf("[DEBUG] Disabling Batch Compute Environment: %s", computeEnvironmentName) - err := disableBatchComputeEnvironment(computeEnvironmentName, d.Timeout(schema.TimeoutDelete), conn) - if err != nil { - return fmt.Errorf("error disabling Batch Compute Environment (%s): %s", computeEnvironmentName, err) + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - log.Printf("[DEBUG] Deleting Batch Compute Environment: %s", computeEnvironmentName) - err = deleteBatchComputeEnvironment(computeEnvironmentName, d.Timeout(schema.TimeoutDelete), conn) - if err != nil { - return fmt.Errorf("error deleting Batch Compute Environment (%s): %s", computeEnvironmentName, err) + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -422,143 +312,317 @@ func resourceAwsBatchComputeEnvironmentDelete(d *schema.ResourceData, meta inter func resourceAwsBatchComputeEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn - if d.HasChanges("compute_resources", "service_role", "state") { - computeEnvironmentName := d.Get("compute_environment_name").(string) - + if d.HasChangesExcept("tags", "tags_all") { input := &batch.UpdateComputeEnvironmentInput{ - ComputeEnvironment: aws.String(computeEnvironmentName), - ComputeResources: &batch.ComputeResourceUpdate{}, + ComputeEnvironment: aws.String(d.Id()), } if d.HasChange("service_role") { input.ServiceRole = aws.String(d.Get("service_role").(string)) } + if d.HasChange("state") { input.State = aws.String(d.Get("state").(string)) } - if d.HasChange("compute_resources") { - computeResources := d.Get("compute_resources").([]interface{}) - if len(computeResources) == 0 { - return fmt.Errorf("One compute environment is expected, but no compute environments are set") + // TODO See above on how to remove check on type. + if computeEnvironmentType := strings.ToUpper(d.Get("type").(string)); computeEnvironmentType == batch.CETypeManaged { + // "At least one compute-resources attribute must be specified" + computeResourceUpdate := &batch.ComputeResourceUpdate{ + MaxvCpus: aws.Int64(int64(d.Get("compute_resources.0.max_vcpus").(int))), } - computeResource := computeResources[0].(map[string]interface{}) if d.HasChange("compute_resources.0.desired_vcpus") { - input.ComputeResources.DesiredvCpus = aws.Int64(int64(computeResource["desired_vcpus"].(int))) + computeResourceUpdate.DesiredvCpus = aws.Int64(int64(d.Get("compute_resources.0.desired_vcpus").(int))) } - input.ComputeResources.MaxvCpus = aws.Int64(int64(computeResource["max_vcpus"].(int))) - input.ComputeResources.MinvCpus = aws.Int64(int64(computeResource["min_vcpus"].(int))) - } + if d.HasChange("compute_resources.0.min_vcpus") { + computeResourceUpdate.MinvCpus = aws.Int64(int64(d.Get("compute_resources.0.min_vcpus").(int))) + } - log.Printf("[DEBUG] Update compute environment %s.\n", input) + if d.HasChange("compute_resources.0.security_group_ids") { + computeResourceUpdate.SecurityGroupIds = expandStringSet(d.Get("compute_resources.0.security_group_ids").(*schema.Set)) + } - if _, err := conn.UpdateComputeEnvironment(input); err != nil { - return fmt.Errorf("error updating Batch Compute Environment (%s): %w", d.Id(), err) + if d.HasChange("compute_resources.0.subnets") { + computeResourceUpdate.Subnets = expandStringSet(d.Get("compute_resources.0.subnets").(*schema.Set)) + } + + input.ComputeResources = computeResourceUpdate } - stateConf := &resource.StateChangeConf{ - Pending: []string{batch.CEStatusUpdating}, - Target: []string{batch.CEStatusValid}, - Refresh: resourceAwsBatchComputeEnvironmentStatusRefreshFunc(computeEnvironmentName, conn), - Timeout: d.Timeout(schema.TimeoutUpdate), - MinTimeout: 5 * time.Second, + log.Printf("[DEBUG] Updating Batch Compute Environment: %s", input) + if _, err := conn.UpdateComputeEnvironment(input); err != nil { + return fmt.Errorf("error updating Batch Compute Environment (%s): %w", d.Id(), err) } - if _, err := stateConf.WaitForState(); err != nil { + if _, err := waiter.ComputeEnvironmentUpdated(conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { return fmt.Errorf("error waiting for Batch Compute Environment (%s) update: %w", d.Id(), err) } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.BatchUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) + return fmt.Errorf("error updating tags: %w", err) } } return resourceAwsBatchComputeEnvironmentRead(d, meta) } -func resourceAwsBatchComputeEnvironmentStatusRefreshFunc(computeEnvironmentName string, conn *batch.Batch) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - result, err := conn.DescribeComputeEnvironments(&batch.DescribeComputeEnvironmentsInput{ - ComputeEnvironments: []*string{ - aws.String(computeEnvironmentName), - }, - }) - if err != nil { - return nil, "failed", err +func resourceAwsBatchComputeEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).batchconn + + log.Printf("[DEBUG] Disabling Batch Compute Environment (%s)", d.Id()) + { + input := &batch.UpdateComputeEnvironmentInput{ + ComputeEnvironment: aws.String(d.Id()), + State: aws.String(batch.CEStateDisabled), + } + + if _, err := conn.UpdateComputeEnvironment(input); err != nil { + return fmt.Errorf("error disabling Batch Compute Environment (%s): %w", d.Id(), err) + } + + if _, err := waiter.ComputeEnvironmentDisabled(conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("error waiting for Batch Compute Environment (%s) disable: %w", d.Id(), err) } + } - if len(result.ComputeEnvironments) == 0 { - return nil, "failed", fmt.Errorf("One compute environment is expected, but AWS return no compute environment") + log.Printf("[DEBUG] Deleting Batch Compute Environment (%s)", d.Id()) + { + input := &batch.DeleteComputeEnvironmentInput{ + ComputeEnvironment: aws.String(d.Id()), } - computeEnvironment := result.ComputeEnvironments[0] - return result, *(computeEnvironment.Status), nil + if _, err := conn.DeleteComputeEnvironment(input); err != nil { + return fmt.Errorf("error deleting Batch Compute Environment (%s): %w", d.Id(), err) + } + + if _, err := waiter.ComputeEnvironmentDeleted(conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("error waiting for Batch Compute Environment (%s) delete: %w", d.Id(), err) + } } + + return nil } -func resourceAwsBatchComputeEnvironmentDeleteRefreshFunc(computeEnvironmentName string, conn *batch.Batch) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - result, err := conn.DescribeComputeEnvironments(&batch.DescribeComputeEnvironmentsInput{ - ComputeEnvironments: []*string{ - aws.String(computeEnvironmentName), - }, - }) - if err != nil { - return nil, "failed", err +func resourceAwsBatchComputeEnvironmentCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if diff.Id() != "" { + // Update. + + computeResourceType := strings.ToUpper(diff.Get("compute_resources.0.type").(string)) + fargateComputeResources := false + if computeResourceType == batch.CRTypeFargate || computeResourceType == batch.CRTypeFargateSpot { + fargateComputeResources = true } - if len(result.ComputeEnvironments) == 0 { - return result, batch.CEStatusDeleted, nil + if diff.HasChange("compute_resources.0.security_group_ids") && !fargateComputeResources { + if err := diff.ForceNew("compute_resources.0.security_group_ids"); err != nil { + return err + } } - computeEnvironment := result.ComputeEnvironments[0] - return result, *(computeEnvironment.Status), nil + if diff.HasChange("compute_resources.0.subnets") && !fargateComputeResources { + if err := diff.ForceNew("compute_resources.0.subnets"); err != nil { + return err + } + } + } + + return nil +} + +func expandBatchComputeResource(tfMap map[string]interface{}) *batch.ComputeResource { + if tfMap == nil { + return nil + } + + var computeResourceType string + + if v, ok := tfMap["type"].(string); ok && v != "" { + computeResourceType = v + } + + apiObject := &batch.ComputeResource{} + + if v, ok := tfMap["allocation_strategy"].(string); ok && v != "" { + apiObject.AllocationStrategy = aws.String(v) + } + + if v, ok := tfMap["bid_percentage"].(int); ok && v != 0 { + apiObject.BidPercentage = aws.Int64(int64(v)) + } + + if v, ok := tfMap["desired_vcpus"].(int); ok && v != 0 { + apiObject.DesiredvCpus = aws.Int64(int64(v)) + } + + if v, ok := tfMap["ec2_key_pair"].(string); ok && v != "" { + apiObject.Ec2KeyPair = aws.String(v) + } + + if v, ok := tfMap["image_id"].(string); ok && v != "" { + apiObject.ImageId = aws.String(v) + } + + if v, ok := tfMap["instance_role"].(string); ok && v != "" { + apiObject.InstanceRole = aws.String(v) + } + + if v, ok := tfMap["instance_type"].(*schema.Set); ok && v.Len() > 0 { + apiObject.InstanceTypes = expandStringSet(v) + } + + if v, ok := tfMap["launch_template"].([]interface{}); ok && len(v) > 0 { + apiObject.LaunchTemplate = expandBatchLaunchTemplateSpecification(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["max_vcpus"].(int); ok && v != 0 { + apiObject.MaxvCpus = aws.Int64(int64(v)) + } + + if v, ok := tfMap["min_vcpus"].(int); ok && v != 0 { + apiObject.MinvCpus = aws.Int64(int64(v)) + } else if computeResourceType := strings.ToUpper(computeResourceType); computeResourceType == batch.CRTypeEc2 || computeResourceType == batch.CRTypeSpot { + apiObject.MinvCpus = aws.Int64(0) + } + + if v, ok := tfMap["security_group_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SecurityGroupIds = expandStringSet(v) + } + + if v, ok := tfMap["spot_iam_fleet_role"].(string); ok && v != "" { + apiObject.SpotIamFleetRole = aws.String(v) + } + + if v, ok := tfMap["subnets"].(*schema.Set); ok && v.Len() > 0 { + apiObject.Subnets = expandStringSet(v) + } + + if v, ok := tfMap["tags"].(map[string]interface{}); ok && len(v) > 0 { + apiObject.Tags = keyvaluetags.New(v).IgnoreAws().BatchTags() + } + + if computeResourceType != "" { + apiObject.Type = aws.String(computeResourceType) } + + return apiObject } -func deleteBatchComputeEnvironment(computeEnvironment string, timeout time.Duration, conn *batch.Batch) error { - input := &batch.DeleteComputeEnvironmentInput{ - ComputeEnvironment: aws.String(computeEnvironment), +func expandBatchLaunchTemplateSpecification(tfMap map[string]interface{}) *batch.LaunchTemplateSpecification { + if tfMap == nil { + return nil + } + + apiObject := &batch.LaunchTemplateSpecification{} + + if v, ok := tfMap["launch_template_id"].(string); ok && v != "" { + apiObject.LaunchTemplateId = aws.String(v) + } + + if v, ok := tfMap["launch_template_name"].(string); ok && v != "" { + apiObject.LaunchTemplateName = aws.String(v) + } + + if v, ok := tfMap["version"].(string); ok && v != "" { + apiObject.Version = aws.String(v) + } + + return apiObject +} + +func flattenBatchComputeResource(apiObject *batch.ComputeResource) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.AllocationStrategy; v != nil { + tfMap["allocation_strategy"] = aws.StringValue(v) + } + + if v := apiObject.BidPercentage; v != nil { + tfMap["bid_percentage"] = aws.Int64Value(v) + } + + if v := apiObject.DesiredvCpus; v != nil { + tfMap["desired_vcpus"] = aws.Int64Value(v) + } + + if v := apiObject.Ec2KeyPair; v != nil { + tfMap["ec2_key_pair"] = aws.StringValue(v) + } + + if v := apiObject.ImageId; v != nil { + tfMap["image_id"] = aws.StringValue(v) + } + + if v := apiObject.InstanceRole; v != nil { + tfMap["instance_role"] = aws.StringValue(v) } - if _, err := conn.DeleteComputeEnvironment(input); err != nil { - return err + if v := apiObject.InstanceTypes; v != nil { + tfMap["instance_type"] = aws.StringValueSlice(v) } - stateChangeConf := &resource.StateChangeConf{ - Pending: []string{batch.CEStatusDeleting}, - Target: []string{batch.CEStatusDeleted}, - Refresh: resourceAwsBatchComputeEnvironmentDeleteRefreshFunc(computeEnvironment, conn), - Timeout: timeout, - MinTimeout: 5 * time.Second, + if v := apiObject.LaunchTemplate; v != nil { + tfMap["launch_template"] = []interface{}{flattenBatchLaunchTemplateSpecification(v)} } - _, err := stateChangeConf.WaitForState() - return err + + if v := apiObject.MaxvCpus; v != nil { + tfMap["max_vcpus"] = aws.Int64Value(v) + } + + if v := apiObject.MinvCpus; v != nil { + tfMap["min_vcpus"] = aws.Int64Value(v) + } + + if v := apiObject.SecurityGroupIds; v != nil { + tfMap["security_group_ids"] = aws.StringValueSlice(v) + } + + if v := apiObject.SpotIamFleetRole; v != nil { + tfMap["spot_iam_fleet_role"] = aws.StringValue(v) + } + + if v := apiObject.Subnets; v != nil { + tfMap["subnets"] = aws.StringValueSlice(v) + } + + if v := apiObject.Tags; v != nil { + tfMap["tags"] = keyvaluetags.BatchKeyValueTags(v).IgnoreAws().Map() + } + + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) + } + + return tfMap } -func disableBatchComputeEnvironment(computeEnvironment string, timeout time.Duration, conn *batch.Batch) error { - input := &batch.UpdateComputeEnvironmentInput{ - ComputeEnvironment: aws.String(computeEnvironment), - State: aws.String(batch.CEStateDisabled), +func flattenBatchLaunchTemplateSpecification(apiObject *batch.LaunchTemplateSpecification) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.LaunchTemplateId; v != nil { + tfMap["launch_template_id"] = aws.StringValue(v) } - if _, err := conn.UpdateComputeEnvironment(input); err != nil { - return err + if v := apiObject.LaunchTemplateName; v != nil { + tfMap["launch_template_name"] = aws.StringValue(v) } - stateChangeConf := &resource.StateChangeConf{ - Pending: []string{batch.CEStatusUpdating}, - Target: []string{batch.CEStatusValid}, - Refresh: resourceAwsBatchComputeEnvironmentStatusRefreshFunc(computeEnvironment, conn), - Timeout: timeout, - MinTimeout: 5 * time.Second, + if v := apiObject.Version; v != nil { + tfMap["version"] = aws.StringValue(v) } - _, err := stateChangeConf.WaitForState() - return err + + return tfMap } diff --git a/aws/resource_aws_batch_compute_environment_test.go b/aws/resource_aws_batch_compute_environment_test.go index 7c2aeb156c76..36da0f7bb931 100644 --- a/aws/resource_aws_batch_compute_environment_test.go +++ b/aws/resource_aws_batch_compute_environment_test.go @@ -15,6 +15,9 @@ import ( "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/terraform-providers/terraform-provider-aws/aws/internal/naming" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/batch/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -151,82 +154,107 @@ func testSweepBatchComputeEnvironments(region string) error { return sweeperErrs.ErrorOrNil() } -func TestAccAWSBatchComputeEnvironment_disappears(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_basic(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + serviceRoleResourceName := "aws_iam_role.batch_service" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigBasic(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - testAccCheckResourceDisappears(testAccProvider, resourceAwsBatchComputeEnvironment(), resourceName), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "UNMANAGED"), ), - ExpectNonEmptyPlan: true, }, }, }) } -func TestAccAWSBatchComputeEnvironment_createEc2(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_disappears(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigBasic(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBatchComputeEnvironment(), resourceName), ), + ExpectNonEmptyPlan: true, }, }, }) } -func TestAccAWSBatchComputeEnvironment_createWithNamePrefix(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_NameGenerated(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigNamePrefix(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigNameGenerated(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestMatchResourceAttr( - "aws_batch_compute_environment.ec2", "compute_environment_name", regexp.MustCompile("^tf_acc_test"), - ), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + naming.TestCheckResourceAttrNameGenerated(resourceName, "compute_environment_name"), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", "terraform-"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccAWSBatchComputeEnvironment_createEc2WithTags(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_NamePrefix(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2WithTags(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigNamePrefix(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.Key1", "Value1"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + naming.TestCheckResourceAttrNameFromPrefix(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", rName), ), }, { @@ -238,67 +266,181 @@ func TestAccAWSBatchComputeEnvironment_createEc2WithTags(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_createSpot(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_createEc2(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigSpot(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigEC2(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccAWSBatchComputeEnvironment_createUnmanaged(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_createEc2_DesiredVcpus_Ec2KeyPair_ImageId_ComputeResourcesTags(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + resourceName := "aws_batch_compute_environment.test" + amiDatasourceName := "data.aws_ami.amzn-ami-minimal-hvm-ebs" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + keyPairResourceName := "aws_key_pair.test" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + publicKey, _, err := acctest.RandSSHKeyPair(testAccDefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigUnmanaged(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigEC2WithDesiredVcpusEc2KeyPairImageIdAndComputeResourcesTags(rName, publicKey), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "8"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.ec2_key_pair", keyPairResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.image_id", amiDatasourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccAWSBatchComputeEnvironment_ComputeResources_DesiredVcpus_Computed(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_createSpot(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + spotFleetRoleResourceName := "aws_iam_role.ec2_spot_fleet" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 8, 4), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 4, 2), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 8, 8), + Config: testAccAWSBatchComputeEnvironmentConfigSpot(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "2"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "2"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.spot_iam_fleet_role", spotFleetRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -310,34 +452,55 @@ func TestAccAWSBatchComputeEnvironment_ComputeResources_DesiredVcpus_Computed(t }) } -func TestAccAWSBatchComputeEnvironment_ComputeResources_MinVcpus(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_createSpot_AllocationStrategy_BidPercentage(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + spotFleetRoleResourceName := "aws_iam_role.ec2_spot_fleet" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 4, 0), + Config: testAccAWSBatchComputeEnvironmentConfigSpotWithAllocationStrategyAndBidPercentage(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", "BEST_FIT"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "60"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 4, 4), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "4"), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 4, 2), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "2"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.spot_iam_fleet_role", spotFleetRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -349,34 +512,52 @@ func TestAccAWSBatchComputeEnvironment_ComputeResources_MinVcpus(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_ComputeResources_MaxVcpus(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_createFargate(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 4, 0), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "4"), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 8, 0), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "8"), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rInt, 2, 0), + Config: testAccAWSBatchComputeEnvironmentConfigFargate(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "2"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -388,27 +569,52 @@ func TestAccAWSBatchComputeEnvironment_ComputeResources_MaxVcpus(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_updateInstanceType(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_createFargateSpot(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2(rInt), - Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), - ), - }, - { - Config: testAccAWSBatchComputeEnvironmentConfigEC2UpdateInstanceType(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigFargateSpot(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "2"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE_SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -420,29 +626,50 @@ func TestAccAWSBatchComputeEnvironment_updateInstanceType(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_updateComputeEnvironmentName(t *testing.T) { - rInt := acctest.RandInt() - expectedName := fmt.Sprintf("tf_acc_test_%d", rInt) - expectedUpdatedName := fmt.Sprintf("tf_acc_test_updated_%d", rInt) - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_updateState(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + serviceRoleResourceName := "aws_iam_role.batch_service" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigState(rName, "ENABLED"), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_environment_name", expectedName), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "UNMANAGED"), ), }, { - Config: testAccAWSBatchComputeEnvironmentConfigEC2UpdateComputeEnvironmentName(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigState(rName, "disabled"), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_environment_name", expectedUpdatedName), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "DISABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "UNMANAGED"), ), }, { @@ -454,60 +681,276 @@ func TestAccAWSBatchComputeEnvironment_updateComputeEnvironmentName(t *testing.T }) } -func TestAccAWSBatchComputeEnvironment_createEc2WithoutComputeResources(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_updateServiceRole(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + serviceRoleResourceName1 := "aws_iam_role.batch_service" + serviceRoleResourceName2 := "aws_iam_role.batch_service_2" + securityGroupResourceName := "aws_security_group.test" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2WithoutComputeResources(rInt), - ExpectError: regexp.MustCompile(`One compute environment is expected, but no compute environments are set`), + Config: testAccAWSBatchComputeEnvironmentConfigFargate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName1, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + Config: testAccAWSBatchComputeEnvironmentConfigFargateUpdatedServiceRole(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName2, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) } -func TestAccAWSBatchComputeEnvironment_createUnmanagedWithComputeResources(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_defaultServiceRole(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + securityGroupResourceName := "aws_security_group.test" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAWSBatch(t) + testAccPreCheckIamServiceLinkedRole(t, "/aws-service-role/batch") + }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigUnmanagedWithComputeResources(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigFargateDefaultServiceRole(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr("aws_batch_compute_environment.unmanaged", "type", "UNMANAGED"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + testAccMatchResourceAttrGlobalARN(resourceName, "service_role", "iam", regexp.MustCompile(`role/aws-service-role/batch`)), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccAWSBatchComputeEnvironment_launchTemplate(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_ComputeResources_MinVcpus(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigLaunchTemplate(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 4, 0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 4, 4), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 4, 2), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, - "compute_resources.0.launch_template.#", - "1"), - resource.TestCheckResourceAttr(resourceName, - "compute_resources.0.launch_template.0.launch_template_name", - fmt.Sprintf("tf_acc_test_%d", rInt)), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "2"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -519,27 +962,124 @@ func TestAccAWSBatchComputeEnvironment_launchTemplate(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_UpdateLaunchTemplate(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_ComputeResources_MaxVcpus(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentUpdateLaunchTemplateInExistingComputeEnvironment(rInt, "$Default"), + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 4, 0), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.version", "$Default"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "4"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { - Config: testAccAWSBatchComputeEnvironmentUpdateLaunchTemplateInExistingComputeEnvironment(rInt, "$Latest"), + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 8, 0), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.version", "$Latest"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "8"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + Config: testAccAWSBatchComputeEnvironmentConfigComputeResourcesMaxVcpusMinVcpus(rName, 2, 0), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "optimal"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "2"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "EC2"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -551,62 +1091,256 @@ func TestAccAWSBatchComputeEnvironment_UpdateLaunchTemplate(t *testing.T) { }) } -func TestAccAWSBatchComputeEnvironment_createSpotWithAllocationStrategy(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_launchTemplate(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + launchTemplateResourceName := "aws_launch_template.test" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + spotFleetRoleResourceName := "aws_iam_role.ec2_spot_fleet" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigSpotWithAllocationStrategy(rInt), + Config: testAccAWSBatchComputeEnvironmentConfigLaunchTemplate(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr("aws_batch_compute_environment.ec2", "compute_resources.0.allocation_strategy", "BEST_FIT"), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.launch_template_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.launch_template.0.launch_template_name", launchTemplateResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.version", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.spot_iam_fleet_role", spotFleetRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } -func TestAccAWSBatchComputeEnvironment_createSpotWithoutBidPercentage(t *testing.T) { - rInt := acctest.RandInt() +func TestAccAWSBatchComputeEnvironment_UpdateLaunchTemplate(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + instanceProfileResourceName := "aws_iam_instance_profile.ecs_instance" + launchTemplateResourceName := "aws_launch_template.test" + securityGroupResourceName := "aws_security_group.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + spotFleetRoleResourceName := "aws_iam_role.ec2_spot_fleet" + subnetResourceName := "aws_subnet.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigSpotWithoutBidPercentage(rInt), - ExpectError: regexp.MustCompile(`ComputeResources.spotIamFleetRole cannot not be null or empty`), + Config: testAccAWSBatchComputeEnvironmentUpdateLaunchTemplateInExistingComputeEnvironment(rName, "$Default"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.launch_template.0.launch_template_id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.launch_template_name", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.version", "$Default"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.spot_iam_fleet_role", spotFleetRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + Config: testAccAWSBatchComputeEnvironmentUpdateLaunchTemplateInExistingComputeEnvironment(rName, "$Latest"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.instance_role", instanceProfileResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "compute_resources.0.instance_type.*", "c4.large"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.launch_template.0.launch_template_id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.launch_template_name", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.0.version", "$Latest"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "compute_resources.0.spot_iam_fleet_role", spotFleetRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "SPOT"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) } -func TestAccAWSBatchComputeEnvironment_updateState(t *testing.T) { - rInt := acctest.RandInt() - resourceName := "aws_batch_compute_environment.ec2" +func TestAccAWSBatchComputeEnvironment_UpdateSecurityGroupsAndSubnets_Fargate(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + securityGroupResourceName1 := "aws_security_group.test" + securityGroupResourceName2 := "aws_security_group.test_2" + securityGroupResourceName3 := "aws_security_group.test_3" + serviceRoleResourceName := "aws_iam_role.batch_service" + subnetResourceName1 := "aws_subnet.test" + subnetResourceName2 := "aws_subnet.test_2" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigEC2UpdateState(rInt, batch.CEStateEnabled), + Config: testAccAWSBatchComputeEnvironmentConfigFargate(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "state", batch.CEStateEnabled), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName1, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName1, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { - Config: testAccAWSBatchComputeEnvironmentConfigEC2UpdateState(rInt, batch.CEStateDisabled), + Config: testAccAWSBatchComputeEnvironmentConfigFargateUpdatedSecurityGroupsAndSubnets(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), - resource.TestCheckResourceAttr(resourceName, "state", batch.CEStateDisabled), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.allocation_strategy", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.bid_percentage", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.desired_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.ec2_key_pair", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.image_id", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.instance_type.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.launch_template.#", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.max_vcpus", "16"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.min_vcpus", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.security_group_ids.#", "2"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName2, "id"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.security_group_ids.*", securityGroupResourceName3, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.spot_iam_fleet_role", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.subnets.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "compute_resources.0.subnets.*", subnetResourceName2, "id"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "compute_resources.0.type", "FARGATE"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "MANAGED"), ), }, { @@ -619,18 +1353,20 @@ func TestAccAWSBatchComputeEnvironment_updateState(t *testing.T) { } func TestAccAWSBatchComputeEnvironment_Tags(t *testing.T) { - rInt := acctest.RandInt() + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_batch_compute_environment.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSBatchComputeEnvironmentConfigTags1(rInt, "key1", "value1"), + Config: testAccAWSBatchComputeEnvironmentConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), @@ -641,18 +1377,18 @@ func TestAccAWSBatchComputeEnvironment_Tags(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSBatchComputeEnvironmentConfigTags2(rInt, "key1", "value1updated", "key2", "value2"), + Config: testAccAWSBatchComputeEnvironmentConfigTags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), }, { - Config: testAccAWSBatchComputeEnvironmentConfigTags1(rInt, "key2", "value2"), + Config: testAccAWSBatchComputeEnvironmentConfigTags1(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckAwsBatchComputeEnvironmentExists(), + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), @@ -661,6 +1397,76 @@ func TestAccAWSBatchComputeEnvironment_Tags(t *testing.T) { }) } +func TestAccAWSBatchComputeEnvironment_createUnmanagedWithComputeResources(t *testing.T) { + var ce batch.ComputeEnvironmentDetail + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_compute_environment.test" + serviceRoleResourceName := "aws_iam_role.batch_service" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBatchComputeEnvironmentConfigUnmanagedWithComputeResources(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBatchComputeEnvironmentExists(resourceName, &ce), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "batch", fmt.Sprintf("compute-environment/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name", rName), + resource.TestCheckResourceAttr(resourceName, "compute_environment_name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "compute_resources.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "ecs_cluster_arn"), + resource.TestCheckResourceAttrPair(resourceName, "service_role", serviceRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "status_reason"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "UNMANAGED"), + ), + }, + // Can't import in this scenario. + }, + }) +} + +// Test plan time errors... + +func TestAccAWSBatchComputeEnvironment_createEc2WithoutComputeResources(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBatchComputeEnvironmentConfigEC2WithoutComputeResources(rName), + ExpectError: regexp.MustCompile(`computeResources must be provided for a MANAGED compute environment`), + }, + }, + }) +} + +func TestAccAWSBatchComputeEnvironment_createSpotWithoutIamFleetRole(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchComputeEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBatchComputeEnvironmentConfigSpotWithoutIamFleetRole(rName), + ExpectError: regexp.MustCompile(`ComputeResources.spotIamFleetRole cannot not be null or empty`), + }, + }, + }) +} + func testAccCheckBatchComputeEnvironmentDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).batchconn @@ -669,49 +1475,42 @@ func testAccCheckBatchComputeEnvironmentDestroy(s *terraform.State) error { continue } - result, err := conn.DescribeComputeEnvironments(&batch.DescribeComputeEnvironmentsInput{ - ComputeEnvironments: []*string{ - aws.String(rs.Primary.ID), - }, - }) + _, err := finder.ComputeEnvironmentDetailByName(conn, rs.Primary.ID) - if err != nil { - return fmt.Errorf("Error occurred when get compute environment information.") + if tfresource.NotFound(err) { + continue } - if len(result.ComputeEnvironments) == 1 { - return fmt.Errorf("Compute environment still exists.") + + if err != nil { + return err } + return fmt.Errorf("Batch Compute Environment %s still exists", rs.Primary.ID) } - return nil } -func testAccCheckAwsBatchComputeEnvironmentExists() resource.TestCheckFunc { +func testAccCheckAwsBatchComputeEnvironmentExists(n string, v *batch.ComputeEnvironmentDetail) resource.TestCheckFunc { return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).batchconn + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_batch_compute_environment" { - continue - } + if rs.Primary.ID == "" { + return fmt.Errorf("No Batch Compute Environment ID is set") + } - result, err := conn.DescribeComputeEnvironments(&batch.DescribeComputeEnvironmentsInput{ - ComputeEnvironments: []*string{ - aws.String(rs.Primary.ID), - }, - }) + conn := testAccProvider.Meta().(*AWSClient).batchconn - if err != nil { - return fmt.Errorf("Error occurred when get compute environment information.") - } - if len(result.ComputeEnvironments) == 0 { - return fmt.Errorf("Compute environment doesn't exists.") - } else if len(result.ComputeEnvironments) >= 2 { - return fmt.Errorf("Too many compute environments exist.") - } + computeEnvironment, err := finder.ComputeEnvironmentDetailByName(conn, rs.Primary.ID) + + if err != nil { + return err } + *v = *computeEnvironment + return nil } } @@ -732,559 +1531,684 @@ func testAccPreCheckAWSBatch(t *testing.T) { } } -func testAccAWSBatchComputeEnvironmentConfigBase(rInt int) string { +func testAccAWSBatchComputeEnvironmentConfigBase(rName string) string { return fmt.Sprintf(` -data "aws_partition" "current" { -} +data "aws_partition" "current" {} -########## ecs_instance_role ########## - -resource "aws_iam_role" "ecs_instance_role" { - name = "tf_acc_test_batch_inst_role_%d" +resource "aws_iam_role" "ecs_instance" { + name = "%[1]s_ecs_instance" assume_role_policy = < 0 { + input.PlatformCapabilities = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("retry_strategy"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.RetryStrategy = expandBatchRetryStrategy(v.([]interface{})[0].(map[string]interface{})) } - if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - input.Tags = keyvaluetags.New(v).IgnoreAws().BatchTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().BatchTags() } - if v, ok := d.GetOk("timeout"); ok { - input.Timeout = expandJobDefinitionTimeout(v.([]interface{})) + if v, ok := d.GetOk("timeout"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Timeout = expandBatchJobTimeout(v.([]interface{})[0].(map[string]interface{})) } - out, err := conn.RegisterJobDefinition(input) + output, err := conn.RegisterJobDefinition(input) + if err != nil { - return fmt.Errorf("%s %q", err, name) + return fmt.Errorf("error creating Batch Job Definition (%s): %w", name, err) } - d.SetId(aws.StringValue(out.JobDefinitionArn)) - d.Set("arn", out.JobDefinitionArn) + + d.SetId(aws.StringValue(output.JobDefinitionArn)) + return resourceAwsBatchJobDefinitionRead(d, meta) } func resourceAwsBatchJobDefinitionRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - arn := d.Get("arn").(string) - job, err := getJobDefinition(conn, arn) - if err != nil { - return fmt.Errorf("%s %q", err, arn) - } - if job == nil { + jobDefinition, err := finder.JobDefinitionByARN(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Batch Job Definition (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - d.Set("arn", job.JobDefinitionArn) - containerProperties, err := flattenBatchContainerProperties(job.ContainerProperties) + if err != nil { + return fmt.Errorf("error reading Batch Job Definition (%s): %w", d.Id(), err) + } + + d.Set("arn", jobDefinition.JobDefinitionArn) + + containerProperties, err := flattenBatchContainerProperties(jobDefinition.ContainerProperties) if err != nil { - return fmt.Errorf("error converting Batch Container Properties to JSON: %s", err) + return fmt.Errorf("error converting Batch Container Properties to JSON: %w", err) } if err := d.Set("container_properties", containerProperties); err != nil { - return fmt.Errorf("error setting container_properties: %s", err) + return fmt.Errorf("error setting container_properties: %w", err) } - d.Set("name", job.JobDefinitionName) + d.Set("name", jobDefinition.JobDefinitionName) + d.Set("parameters", aws.StringValueMap(jobDefinition.Parameters)) + d.Set("platform_capabilities", aws.StringValueSlice(jobDefinition.PlatformCapabilities)) + d.Set("propagate_tags", jobDefinition.PropagateTags) - d.Set("parameters", aws.StringValueMap(job.Parameters)) + if jobDefinition.RetryStrategy != nil { + if err := d.Set("retry_strategy", []interface{}{flattenBatchRetryStrategy(jobDefinition.RetryStrategy)}); err != nil { + return fmt.Errorf("error setting retry_strategy: %w", err) + } + } else { + d.Set("retry_strategy", nil) + } + + tags := keyvaluetags.BatchKeyValueTags(jobDefinition.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) - if err := d.Set("retry_strategy", flattenBatchRetryStrategy(job.RetryStrategy)); err != nil { - return fmt.Errorf("error setting retry_strategy: %s", err) + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - if err := d.Set("tags", keyvaluetags.BatchKeyValueTags(job.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } - if err := d.Set("timeout", flattenBatchJobTimeout(job.Timeout)); err != nil { - return fmt.Errorf("error setting timeout: %s", err) + if jobDefinition.Timeout != nil { + if err := d.Set("timeout", []interface{}{flattenBatchJobTimeout(jobDefinition.Timeout)}); err != nil { + return fmt.Errorf("error setting timeout: %w", err) + } + } else { + d.Set("timeout", nil) } - d.Set("revision", job.Revision) - d.Set("type", job.Type) + d.Set("revision", jobDefinition.Revision) + d.Set("type", jobDefinition.Type) + return nil } func resourceAwsBatchJobDefinitionUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") - if err := keyvaluetags.BatchUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) + if err := keyvaluetags.BatchUpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) } } @@ -211,39 +308,16 @@ func resourceAwsBatchJobDefinitionUpdate(d *schema.ResourceData, meta interface{ func resourceAwsBatchJobDefinitionDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn - arn := d.Get("arn").(string) + _, err := conn.DeregisterJobDefinition(&batch.DeregisterJobDefinitionInput{ - JobDefinition: aws.String(arn), + JobDefinition: aws.String(d.Id()), }) - if err != nil { - return fmt.Errorf("%s %q", err, arn) - } - - return nil -} -func getJobDefinition(conn *batch.Batch, arn string) (*batch.JobDefinition, error) { - describeOpts := &batch.DescribeJobDefinitionsInput{ - JobDefinitions: []*string{aws.String(arn)}, - } - resp, err := conn.DescribeJobDefinitions(describeOpts) if err != nil { - return nil, err + return fmt.Errorf("error deleting Batch Job Definition (%s): %w", d.Id(), err) } - numJobDefinitions := len(resp.JobDefinitions) - switch { - case numJobDefinitions == 0: - return nil, nil - case numJobDefinitions == 1: - if *resp.JobDefinitions[0].Status == "ACTIVE" { - return resp.JobDefinitions[0], nil - } - return nil, nil - case numJobDefinitions > 1: - return nil, fmt.Errorf("Multiple Job Definitions with name %s", arn) - } - return nil, nil + return nil } func validateAwsBatchJobContainerProperties(v interface{}, k string) (ws []string, errors []error) { @@ -286,44 +360,162 @@ func expandJobDefinitionParameters(params map[string]interface{}) map[string]*st return jobParams } -func expandJobDefinitionRetryStrategy(item []interface{}) *batch.RetryStrategy { - retryStrategy := &batch.RetryStrategy{} - data := item[0].(map[string]interface{}) +func expandBatchRetryStrategy(tfMap map[string]interface{}) *batch.RetryStrategy { + if tfMap == nil { + return nil + } + + apiObject := &batch.RetryStrategy{} - if v, ok := data["attempts"].(int); ok && v > 0 && v <= 10 { - retryStrategy.Attempts = aws.Int64(int64(v)) + if v, ok := tfMap["attempts"].(int); ok && v != 0 { + apiObject.Attempts = aws.Int64(int64(v)) } - return retryStrategy + if v, ok := tfMap["evaluate_on_exit"].([]interface{}); ok && len(v) > 0 { + apiObject.EvaluateOnExit = expandBatchEvaluateOnExits(v) + } + + return apiObject } -func flattenBatchRetryStrategy(item *batch.RetryStrategy) []map[string]interface{} { - data := []map[string]interface{}{} - if item != nil && item.Attempts != nil { - data = append(data, map[string]interface{}{ - "attempts": int(aws.Int64Value(item.Attempts)), - }) +func expandBatchEvaluateOnExit(tfMap map[string]interface{}) *batch.EvaluateOnExit { + if tfMap == nil { + return nil + } + + apiObject := &batch.EvaluateOnExit{} + + if v, ok := tfMap["action"].(string); ok && v != "" { + apiObject.Action = aws.String(v) + } + + if v, ok := tfMap["on_exit_code"].(string); ok && v != "" { + apiObject.OnExitCode = aws.String(v) + } + + if v, ok := tfMap["on_reason"].(string); ok && v != "" { + apiObject.OnReason = aws.String(v) } - return data + + if v, ok := tfMap["on_status_reason"].(string); ok && v != "" { + apiObject.OnStatusReason = aws.String(v) + } + + return apiObject } -func expandJobDefinitionTimeout(item []interface{}) *batch.JobTimeout { - timeout := &batch.JobTimeout{} - data := item[0].(map[string]interface{}) +func expandBatchEvaluateOnExits(tfList []interface{}) []*batch.EvaluateOnExit { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*batch.EvaluateOnExit + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandBatchEvaluateOnExit(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenBatchRetryStrategy(apiObject *batch.RetryStrategy) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Attempts; v != nil { + tfMap["attempts"] = aws.Int64Value(v) + } - if v, ok := data["attempt_duration_seconds"].(int); ok && v >= 60 { - timeout.AttemptDurationSeconds = aws.Int64(int64(v)) + if v := apiObject.EvaluateOnExit; v != nil { + tfMap["evaluate_on_exit"] = flattenBatchEvaluateOnExits(v) } - return timeout + return tfMap } -func flattenBatchJobTimeout(item *batch.JobTimeout) []map[string]interface{} { - data := []map[string]interface{}{} - if item != nil && item.AttemptDurationSeconds != nil { - data = append(data, map[string]interface{}{ - "attempt_duration_seconds": int(aws.Int64Value(item.AttemptDurationSeconds)), - }) +func flattenBatchEvaluateOnExit(apiObject *batch.EvaluateOnExit) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Action; v != nil { + tfMap["action"] = aws.StringValue(v) + } + + if v := apiObject.OnExitCode; v != nil { + tfMap["on_exit_code"] = aws.StringValue(v) + } + + if v := apiObject.OnReason; v != nil { + tfMap["on_reason"] = aws.StringValue(v) + } + + if v := apiObject.OnStatusReason; v != nil { + tfMap["on_status_reason"] = aws.StringValue(v) } - return data + + return tfMap +} + +func flattenBatchEvaluateOnExits(apiObjects []*batch.EvaluateOnExit) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenBatchEvaluateOnExit(apiObject)) + } + + return tfList +} + +func expandBatchJobTimeout(tfMap map[string]interface{}) *batch.JobTimeout { + if tfMap == nil { + return nil + } + + apiObject := &batch.JobTimeout{} + + if v, ok := tfMap["attempt_duration_seconds"].(int); ok && v != 0 { + apiObject.AttemptDurationSeconds = aws.Int64(int64(v)) + } + + return apiObject +} + +func flattenBatchJobTimeout(apiObject *batch.JobTimeout) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.AttemptDurationSeconds; v != nil { + tfMap["attempt_duration_seconds"] = aws.Int64Value(v) + } + + return tfMap } diff --git a/aws/resource_aws_batch_job_definition_test.go b/aws/resource_aws_batch_job_definition_test.go index 4ee8e200d9f6..39338af78989 100644 --- a/aws/resource_aws_batch_job_definition_test.go +++ b/aws/resource_aws_batch_job_definition_test.go @@ -4,6 +4,8 @@ import ( "fmt" "log" "reflect" + "regexp" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -12,6 +14,8 @@ import ( "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/terraform-providers/terraform-provider-aws/aws/internal/service/batch/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -35,9 +39,9 @@ func testSweepBatchJobDefinitions(region string) error { } var sweeperErrs *multierror.Error - err = conn.DescribeJobDefinitionsPages(input, func(page *batch.DescribeJobDefinitionsOutput, isLast bool) bool { + err = conn.DescribeJobDefinitionsPages(input, func(page *batch.DescribeJobDefinitionsOutput, lastPage bool) bool { if page == nil { - return !isLast + return !lastPage } for _, jobDefinition := range page.JobDefinitions { @@ -55,7 +59,7 @@ func testSweepBatchJobDefinitions(region string) error { } } - return !isLast + return !lastPage }) if testSweepSkipSweepError(err) { log.Printf("[WARN] Skipping Batch Job Definitions sweep for %s: %s", region, err) @@ -75,6 +79,7 @@ func TestAccAWSBatchJobDefinition_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobDefinitionDestroy, Steps: []resource.TestStep{ @@ -82,7 +87,154 @@ func TestAccAWSBatchJobDefinition_basic(t *testing.T) { Config: testAccBatchJobDefinitionConfigName(rName), Check: resource.ComposeTestCheckFunc( testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "batch", regexp.MustCompile(fmt.Sprintf(`job-definition/%s:\d+`, rName))), + resource.TestCheckResourceAttrSet(resourceName, "container_properties"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "platform_capabilities.#", "0"), + resource.TestCheckResourceAttr(resourceName, "propagate_tags", "false"), + resource.TestCheckResourceAttr(resourceName, "retry_strategy.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "revision"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "timeout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "container"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSBatchJobDefinition_disappears(t *testing.T) { + var jd batch.JobDefinition + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_job_definition.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchJobDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBatchJobDefinitionConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBatchJobDefinition(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSBatchJobDefinition_PlatformCapabilities_EC2(t *testing.T) { + var jd batch.JobDefinition + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_job_definition.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchJobDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBatchJobDefinitionConfigCapabilitiesEC2(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "batch", regexp.MustCompile(fmt.Sprintf(`job-definition/%s:\d+`, rName))), + resource.TestCheckResourceAttrSet(resourceName, "container_properties"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "platform_capabilities.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "platform_capabilities.*", "EC2"), + resource.TestCheckResourceAttr(resourceName, "propagate_tags", "false"), + resource.TestCheckResourceAttr(resourceName, "retry_strategy.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "revision"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "timeout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "container"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSBatchJobDefinition_PlatformCapabilities_Fargate_ContainerPropertiesDefaults(t *testing.T) { + var jd batch.JobDefinition + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_job_definition.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchJobDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBatchJobDefinitionConfigCapabilitiesFargateContainerPropertiesDefaults(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "batch", regexp.MustCompile(fmt.Sprintf(`job-definition/%s:\d+`, rName))), + resource.TestCheckResourceAttrSet(resourceName, "container_properties"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "platform_capabilities.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "platform_capabilities.*", "FARGATE"), + resource.TestCheckResourceAttr(resourceName, "propagate_tags", "false"), + resource.TestCheckResourceAttr(resourceName, "retry_strategy.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "revision"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "timeout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "container"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSBatchJobDefinition_PlatformCapabilities_Fargate(t *testing.T) { + var jd batch.JobDefinition + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_job_definition.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchJobDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBatchJobDefinitionConfigCapabilitiesFargate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "batch", regexp.MustCompile(fmt.Sprintf(`job-definition/%s:\d+`, rName))), + resource.TestCheckResourceAttrSet(resourceName, "container_properties"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "platform_capabilities.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "platform_capabilities.*", "FARGATE"), + resource.TestCheckResourceAttr(resourceName, "propagate_tags", "false"), + resource.TestCheckResourceAttr(resourceName, "retry_strategy.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "revision"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "timeout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "container"), ), }, { @@ -103,6 +255,10 @@ func TestAccAWSBatchJobDefinition_ContainerProperties_Advanced(t *testing.T) { }, RetryStrategy: &batch.RetryStrategy{ Attempts: aws.Int64(int64(1)), + EvaluateOnExit: []*batch.EvaluateOnExit{ + {Action: aws.String(strings.ToLower(batch.RetryActionRetry)), OnStatusReason: aws.String("Host EC2*")}, + {Action: aws.String(strings.ToLower(batch.RetryActionExit)), OnReason: aws.String("*")}, + }, }, Timeout: &batch.JobTimeout{ AttemptDurationSeconds: aws.Int64(int64(60)), @@ -136,6 +292,7 @@ func TestAccAWSBatchJobDefinition_ContainerProperties_Advanced(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobDefinitionDestroy, Steps: []resource.TestStep{ @@ -162,6 +319,7 @@ func TestAccAWSBatchJobDefinition_updateForcesNewResource(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobDefinitionDestroy, Steps: []resource.TestStep{ @@ -195,6 +353,7 @@ func TestAccAWSBatchJobDefinition_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobDefinitionDestroy, Steps: []resource.TestStep{ @@ -232,6 +391,38 @@ func TestAccAWSBatchJobDefinition_Tags(t *testing.T) { }) } +func TestAccAWSBatchJobDefinition_PropagateTags(t *testing.T) { + var jd batch.JobDefinition + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_batch_job_definition.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckBatchJobDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBatchJobDefinitionPropagateTags(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBatchJobDefinitionExists(resourceName, &jd), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "batch", regexp.MustCompile(fmt.Sprintf(`job-definition/%s:\d+`, rName))), + resource.TestCheckResourceAttrSet(resourceName, "container_properties"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "platform_capabilities.#", "0"), + resource.TestCheckResourceAttr(resourceName, "propagate_tags", "true"), + resource.TestCheckResourceAttr(resourceName, "retry_strategy.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "revision"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "timeout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "container"), + ), + }, + }, + }) +} + func testAccCheckBatchJobDefinitionExists(n string, jd *batch.JobDefinition) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -244,15 +435,14 @@ func testAccCheckBatchJobDefinitionExists(n string, jd *batch.JobDefinition) res } conn := testAccProvider.Meta().(*AWSClient).batchconn - arn := rs.Primary.Attributes["arn"] - def, err := getJobDefinition(conn, arn) + + jobDefinition, err := finder.JobDefinitionByARN(conn, rs.Primary.ID) + if err != nil { return err } - if def == nil { - return fmt.Errorf("Not found: %s", n) - } - *jd = *def + + *jd = *jobDefinition return nil } @@ -264,15 +454,18 @@ func testAccCheckBatchJobDefinitionAttributes(jd *batch.JobDefinition, compare * if rs.Type != "aws_batch_job_definition" { continue } - if *jd.JobDefinitionArn != rs.Primary.Attributes["arn"] { - return fmt.Errorf("Bad Job Definition ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], *jd.JobDefinitionArn) + if aws.StringValue(jd.JobDefinitionArn) != rs.Primary.Attributes["arn"] { + return fmt.Errorf("Bad Job Definition ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], aws.StringValue(jd.JobDefinitionArn)) } if compare != nil { if compare.Parameters != nil && !reflect.DeepEqual(compare.Parameters, jd.Parameters) { return fmt.Errorf("Bad Job Definition Params\n\t expected: %v\n\tgot: %v\n", compare.Parameters, jd.Parameters) } - if compare.RetryStrategy != nil && *compare.RetryStrategy.Attempts != *jd.RetryStrategy.Attempts { - return fmt.Errorf("Bad Job Definition Retry Strategy\n\t expected: %d\n\tgot: %d\n", *compare.RetryStrategy.Attempts, *jd.RetryStrategy.Attempts) + if compare.RetryStrategy != nil && aws.Int64Value(compare.RetryStrategy.Attempts) != aws.Int64Value(jd.RetryStrategy.Attempts) { + return fmt.Errorf("Bad Job Definition Retry Strategy\n\t expected: %d\n\tgot: %d\n", aws.Int64Value(compare.RetryStrategy.Attempts), aws.Int64Value(jd.RetryStrategy.Attempts)) + } + if compare.RetryStrategy != nil && !reflect.DeepEqual(compare.RetryStrategy.EvaluateOnExit, jd.RetryStrategy.EvaluateOnExit) { + return fmt.Errorf("Bad Job Definition Retry Strategy\n\t expected: %v\n\tgot: %v\n", compare.RetryStrategy.EvaluateOnExit, jd.RetryStrategy.EvaluateOnExit) } if compare.ContainerProperties != nil && compare.ContainerProperties.Command != nil && !reflect.DeepEqual(compare.ContainerProperties, jd.ContainerProperties) { return fmt.Errorf("Bad Job Definition Container Properties\n\t expected: %s\n\tgot: %s\n", compare.ContainerProperties, jd.ContainerProperties) @@ -283,29 +476,34 @@ func testAccCheckBatchJobDefinitionAttributes(jd *batch.JobDefinition, compare * } } -func testAccCheckJobDefinitionRecreated(t *testing.T, - before, after *batch.JobDefinition) resource.TestCheckFunc { +func testAccCheckJobDefinitionRecreated(t *testing.T, before, after *batch.JobDefinition) resource.TestCheckFunc { return func(s *terraform.State) error { - if *before.Revision == *after.Revision { - t.Fatalf("Expected change of JobDefinition Revisions, but both were %v", before.Revision) + if aws.Int64Value(before.Revision) == aws.Int64Value(after.Revision) { + t.Fatalf("Expected change of JobDefinition Revisions, but both were %d", aws.Int64Value(before.Revision)) } return nil } } func testAccCheckBatchJobDefinitionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).batchconn + for _, rs := range s.RootModule().Resources { if rs.Type != "aws_batch_job_definition" { continue } - conn := testAccProvider.Meta().(*AWSClient).batchconn - js, err := getJobDefinition(conn, rs.Primary.Attributes["arn"]) - if err == nil && js != nil { - if *js.Status == "ACTIVE" { - return fmt.Errorf("Error: Job Definition still active") - } + + _, err := finder.JobDefinitionByARN(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue } - return nil + + if err != nil { + return err + } + + return fmt.Errorf("Batch Job Definition %s still exists", rs.Primary.ID) } return nil } @@ -321,17 +519,25 @@ resource "aws_batch_job_definition" "test" { } retry_strategy { attempts = 1 + evaluate_on_exit { + action = "RETRY" + on_status_reason = "Host EC2*" + } + evaluate_on_exit { + action = "exit" + on_reason = "*" + } } timeout { attempt_duration_seconds = 60 } container_properties = < 0 { - input.Tags = keyvaluetags.New(v).IgnoreAws().BatchTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().BatchTags() } name := d.Get("name").(string) @@ -91,7 +97,7 @@ func resourceAwsBatchJobQueueCreate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Error waiting for JobQueue state to be \"VALID\": %s", err) } - arn := *out.JobQueueArn + arn := aws.StringValue(out.JobQueueArn) log.Printf("[DEBUG] JobQueue created: %s", arn) d.SetId(arn) @@ -100,6 +106,7 @@ func resourceAwsBatchJobQueueCreate(d *schema.ResourceData, meta interface{}) er func resourceAwsBatchJobQueueRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).batchconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig jq, err := getJobQueue(conn, d.Id()) @@ -132,8 +139,15 @@ func resourceAwsBatchJobQueueRead(d *schema.ResourceData, meta interface{}) erro d.Set("priority", jq.Priority) d.Set("state", jq.State) - if err := d.Set("tags", keyvaluetags.BatchKeyValueTags(jq.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.BatchKeyValueTags(jq.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } return nil @@ -169,8 +183,8 @@ func resourceAwsBatchJobQueueUpdate(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.BatchUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) diff --git a/aws/resource_aws_batch_job_queue_test.go b/aws/resource_aws_batch_job_queue_test.go index e6b0eaec4e37..31d39a2783c4 100644 --- a/aws/resource_aws_batch_job_queue_test.go +++ b/aws/resource_aws_batch_job_queue_test.go @@ -61,6 +61,7 @@ func TestAccAWSBatchJobQueue_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobQueueDestroy, Steps: []resource.TestStep{ @@ -92,6 +93,7 @@ func TestAccAWSBatchJobQueue_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, Steps: []resource.TestStep{ @@ -115,6 +117,7 @@ func TestAccAWSBatchJobQueue_ComputeEnvironments_ExternalOrderUpdate(t *testing. resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobQueueDestroy, Steps: []resource.TestStep{ @@ -141,6 +144,7 @@ func TestAccAWSBatchJobQueue_Priority(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobQueueDestroy, Steps: []resource.TestStep{ @@ -174,6 +178,7 @@ func TestAccAWSBatchJobQueue_State(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobQueueDestroy, Steps: []resource.TestStep{ @@ -207,6 +212,7 @@ func TestAccAWSBatchJobQueue_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBatch(t) }, + ErrorCheck: testAccErrorCheck(t, batch.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckBatchJobQueueDestroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_budgets_budget.go b/aws/resource_aws_budgets_budget.go index 7979bae02c19..95698350dcfb 100644 --- a/aws/resource_aws_budgets_budget.go +++ b/aws/resource_aws_budgets_budget.go @@ -4,23 +4,32 @@ import ( "fmt" "log" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/budgets" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/shopspring/decimal" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/naming" + tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsBudgetsBudget() *schema.Resource { return &schema.Resource{ + Create: resourceAwsBudgetsBudgetCreate, + Read: resourceAwsBudgetsBudgetRead, + Update: resourceAwsBudgetsBudgetUpdate, + Delete: resourceAwsBudgetsBudgetDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ - "arn": { - Type: schema.TypeString, - Computed: true, - }, "account_id": { Type: schema.TypeString, Computed: true, @@ -28,31 +37,42 @@ func resourceAwsBudgetsBudget() *schema.Resource { ForceNew: true, ValidateFunc: validateAwsAccountId, }, - "name": { - Type: schema.TypeString, - Computed: true, - Optional: true, - ForceNew: true, - ConflictsWith: []string{"name_prefix"}, - }, - "name_prefix": { + "arn": { Type: schema.TypeString, Computed: true, - Optional: true, - ForceNew: true, }, "budget_type": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice(budgets.BudgetType_Values(), false), }, - "limit_amount": { - Type: schema.TypeString, - Required: true, + "cost_filters": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"cost_filter"}, }, - "limit_unit": { - Type: schema.TypeString, - Required: true, + "cost_filter": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ConflictsWith: []string{"cost_filters"}, }, "cost_types": { Type: schema.TypeList, @@ -119,25 +139,28 @@ func resourceAwsBudgetsBudget() *schema.Resource { }, }, }, - "time_period_start": { - Type: schema.TypeString, - Required: true, + "limit_amount": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressEquivalentBudgetLimitAmount, }, - "time_period_end": { + "limit_unit": { Type: schema.TypeString, - Optional: true, - Default: "2087-06-15_00:00", + Required: true, }, - "time_unit": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(budgets.TimeUnit_Values(), false), + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, }, - "cost_filters": { - Type: schema.TypeMap, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, }, "notification": { Type: schema.TypeSet, @@ -149,15 +172,6 @@ func resourceAwsBudgetsBudget() *schema.Resource { Required: true, ValidateFunc: validation.StringInSlice(budgets.ComparisonOperator_Values(), false), }, - "threshold": { - Type: schema.TypeFloat, - Required: true, - }, - "threshold_type": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(budgets.ThresholdType_Values(), false), - }, "notification_type": { Type: schema.TypeString, Required: true, @@ -176,41 +190,52 @@ func resourceAwsBudgetsBudget() *schema.Resource { ValidateFunc: validateArn, }, }, + "threshold": { + Type: schema.TypeFloat, + Required: true, + }, + "threshold_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ThresholdType_Values(), false), + }, }, }, }, - }, - Create: resourceAwsBudgetsBudgetCreate, - Read: resourceAwsBudgetsBudgetRead, - Update: resourceAwsBudgetsBudgetUpdate, - Delete: resourceAwsBudgetsBudgetDelete, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + "time_period_end": { + Type: schema.TypeString, + Optional: true, + Default: "2087-06-15_00:00", + ValidateFunc: tfbudgets.ValidateTimePeriodTimestamp, + }, + "time_period_start": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: tfbudgets.ValidateTimePeriodTimestamp, + }, + "time_unit": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.TimeUnit_Values(), false), + }, }, } } func resourceAwsBudgetsBudgetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + budget, err := expandBudgetsBudgetUnmarshal(d) if err != nil { return fmt.Errorf("failed unmarshalling budget: %v", err) } - if v, ok := d.GetOk("name"); ok { - budget.BudgetName = aws.String(v.(string)) + name := naming.Generate(d.Get("name").(string), d.Get("name_prefix").(string)) + budget.BudgetName = aws.String(name) - } else if v, ok := d.GetOk("name_prefix"); ok { - budget.BudgetName = aws.String(resource.PrefixedUniqueId(v.(string))) - - } else { - budget.BudgetName = aws.String(resource.UniqueId()) - } - - conn := meta.(*AWSClient).budgetconn - var accountID string - if v, ok := d.GetOk("account_id"); ok { - accountID = v.(string) - } else { + accountID := d.Get("account_id").(string) + if accountID == "" { accountID = meta.(*AWSClient).accountid } @@ -218,8 +243,9 @@ func resourceAwsBudgetsBudgetCreate(d *schema.ResourceData, meta interface{}) er AccountId: aws.String(accountID), Budget: budget, }) + if err != nil { - return fmt.Errorf("create budget failed: %v", err) + return fmt.Errorf("error creating Budget (%s): %w", name, err) } d.SetId(fmt.Sprintf("%s:%s", accountID, aws.StringValue(budget.BudgetName))) @@ -236,104 +262,47 @@ func resourceAwsBudgetsBudgetCreate(d *schema.ResourceData, meta interface{}) er return resourceAwsBudgetsBudgetRead(d, meta) } -func resourceAwsBudgetsBudgetNotificationsCreate(notifications []*budgets.Notification, subscribers [][]*budgets.Subscriber, budgetName string, accountID string, meta interface{}) error { +func resourceAwsBudgetsBudgetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).budgetconn - for i, notification := range notifications { - subscribers := subscribers[i] - if len(subscribers) == 0 { - return fmt.Errorf("Notification must have at least one subscriber!") - } - _, err := conn.CreateNotification(&budgets.CreateNotificationInput{ - BudgetName: aws.String(budgetName), - AccountId: aws.String(accountID), - Notification: notification, - Subscribers: subscribers, - }) - - if err != nil { - return err - } - } - - return nil -} - -func expandBudgetNotificationsUnmarshal(notificationsRaw []interface{}) ([]*budgets.Notification, [][]*budgets.Subscriber) { - - notifications := make([]*budgets.Notification, len(notificationsRaw)) - subscribersForNotifications := make([][]*budgets.Subscriber, len(notificationsRaw)) - for i, notificationRaw := range notificationsRaw { - notificationRaw := notificationRaw.(map[string]interface{}) - comparisonOperator := notificationRaw["comparison_operator"].(string) - threshold := notificationRaw["threshold"].(float64) - thresholdType := notificationRaw["threshold_type"].(string) - notificationType := notificationRaw["notification_type"].(string) - - notifications[i] = &budgets.Notification{ - ComparisonOperator: aws.String(comparisonOperator), - Threshold: aws.Float64(threshold), - ThresholdType: aws.String(thresholdType), - NotificationType: aws.String(notificationType), - } - - emailSubscribers := expandBudgetSubscribers(notificationRaw["subscriber_email_addresses"], budgets.SubscriptionTypeEmail) - snsSubscribers := expandBudgetSubscribers(notificationRaw["subscriber_sns_topic_arns"], budgets.SubscriptionTypeSns) + accountID, budgetName, err := tfbudgets.BudgetParseResourceID(d.Id()) - subscribersForNotifications[i] = append(emailSubscribers, snsSubscribers...) - } - return notifications, subscribersForNotifications -} - -func expandBudgetSubscribers(rawList interface{}, subscriptionType string) []*budgets.Subscriber { - result := make([]*budgets.Subscriber, 0) - addrs := expandStringSet(rawList.(*schema.Set)) - for _, addr := range addrs { - result = append(result, &budgets.Subscriber{ - SubscriptionType: aws.String(subscriptionType), - Address: addr, - }) - } - return result -} - -func resourceAwsBudgetsBudgetRead(d *schema.ResourceData, meta interface{}) error { - accountID, budgetName, err := decodeBudgetsBudgetID(d.Id()) if err != nil { return err } - conn := meta.(*AWSClient).budgetconn - describeBudgetOutput, err := conn.DescribeBudget(&budgets.DescribeBudgetInput{ - BudgetName: aws.String(budgetName), - AccountId: aws.String(accountID), - }) - if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { - log.Printf("[WARN] Budget %s not found, removing from state", d.Id()) + budget, err := finder.BudgetByAccountIDAndBudgetName(conn, accountID, budgetName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Budget (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("describe budget failed: %v", err) - } - - budget := describeBudgetOutput.Budget - if budget == nil { - log.Printf("[WARN] Budget %s not found, removing from state", d.Id()) - d.SetId("") - return nil + return fmt.Errorf("error reading Budget (%s): %w", d.Id(), err) } d.Set("account_id", accountID) + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "budgets", + AccountID: accountID, + Resource: fmt.Sprintf("budget/%s", budgetName), + } + d.Set("arn", arn.String()) d.Set("budget_type", budget.BudgetType) + // `cost_filters` should be removed in future releases + if err := d.Set("cost_filter", convertCostFiltersToMap(budget.CostFilters)); err != nil { + return fmt.Errorf("error setting cost_filter: %w", err) + } if err := d.Set("cost_filters", convertCostFiltersToStringMap(budget.CostFilters)); err != nil { - return fmt.Errorf("error setting cost_filters: %s", err) + return fmt.Errorf("error setting cost_filters: %w", err) } if err := d.Set("cost_types", flattenBudgetsCostTypes(budget.CostTypes)); err != nil { - return fmt.Errorf("error setting cost_types: %s %s", err, budget.CostTypes) + return fmt.Errorf("error setting cost_types: %w", err) } if budget.BudgetLimit != nil { @@ -342,103 +311,86 @@ func resourceAwsBudgetsBudgetRead(d *schema.ResourceData, meta interface{}) erro } d.Set("name", budget.BudgetName) + d.Set("name_prefix", naming.NamePrefixFromName(aws.StringValue(budget.BudgetName))) if budget.TimePeriod != nil { - d.Set("time_period_end", aws.TimeValue(budget.TimePeriod.End).Format("2006-01-02_15:04")) - d.Set("time_period_start", aws.TimeValue(budget.TimePeriod.Start).Format("2006-01-02_15:04")) + d.Set("time_period_end", tfbudgets.TimePeriodTimestampToString(budget.TimePeriod.End)) + d.Set("time_period_start", tfbudgets.TimePeriodTimestampToString(budget.TimePeriod.Start)) } d.Set("time_unit", budget.TimeUnit) - arn := arn.ARN{ - Partition: meta.(*AWSClient).partition, - Service: "budgetservice", - AccountID: meta.(*AWSClient).accountid, - Resource: fmt.Sprintf("budget/%s", aws.StringValue(budget.BudgetName)), - } - d.Set("arn", arn.String()) - - return resourceAwsBudgetsBudgetNotificationRead(d, meta) -} - -func resourceAwsBudgetsBudgetNotificationRead(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*AWSClient).budgetconn + notifications, err := finder.NotificationsByAccountIDAndBudgetName(conn, accountID, budgetName) - accountID, budgetName, err := decodeBudgetsBudgetID(d.Id()) - - if err != nil { - return fmt.Errorf("error decoding Budget (%s) ID: %s", d.Id(), err) + if tfresource.NotFound(err) { + return nil } - describeNotificationsForBudgetOutput, err := conn.DescribeNotificationsForBudget(&budgets.DescribeNotificationsForBudgetInput{ - BudgetName: aws.String(budgetName), - AccountId: aws.String(accountID), - }) - if err != nil { - return fmt.Errorf("error describing Budget (%s) Notifications: %s", d.Id(), err) + return fmt.Errorf("error reading Budget (%s) notifications: %w", d.Id(), err) } - notifications := make([]map[string]interface{}, 0) + var tfList []interface{} - for _, notificationOutput := range describeNotificationsForBudgetOutput.Notifications { - notification := make(map[string]interface{}) + for _, notification := range notifications { + tfMap := make(map[string]interface{}) - notification["comparison_operator"] = aws.StringValue(notificationOutput.ComparisonOperator) - notification["threshold"] = aws.Float64Value(notificationOutput.Threshold) - notification["notification_type"] = aws.StringValue(notificationOutput.NotificationType) + tfMap["comparison_operator"] = aws.StringValue(notification.ComparisonOperator) + tfMap["threshold"] = aws.Float64Value(notification.Threshold) + tfMap["notification_type"] = aws.StringValue(notification.NotificationType) - if notificationOutput.ThresholdType == nil { + if notification.ThresholdType == nil { // The AWS API doesn't seem to return a ThresholdType if it's set to PERCENTAGE // Set it manually to make behavior more predictable - notification["threshold_type"] = budgets.ThresholdTypePercentage + tfMap["threshold_type"] = budgets.ThresholdTypePercentage } else { - notification["threshold_type"] = aws.StringValue(notificationOutput.ThresholdType) + tfMap["threshold_type"] = aws.StringValue(notification.ThresholdType) } - subscribersOutput, err := conn.DescribeSubscribersForNotification(&budgets.DescribeSubscribersForNotificationInput{ - BudgetName: aws.String(budgetName), - AccountId: aws.String(accountID), - Notification: notificationOutput, - }) + subscribers, err := finder.SubscribersByAccountIDBudgetNameAndNotification(conn, accountID, budgetName, notification) + + if tfresource.NotFound(err) { + tfList = append(tfList, tfMap) + continue + } if err != nil { - return fmt.Errorf("error describing Budget (%s) Notification Subscribers: %s", d.Id(), err) + return fmt.Errorf("error reading Budget (%s) subscribers: %w", d.Id(), err) } - snsSubscribers := make([]interface{}, 0) - emailSubscribers := make([]interface{}, 0) + var emailSubscribers []string + var snsSubscribers []string - for _, subscriberOutput := range subscribersOutput.Subscribers { - if aws.StringValue(subscriberOutput.SubscriptionType) == budgets.SubscriptionTypeSns { - snsSubscribers = append(snsSubscribers, *subscriberOutput.Address) - } else if aws.StringValue(subscriberOutput.SubscriptionType) == budgets.SubscriptionTypeEmail { - emailSubscribers = append(emailSubscribers, *subscriberOutput.Address) + for _, subscriber := range subscribers { + if aws.StringValue(subscriber.SubscriptionType) == budgets.SubscriptionTypeSns { + snsSubscribers = append(snsSubscribers, aws.StringValue(subscriber.Address)) + } else if aws.StringValue(subscriber.SubscriptionType) == budgets.SubscriptionTypeEmail { + emailSubscribers = append(emailSubscribers, aws.StringValue(subscriber.Address)) } } - if len(snsSubscribers) > 0 { - notification["subscriber_sns_topic_arns"] = schema.NewSet(schema.HashString, snsSubscribers) - } - if len(emailSubscribers) > 0 { - notification["subscriber_email_addresses"] = schema.NewSet(schema.HashString, emailSubscribers) - } - notifications = append(notifications, notification) + + tfMap["subscriber_email_addresses"] = emailSubscribers + tfMap["subscriber_sns_topic_arns"] = snsSubscribers + + tfList = append(tfList, tfMap) } - if err := d.Set("notification", notifications); err != nil { - return fmt.Errorf("error setting notification: %s %s", err, describeNotificationsForBudgetOutput.Notifications) + if err := d.Set("notification", tfList); err != nil { + return fmt.Errorf("error setting notification: %w", err) } return nil } func resourceAwsBudgetsBudgetUpdate(d *schema.ResourceData, meta interface{}) error { - accountID, _, err := decodeBudgetsBudgetID(d.Id()) + conn := meta.(*AWSClient).budgetconn + + accountID, _, err := tfbudgets.BudgetParseResourceID(d.Id()) + if err != nil { return err } - conn := meta.(*AWSClient).budgetconn budget, err := expandBudgetsBudgetUnmarshal(d) if err != nil { return fmt.Errorf("could not create budget: %v", err) @@ -460,9 +412,60 @@ func resourceAwsBudgetsBudgetUpdate(d *schema.ResourceData, meta interface{}) er return resourceAwsBudgetsBudgetRead(d, meta) } + +func resourceAwsBudgetsBudgetDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + accountID, budgetName, err := tfbudgets.BudgetParseResourceID(d.Id()) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting Budget: %s", d.Id()) + _, err = conn.DeleteBudget(&budgets.DeleteBudgetInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(budgetName), + }) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Budget (%s): %w", d.Id(), err) + } + + return nil +} + +func resourceAwsBudgetsBudgetNotificationsCreate(notifications []*budgets.Notification, subscribers [][]*budgets.Subscriber, budgetName string, accountID string, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + for i, notification := range notifications { + subscribers := subscribers[i] + if len(subscribers) == 0 { + return fmt.Errorf("Notification must have at least one subscriber!") + } + _, err := conn.CreateNotification(&budgets.CreateNotificationInput{ + BudgetName: aws.String(budgetName), + AccountId: aws.String(accountID), + Notification: notification, + Subscribers: subscribers, + }) + + if err != nil { + return err + } + } + + return nil +} + func resourceAwsBudgetsBudgetNotificationsUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).budgetconn - accountID, budgetName, err := decodeBudgetsBudgetID(d.Id()) + + accountID, budgetName, err := tfbudgets.BudgetParseResourceID(d.Id()) if err != nil { return err @@ -500,29 +503,6 @@ func resourceAwsBudgetsBudgetNotificationsUpdate(d *schema.ResourceData, meta in return nil } -func resourceAwsBudgetsBudgetDelete(d *schema.ResourceData, meta interface{}) error { - accountID, budgetName, err := decodeBudgetsBudgetID(d.Id()) - if err != nil { - return err - } - - conn := meta.(*AWSClient).budgetconn - _, err = conn.DeleteBudget(&budgets.DeleteBudgetInput{ - BudgetName: aws.String(budgetName), - AccountId: aws.String(accountID), - }) - if err != nil { - if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { - log.Printf("[INFO] budget %s could not be found. skipping delete.", d.Id()) - return nil - } - - return fmt.Errorf("delete budget failed: %v", err) - } - - return nil -} - func flattenBudgetsCostTypes(costTypes *budgets.CostTypes) []map[string]interface{} { if costTypes == nil { return []map[string]interface{}{} @@ -544,6 +524,22 @@ func flattenBudgetsCostTypes(costTypes *budgets.CostTypes) []map[string]interfac return []map[string]interface{}{m} } +func convertCostFiltersToMap(costFilters map[string][]*string) []map[string]interface{} { + convertedCostFilters := make([]map[string]interface{}, 0) + for k, v := range costFilters { + convertedCostFilter := make(map[string]interface{}) + filterValues := make([]string, 0) + for _, singleFilterValue := range v { + filterValues = append(filterValues, *singleFilterValue) + } + convertedCostFilter["values"] = filterValues + convertedCostFilter["name"] = k + convertedCostFilters = append(convertedCostFilters, convertedCostFilter) + } + + return convertedCostFilters +} + func convertCostFiltersToStringMap(costFilters map[string][]*string) map[string]string { convertedCostFilters := make(map[string]string) for k, v := range costFilters { @@ -563,22 +559,34 @@ func expandBudgetsBudgetUnmarshal(d *schema.ResourceData) (*budgets.Budget, erro budgetType := d.Get("budget_type").(string) budgetLimitAmount := d.Get("limit_amount").(string) budgetLimitUnit := d.Get("limit_unit").(string) - costTypes := expandBudgetsCostTypesUnmarshal(d.Get("cost_types").([]interface{})) budgetTimeUnit := d.Get("time_unit").(string) budgetCostFilters := make(map[string][]*string) - for k, v := range d.Get("cost_filters").(map[string]interface{}) { - filterValue := v.(string) - budgetCostFilters[k] = append(budgetCostFilters[k], aws.String(filterValue)) + + if costFilter, ok := d.GetOk("cost_filter"); ok { + for _, v := range costFilter.(*schema.Set).List() { + element := v.(map[string]interface{}) + key := element["name"].(string) + for _, filterValue := range element["values"].([]interface{}) { + budgetCostFilters[key] = append(budgetCostFilters[key], aws.String(filterValue.(string))) + } + } + } else if costFilters, ok := d.GetOk("cost_filters"); ok { + for k, v := range costFilters.(map[string]interface{}) { + filterValue := v.(string) + budgetCostFilters[k] = append(budgetCostFilters[k], aws.String(filterValue)) + } } - budgetTimePeriodStart, err := time.Parse("2006-01-02_15:04", d.Get("time_period_start").(string)) + budgetTimePeriodStart, err := tfbudgets.TimePeriodTimestampFromString(d.Get("time_period_start").(string)) + if err != nil { - return nil, fmt.Errorf("failure parsing time: %v", err) + return nil, err } - budgetTimePeriodEnd, err := time.Parse("2006-01-02_15:04", d.Get("time_period_end").(string)) + budgetTimePeriodEnd, err := tfbudgets.TimePeriodTimestampFromString(d.Get("time_period_end").(string)) + if err != nil { - return nil, fmt.Errorf("failure parsing time: %v", err) + return nil, err } budget := &budgets.Budget{ @@ -588,59 +596,115 @@ func expandBudgetsBudgetUnmarshal(d *schema.ResourceData) (*budgets.Budget, erro Amount: aws.String(budgetLimitAmount), Unit: aws.String(budgetLimitUnit), }, - CostTypes: costTypes, TimePeriod: &budgets.TimePeriod{ - End: &budgetTimePeriodEnd, - Start: &budgetTimePeriodStart, + End: budgetTimePeriodEnd, + Start: budgetTimePeriodStart, }, TimeUnit: aws.String(budgetTimeUnit), CostFilters: budgetCostFilters, } + + if v, ok := d.GetOk("cost_types"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + budget.CostTypes = expandBudgetsCostTypes(v.([]interface{})[0].(map[string]interface{})) + } + return budget, nil } -func decodeBudgetsBudgetID(id string) (string, string, error) { - parts := strings.Split(id, ":") - if len(parts) != 2 { - return "", "", fmt.Errorf("Unexpected format of ID (%q), expected AccountID:BudgetName", id) +func expandBudgetsCostTypes(tfMap map[string]interface{}) *budgets.CostTypes { + if tfMap == nil { + return nil + } + + apiObject := &budgets.CostTypes{} + + if v, ok := tfMap["include_credit"].(bool); ok { + apiObject.IncludeCredit = aws.Bool(v) + } + if v, ok := tfMap["include_discount"].(bool); ok { + apiObject.IncludeDiscount = aws.Bool(v) + } + if v, ok := tfMap["include_other_subscription"].(bool); ok { + apiObject.IncludeOtherSubscription = aws.Bool(v) + } + if v, ok := tfMap["include_recurring"].(bool); ok { + apiObject.IncludeRecurring = aws.Bool(v) + } + if v, ok := tfMap["include_refund"].(bool); ok { + apiObject.IncludeRefund = aws.Bool(v) + } + if v, ok := tfMap["include_subscription"].(bool); ok { + apiObject.IncludeSubscription = aws.Bool(v) + } + if v, ok := tfMap["include_support"].(bool); ok { + apiObject.IncludeSupport = aws.Bool(v) + } + if v, ok := tfMap["include_tax"].(bool); ok { + apiObject.IncludeTax = aws.Bool(v) + } + if v, ok := tfMap["include_upfront"].(bool); ok { + apiObject.IncludeUpfront = aws.Bool(v) + } + if v, ok := tfMap["use_amortized"].(bool); ok { + apiObject.UseAmortized = aws.Bool(v) + } + if v, ok := tfMap["use_blended"].(bool); ok { + apiObject.UseBlended = aws.Bool(v) } - return parts[0], parts[1], nil + + return apiObject } -func expandBudgetsCostTypesUnmarshal(budgetCostTypes []interface{}) *budgets.CostTypes { - costTypes := &budgets.CostTypes{ - IncludeCredit: aws.Bool(true), - IncludeDiscount: aws.Bool(true), - IncludeOtherSubscription: aws.Bool(true), - IncludeRecurring: aws.Bool(true), - IncludeRefund: aws.Bool(true), - IncludeSubscription: aws.Bool(true), - IncludeSupport: aws.Bool(true), - IncludeTax: aws.Bool(true), - IncludeUpfront: aws.Bool(true), - UseAmortized: aws.Bool(false), - UseBlended: aws.Bool(false), - } - if len(budgetCostTypes) == 1 { - costTypesMap := budgetCostTypes[0].(map[string]interface{}) - for k, v := range map[string]*bool{ - "include_credit": costTypes.IncludeCredit, - "include_discount": costTypes.IncludeDiscount, - "include_other_subscription": costTypes.IncludeOtherSubscription, - "include_recurring": costTypes.IncludeRecurring, - "include_refund": costTypes.IncludeRefund, - "include_subscription": costTypes.IncludeSubscription, - "include_support": costTypes.IncludeSupport, - "include_tax": costTypes.IncludeTax, - "include_upfront": costTypes.IncludeUpfront, - "use_amortized": costTypes.UseAmortized, - "use_blended": costTypes.UseBlended, - } { - if val, ok := costTypesMap[k]; ok { - *v = val.(bool) - } +func expandBudgetNotificationsUnmarshal(notificationsRaw []interface{}) ([]*budgets.Notification, [][]*budgets.Subscriber) { + + notifications := make([]*budgets.Notification, len(notificationsRaw)) + subscribersForNotifications := make([][]*budgets.Subscriber, len(notificationsRaw)) + for i, notificationRaw := range notificationsRaw { + notificationRaw := notificationRaw.(map[string]interface{}) + comparisonOperator := notificationRaw["comparison_operator"].(string) + threshold := notificationRaw["threshold"].(float64) + thresholdType := notificationRaw["threshold_type"].(string) + notificationType := notificationRaw["notification_type"].(string) + + notifications[i] = &budgets.Notification{ + ComparisonOperator: aws.String(comparisonOperator), + Threshold: aws.Float64(threshold), + ThresholdType: aws.String(thresholdType), + NotificationType: aws.String(notificationType), } + + emailSubscribers := expandBudgetSubscribers(notificationRaw["subscriber_email_addresses"], budgets.SubscriptionTypeEmail) + snsSubscribers := expandBudgetSubscribers(notificationRaw["subscriber_sns_topic_arns"], budgets.SubscriptionTypeSns) + + subscribersForNotifications[i] = append(emailSubscribers, snsSubscribers...) + } + return notifications, subscribersForNotifications +} + +func expandBudgetSubscribers(rawList interface{}, subscriptionType string) []*budgets.Subscriber { + result := make([]*budgets.Subscriber, 0) + addrs := expandStringSet(rawList.(*schema.Set)) + for _, addr := range addrs { + result = append(result, &budgets.Subscriber{ + SubscriptionType: aws.String(subscriptionType), + Address: addr, + }) + } + return result +} + +func suppressEquivalentBudgetLimitAmount(k, old, new string, d *schema.ResourceData) bool { + d1, err := decimal.NewFromString(old) + + if err != nil { + return false + } + + d2, err := decimal.NewFromString(new) + + if err != nil { + return false } - return costTypes + return d1.Equal(d2) } diff --git a/aws/resource_aws_budgets_budget_action.go b/aws/resource_aws_budgets_budget_action.go new file mode 100644 index 000000000000..40dc9acb66c0 --- /dev/null +++ b/aws/resource_aws_budgets_budget_action.go @@ -0,0 +1,629 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/waiter" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsBudgetsBudgetAction() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsBudgetsBudgetActionCreate, + Read: resourceAwsBudgetsBudgetActionRead, + Update: resourceAwsBudgetsBudgetActionUpdate, + Delete: resourceAwsBudgetsBudgetActionDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "account_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "action_id": { + Type: schema.TypeString, + Computed: true, + }, + "action_threshold": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action_threshold_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ThresholdType_Values(), false), + }, + "action_threshold_value": { + Type: schema.TypeFloat, + Required: true, + ValidateFunc: validation.FloatBetween(0, 40000000000), + }, + }, + }, + }, + "action_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(budgets.ActionType_Values(), false), + }, + "approval_model": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ApprovalModel_Values(), false), + }, + "budget_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[^:\\]+`), "The ':' and '\\' characters aren't allowed."), + ), + }, + "definition": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "iam_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "groups": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "roles": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "users": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "ssm_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action_sub_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ActionSubType_Values(), false), + }, + "instance_ids": { + Type: schema.TypeSet, + Required: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "region": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "scp_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_id": { + Type: schema.TypeString, + Required: true, + }, + "target_ids": { + Type: schema.TypeSet, + Required: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "execution_role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "notification_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.NotificationType_Values(), false), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "subscriber": { + Type: schema.TypeSet, + Required: true, + MaxItems: 11, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 2147483647), + validation.StringMatch(regexp.MustCompile(`(.*[\n\r\t\f\ ]?)*`), "Can't contain line breaks."), + )}, + "subscription_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.SubscriptionType_Values(), false), + }, + }, + }, + }, + }, + } +} + +func resourceAwsBudgetsBudgetActionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + accountID := d.Get("account_id").(string) + if accountID == "" { + accountID = meta.(*AWSClient).accountid + } + + input := &budgets.CreateBudgetActionInput{ + AccountId: aws.String(accountID), + ActionThreshold: expandAwsBudgetsBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})), + ActionType: aws.String(d.Get("action_type").(string)), + ApprovalModel: aws.String(d.Get("approval_model").(string)), + BudgetName: aws.String(d.Get("budget_name").(string)), + Definition: expandAwsBudgetsBudgetActionActionDefinition(d.Get("definition").([]interface{})), + ExecutionRoleArn: aws.String(d.Get("execution_role_arn").(string)), + NotificationType: aws.String(d.Get("notification_type").(string)), + Subscribers: expandAwsBudgetsBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)), + } + + log.Printf("[DEBUG] Creating Budget Action: %s", input) + outputRaw, err := tfresource.RetryWhenAwsErrCodeEquals(iamwaiter.PropagationTimeout, func() (interface{}, error) { + return conn.CreateBudgetAction(input) + }, budgets.ErrCodeAccessDeniedException) + + if err != nil { + return fmt.Errorf("error creating Budget Action: %w", err) + } + + output := outputRaw.(*budgets.CreateBudgetActionOutput) + actionID := aws.StringValue(output.ActionId) + budgetName := aws.StringValue(output.BudgetName) + + d.SetId(tfbudgets.BudgetActionCreateResourceID(accountID, actionID, budgetName)) + + if _, err := waiter.ActionAvailable(conn, accountID, actionID, budgetName); err != nil { + return fmt.Errorf("error waiting for Budget Action (%s) to create: %w", d.Id(), err) + } + + return resourceAwsBudgetsBudgetActionRead(d, meta) +} + +func resourceAwsBudgetsBudgetActionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + accountID, actionID, budgetName, err := tfbudgets.BudgetActionParseResourceID(d.Id()) + + if err != nil { + return err + } + + output, err := finder.ActionByAccountIDActionIDAndBudgetName(conn, accountID, actionID, budgetName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Budget Action (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Budget Action (%s): %w", d.Id(), err) + } + + d.Set("account_id", accountID) + d.Set("action_id", actionID) + + if err := d.Set("action_threshold", flattenAwsBudgetsBudgetActionActionThreshold(output.ActionThreshold)); err != nil { + return fmt.Errorf("error setting action_threshold: %w", err) + } + + d.Set("action_type", output.ActionType) + d.Set("approval_model", output.ApprovalModel) + d.Set("budget_name", budgetName) + + if err := d.Set("definition", flattenAwsBudgetsBudgetActionDefinition(output.Definition)); err != nil { + return fmt.Errorf("error setting definition: %w", err) + } + + d.Set("execution_role_arn", output.ExecutionRoleArn) + d.Set("notification_type", output.NotificationType) + d.Set("status", output.Status) + + if err := d.Set("subscriber", flattenAwsBudgetsBudgetActionSubscriber(output.Subscribers)); err != nil { + return fmt.Errorf("error setting subscriber: %w", err) + } + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "budgets", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("budget/%s/action/%s", budgetName, actionID), + } + d.Set("arn", arn.String()) + + return nil +} + +func resourceAwsBudgetsBudgetActionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + accountID, actionID, budgetName, err := tfbudgets.BudgetActionParseResourceID(d.Id()) + + if err != nil { + return err + } + + input := &budgets.UpdateBudgetActionInput{ + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + BudgetName: aws.String(budgetName), + } + + if d.HasChange("action_threshold") { + input.ActionThreshold = expandAwsBudgetsBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})) + } + + if d.HasChange("approval_model") { + input.ApprovalModel = aws.String(d.Get("approval_model").(string)) + } + + if d.HasChange("definition") { + input.Definition = expandAwsBudgetsBudgetActionActionDefinition(d.Get("definition").([]interface{})) + } + + if d.HasChange("execution_role_arn") { + input.ExecutionRoleArn = aws.String(d.Get("execution_role_arn").(string)) + } + + if d.HasChange("notification_type") { + input.NotificationType = aws.String(d.Get("notification_type").(string)) + } + + if d.HasChange("subscriber") { + input.Subscribers = expandAwsBudgetsBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)) + } + + log.Printf("[DEBUG] Updating Budget Action: %s", input) + _, err = conn.UpdateBudgetAction(input) + + if err != nil { + return fmt.Errorf("error updating Budget Action (%s): %w", d.Id(), err) + } + + if _, err := waiter.ActionAvailable(conn, accountID, actionID, budgetName); err != nil { + return fmt.Errorf("error waiting for Budget Action (%s) to update: %w", d.Id(), err) + } + + return resourceAwsBudgetsBudgetActionRead(d, meta) +} + +func resourceAwsBudgetsBudgetActionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + accountID, actionID, budgetName, err := tfbudgets.BudgetActionParseResourceID(d.Id()) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting Budget Action: %s", d.Id()) + _, err = conn.DeleteBudgetAction(&budgets.DeleteBudgetActionInput{ + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + BudgetName: aws.String(budgetName), + }) + + if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Budget Action (%s): %w", d.Id(), err) + } + + return nil +} + +func expandAwsBudgetsBudgetActionActionThreshold(l []interface{}) *budgets.ActionThreshold { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.ActionThreshold{} + + if v, ok := m["action_threshold_type"].(string); ok && v != "" { + config.ActionThresholdType = aws.String(v) + } + + if v, ok := m["action_threshold_value"].(float64); ok { + config.ActionThresholdValue = aws.Float64(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionSubscriber(l *schema.Set) []*budgets.Subscriber { + if l.Len() == 0 { + return []*budgets.Subscriber{} + } + + items := []*budgets.Subscriber{} + + for _, m := range l.List() { + config := &budgets.Subscriber{} + raw := m.(map[string]interface{}) + + if v, ok := raw["address"].(string); ok && v != "" { + config.Address = aws.String(v) + } + + if v, ok := raw["subscription_type"].(string); ok { + config.SubscriptionType = aws.String(v) + } + + items = append(items, config) + } + + return items +} + +func expandAwsBudgetsBudgetActionActionDefinition(l []interface{}) *budgets.Definition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.Definition{} + + if v, ok := m["ssm_action_definition"].([]interface{}); ok && len(v) > 0 { + config.SsmActionDefinition = expandAwsBudgetsBudgetActionActionSsmActionDefinition(v) + } + + if v, ok := m["scp_action_definition"].([]interface{}); ok && len(v) > 0 { + config.ScpActionDefinition = expandAwsBudgetsBudgetActionActionScpActionDefinition(v) + } + + if v, ok := m["iam_action_definition"].([]interface{}); ok && len(v) > 0 { + config.IamActionDefinition = expandAwsBudgetsBudgetActionActionIamActionDefinition(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionScpActionDefinition(l []interface{}) *budgets.ScpActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.ScpActionDefinition{} + + if v, ok := m["policy_id"].(string); ok && v != "" { + config.PolicyId = aws.String(v) + } + + if v, ok := m["target_ids"].(*schema.Set); ok && v.Len() > 0 { + config.TargetIds = expandStringSet(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionSsmActionDefinition(l []interface{}) *budgets.SsmActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.SsmActionDefinition{} + + if v, ok := m["action_sub_type"].(string); ok && v != "" { + config.ActionSubType = aws.String(v) + } + + if v, ok := m["region"].(string); ok && v != "" { + config.Region = aws.String(v) + } + + if v, ok := m["instance_ids"].(*schema.Set); ok && v.Len() > 0 { + config.InstanceIds = expandStringSet(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionIamActionDefinition(l []interface{}) *budgets.IamActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.IamActionDefinition{} + + if v, ok := m["policy_arn"].(string); ok && v != "" { + config.PolicyArn = aws.String(v) + } + + if v, ok := m["groups"].(*schema.Set); ok && v.Len() > 0 { + config.Groups = expandStringSet(v) + } + + if v, ok := m["roles"].(*schema.Set); ok && v.Len() > 0 { + config.Roles = expandStringSet(v) + } + + if v, ok := m["users"].(*schema.Set); ok && v.Len() > 0 { + config.Users = expandStringSet(v) + } + + return config +} + +func flattenAwsBudgetsBudgetActionSubscriber(configured []*budgets.Subscriber) []map[string]interface{} { + dataResources := make([]map[string]interface{}, 0, len(configured)) + + for _, raw := range configured { + item := make(map[string]interface{}) + item["address"] = aws.StringValue(raw.Address) + item["subscription_type"] = aws.StringValue(raw.SubscriptionType) + + dataResources = append(dataResources, item) + } + + return dataResources +} + +func flattenAwsBudgetsBudgetActionActionThreshold(lt *budgets.ActionThreshold) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "action_threshold_type": aws.StringValue(lt.ActionThresholdType), + "action_threshold_value": aws.Float64Value(lt.ActionThresholdValue), + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionIamActionDefinition(lt *budgets.IamActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "policy_arn": aws.StringValue(lt.PolicyArn), + } + + if lt.Users != nil && len(lt.Users) > 0 { + attrs["users"] = flattenStringSet(lt.Users) + } + + if lt.Roles != nil && len(lt.Roles) > 0 { + attrs["roles"] = flattenStringSet(lt.Roles) + } + + if lt.Groups != nil && len(lt.Groups) > 0 { + attrs["groups"] = flattenStringSet(lt.Groups) + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionScpActionDefinition(lt *budgets.ScpActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "policy_id": aws.StringValue(lt.PolicyId), + } + + if lt.TargetIds != nil && len(lt.TargetIds) > 0 { + attrs["target_ids"] = flattenStringSet(lt.TargetIds) + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionSsmActionDefinition(lt *budgets.SsmActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "action_sub_type": aws.StringValue(lt.ActionSubType), + "instance_ids": flattenStringSet(lt.InstanceIds), + "region": aws.StringValue(lt.Region), + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionDefinition(lt *budgets.Definition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{} + + if lt.SsmActionDefinition != nil { + attrs["ssm_action_definition"] = flattenAwsBudgetsBudgetActionSsmActionDefinition(lt.SsmActionDefinition) + } + + if lt.IamActionDefinition != nil { + attrs["iam_action_definition"] = flattenAwsBudgetsBudgetActionIamActionDefinition(lt.IamActionDefinition) + } + + if lt.ScpActionDefinition != nil { + attrs["scp_action_definition"] = flattenAwsBudgetsBudgetActionScpActionDefinition(lt.ScpActionDefinition) + } + + return []map[string]interface{}{attrs} +} diff --git a/aws/resource_aws_budgets_budget_action_test.go b/aws/resource_aws_budgets_budget_action_test.go new file mode 100644 index 000000000000..907c1205c712 --- /dev/null +++ b/aws/resource_aws_budgets_budget_action_test.go @@ -0,0 +1,282 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/go-multierror" + "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" + tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + resource.AddTestSweepers("aws_budgets_budget_action", &resource.Sweeper{ + Name: "aws_budgets_budget_action", + F: testSweepBudgetsBudgetActionss, + }) +} + +func testSweepBudgetsBudgetActionss(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).budgetconn + accountID := client.(*AWSClient).accountid + input := &budgets.DescribeBudgetActionsForAccountInput{ + AccountId: aws.String(accountID), + } + var sweeperErrs *multierror.Error + + for { + output, err := conn.DescribeBudgetActionsForAccount(input) + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Budgets sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Budgets: %w", err)) + return sweeperErrs + } + + for _, action := range output.Actions { + name := aws.StringValue(action.BudgetName) + log.Printf("[INFO] Deleting Budget Action: %s", name) + id := fmt.Sprintf("%s:%s:%s", accountID, aws.StringValue(action.ActionId), name) + + r := resourceAwsBudgetsBudgetAction() + d := r.Data(nil) + d.SetId(id) + + err := r.Delete(d, client) + if err != nil { + sweeperErr := fmt.Errorf("error deleting Budget Action (%s): %w", name, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAWSBudgetsBudgetAction_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_budgets_budget_action.test" + var conf budgets.Action + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(budgets.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, budgets.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccAWSBudgetsBudgetActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBudgetsBudgetActionConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccAWSBudgetsBudgetActionExists(resourceName, &conf), + testAccMatchResourceAttrGlobalARN(resourceName, "arn", "budgets", regexp.MustCompile(fmt.Sprintf(`budget/%s/action/.+`, rName))), + resource.TestCheckResourceAttrPair(resourceName, "budget_name", "aws_budgets_budget.test", "name"), + resource.TestCheckResourceAttrPair(resourceName, "execution_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "action_type", "APPLY_IAM_POLICY"), + resource.TestCheckResourceAttr(resourceName, "approval_model", "AUTOMATIC"), + resource.TestCheckResourceAttr(resourceName, "notification_type", "ACTUAL"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.#", "1"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.0.action_threshold_type", "ABSOLUTE_VALUE"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.0.action_threshold_value", "100"), + resource.TestCheckResourceAttr(resourceName, "definition.#", "1"), + resource.TestCheckResourceAttr(resourceName, "definition.0.iam_action_definition.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "definition.0.iam_action_definition.0.policy_arn", "aws_iam_policy.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "definition.0.iam_action_definition.0.roles.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subscriber.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSBudgetsBudgetAction_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_budgets_budget_action.test" + var conf budgets.Action + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(budgets.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, budgets.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccAWSBudgetsBudgetActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBudgetsBudgetActionConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccAWSBudgetsBudgetActionExists(resourceName, &conf), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBudgetsBudgetAction(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSBudgetsBudgetActionExists(resourceName string, config *budgets.Action) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Budget Action ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).budgetconn + + accountID, actionID, budgetName, err := tfbudgets.BudgetActionParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + output, err := finder.ActionByAccountIDActionIDAndBudgetName(conn, accountID, actionID, budgetName) + + if err != nil { + return err + } + + *config = *output + + return nil + } +} + +func testAccAWSBudgetsBudgetActionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).budgetconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_budgets_budget_action" { + continue + } + + accountID, actionID, budgetName, err := tfbudgets.BudgetActionParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.ActionByAccountIDActionIDAndBudgetName(conn, accountID, actionID, budgetName) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Budget Action %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSBudgetsBudgetActionConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_policy" "test" { + name = %[1]q + description = "My test policy" + + policy = < 0 { + input.VoiceConnectorItems = expandVoiceConnectorItems(v.(*schema.Set).List()) + } + + resp, err := conn.CreateVoiceConnectorGroupWithContext(ctx, input) + if err != nil || resp.VoiceConnectorGroup == nil { + return diag.Errorf("error creating Chime Voice Connector group: %s", err) + } + + d.SetId(aws.StringValue(resp.VoiceConnectorGroup.VoiceConnectorGroupId)) + + return resourceAwsChimeVoiceConnectorGroupRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + getInput := &chime.GetVoiceConnectorGroupInput{ + VoiceConnectorGroupId: aws.String(d.Id()), + } + + resp, err := conn.GetVoiceConnectorGroupWithContext(ctx, getInput) + if !d.IsNewResource() && isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice conector group %s not found", d.Id()) + d.SetId("") + return nil + } + if err != nil || resp.VoiceConnectorGroup == nil { + return diag.Errorf("error getting Chime Voice Connector group (%s): %s", d.Id(), err) + } + + d.Set("name", resp.VoiceConnectorGroup.Name) + + if err := d.Set("connector", flattenVoiceConnectorItems(resp.VoiceConnectorGroup.VoiceConnectorItems)); err != nil { + return diag.Errorf("error setting Chime Voice Connector group items (%s): %s", d.Id(), err) + } + return nil +} + +func resourceAwsChimeVoiceConnectorGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.UpdateVoiceConnectorGroupInput{ + Name: aws.String(d.Get("name").(string)), + VoiceConnectorGroupId: aws.String(d.Id()), + } + + if d.HasChange("connector") { + if v, ok := d.GetOk("connector"); ok { + input.VoiceConnectorItems = expandVoiceConnectorItems(v.(*schema.Set).List()) + } + } else if !d.IsNewResource() { + input.VoiceConnectorItems = make([]*chime.VoiceConnectorItem, 0) + } + + if _, err := conn.UpdateVoiceConnectorGroupWithContext(ctx, input); err != nil { + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice conector group %s not found", d.Id()) + d.SetId("") + return nil + } + return diag.Errorf("error updating Chime Voice Connector group (%s): %s", d.Id(), err) + } + + return resourceAwsChimeVoiceConnectorGroupRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + if v, ok := d.GetOk("connector"); ok && v.(*schema.Set).Len() > 0 { + if err := resourceAwsChimeVoiceConnectorGroupUpdate(ctx, d, meta); err != nil { + return err + } + } + + input := &chime.DeleteVoiceConnectorGroupInput{ + VoiceConnectorGroupId: aws.String(d.Id()), + } + + if _, err := conn.DeleteVoiceConnectorGroupWithContext(ctx, input); err != nil { + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice conector group %s not found", d.Id()) + return nil + } + return diag.Errorf("error deleting Chime Voice Connector group (%s): %s", d.Id(), err) + } + + return nil +} + +func expandVoiceConnectorItems(data []interface{}) []*chime.VoiceConnectorItem { + var connectorsItems []*chime.VoiceConnectorItem + + for _, rItem := range data { + item := rItem.(map[string]interface{}) + connectorsItems = append(connectorsItems, &chime.VoiceConnectorItem{ + VoiceConnectorId: aws.String(item["voice_connector_id"].(string)), + Priority: aws.Int64(int64(item["priority"].(int))), + }) + } + + return connectorsItems +} + +func flattenVoiceConnectorItems(connectors []*chime.VoiceConnectorItem) []interface{} { + var rawConnectors []interface{} + + for _, c := range connectors { + rawC := map[string]interface{}{ + "priority": aws.Int64Value(c.Priority), + "voice_connector_id": aws.StringValue(c.VoiceConnectorId), + } + rawConnectors = append(rawConnectors, rawC) + } + return rawConnectors +} diff --git a/aws/resource_aws_chime_voice_connector_group_test.go b/aws/resource_aws_chime_voice_connector_group_test.go new file mode 100644 index 000000000000..0b9c66e0e73f --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_group_test.go @@ -0,0 +1,184 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnectorGroup_basic(t *testing.T) { + var voiceConnectorGroup *chime.VoiceConnectorGroup + + vcgName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorGroupConfig(vcgName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorGroupExists(resourceName, voiceConnectorGroup), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("vcg-%s", vcgName)), + resource.TestCheckResourceAttr(resourceName, "connector.#", "1"), + resource.TestCheckResourceAttr(resourceName, "connector.0.priority", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorGroup_disappears(t *testing.T) { + var voiceConnectorGroup *chime.VoiceConnectorGroup + + vcgName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorGroupConfig(vcgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorGroupExists(resourceName, voiceConnectorGroup), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnectorGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorGroup_update(t *testing.T) { + var voiceConnectorGroup *chime.VoiceConnectorGroup + + vcgName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorGroupConfig(vcgName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorGroupExists(resourceName, voiceConnectorGroup), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("vcg-%s", vcgName)), + resource.TestCheckResourceAttr(resourceName, "connector.#", "1"), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorGroupUpdated(vcgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("vcg-updated-%s", vcgName)), + resource.TestCheckResourceAttr(resourceName, "connector.0.priority", "10"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSChimeVoiceConnectorGroupConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_group" "test" { + name = "vcg-%[1]s" + + connector { + voice_connector_id = aws_chime_voice_connector.chime.id + priority = 1 + } +} +`, name) +} + +func testAccAWSChimeVoiceConnectorGroupUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = false +} + +resource "aws_chime_voice_connector_group" "test" { + name = "vcg-updated-%[1]s" + + connector { + voice_connector_id = aws_chime_voice_connector.chime.id + priority = 10 + } +} +`, name) +} + +func testAccCheckAWSChimeVoiceConnectorGroupExists(name string, vc *chime.VoiceConnectorGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime voice connector group ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorGroupInput{ + VoiceConnectorGroupId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorGroup(input) + if err != nil || resp.VoiceConnectorGroup == nil { + return err + } + + vc = resp.VoiceConnectorGroup + return nil + } +} + +func testAccCheckAWSChimeVoiceConnectorGroupDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_chime_voice_connector" { + continue + } + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorGroupInput{ + VoiceConnectorGroupId: aws.String(rs.Primary.ID), + } + resp, err := conn.GetVoiceConnectorGroup(input) + if err == nil { + if resp.VoiceConnectorGroup != nil && aws.StringValue(resp.VoiceConnectorGroup.Name) != "" { + return fmt.Errorf("error Chime Voice Connector still exists") + } + } + return nil + } + return nil +} diff --git a/aws/resource_aws_chime_voice_connector_logging.go b/aws/resource_aws_chime_voice_connector_logging.go new file mode 100644 index 000000000000..7d72ab123877 --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_logging.go @@ -0,0 +1,127 @@ +package aws + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAwsChimeVoiceConnectorLogging() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsChimeVoiceConnectorLoggingCreate, + ReadWithoutTimeout: resourceAwsChimeVoiceConnectorLoggingRead, + UpdateWithoutTimeout: resourceAwsChimeVoiceConnectorLoggingUpdate, + DeleteWithoutTimeout: resourceAwsChimeVoiceConnectorLoggingDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "enable_sip_logs": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "voice_connector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsChimeVoiceConnectorLoggingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + vcId := d.Get("voice_connector_id").(string) + input := &chime.PutVoiceConnectorLoggingConfigurationInput{ + VoiceConnectorId: aws.String(vcId), + LoggingConfiguration: &chime.LoggingConfiguration{ + EnableSIPLogs: aws.Bool(d.Get("enable_sip_logs").(bool)), + }, + } + + if _, err := conn.PutVoiceConnectorLoggingConfigurationWithContext(ctx, input); err != nil { + return diag.Errorf("error creating Chime Voice Connector (%s) logging configuration: %s", vcId, err) + } + + d.SetId(vcId) + return resourceAwsChimeVoiceConnectorLoggingRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorLoggingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.GetVoiceConnectorLoggingConfigurationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + resp, err := conn.GetVoiceConnectorLoggingConfigurationWithContext(ctx, input) + if !d.IsNewResource() && isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice Connector logging configuration %s not found", d.Id()) + d.SetId("") + return nil + } + + if err != nil || resp.LoggingConfiguration == nil { + return diag.Errorf("error getting Chime Voice Connector (%s) logging configuration: %s", d.Id(), err) + } + + d.Set("enable_sip_logs", resp.LoggingConfiguration.EnableSIPLogs) + d.Set("voice_connector_id", d.Id()) + + return nil +} + +func resourceAwsChimeVoiceConnectorLoggingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + if d.HasChange("enable_sip_logs") { + input := &chime.PutVoiceConnectorLoggingConfigurationInput{ + VoiceConnectorId: aws.String(d.Id()), + LoggingConfiguration: &chime.LoggingConfiguration{ + EnableSIPLogs: aws.Bool(d.Get("enable_sip_logs").(bool)), + }, + } + + if _, err := conn.PutVoiceConnectorLoggingConfigurationWithContext(ctx, input); err != nil { + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice Connector logging configuration %s not found", d.Id()) + d.SetId("") + return nil + } + return diag.Errorf("error updating Chime Voice Connector (%s) logging configuration: %s", d.Id(), err) + } + } + + return resourceAwsChimeVoiceConnectorLoggingRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorLoggingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.PutVoiceConnectorLoggingConfigurationInput{ + VoiceConnectorId: aws.String(d.Id()), + LoggingConfiguration: &chime.LoggingConfiguration{ + EnableSIPLogs: aws.Bool(false), + }, + } + + _, err := conn.PutVoiceConnectorLoggingConfigurationWithContext(ctx, input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + return nil + } + + if err != nil { + return diag.Errorf("error deleting Chime Voice Connector (%s) logging configuration: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_chime_voice_connector_logging_test.go b/aws/resource_aws_chime_voice_connector_logging_test.go new file mode 100644 index 000000000000..4dddde59ac9d --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_logging_test.go @@ -0,0 +1,149 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnectorLogging_basic(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_logging.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorLoggingConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorLoggingExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "enable_sip_logs", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorLogging_disappears(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_logging.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorLoggingConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorLoggingExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnectorLogging(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorLogging_update(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_logging.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorLoggingConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorLoggingExists(resourceName), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorLoggingUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorLoggingExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "enable_sip_logs", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSChimeVoiceConnectorLoggingConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_logging" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + enable_sip_logs = true +} +`, name) +} + +func testAccAWSChimeVoiceConnectorLoggingUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_logging" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + enable_sip_logs = false +} +`, name) +} + +func testAccCheckAWSChimeVoiceConnectorLoggingExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime Voice Connector logging ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorLoggingConfigurationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorLoggingConfiguration(input) + if err != nil { + return err + } + + if resp == nil || resp.LoggingConfiguration == nil { + return fmt.Errorf("no Chime Voice Connector logging configureation (%s) found", rs.Primary.ID) + } + + return nil + } +} diff --git a/aws/resource_aws_chime_voice_connector_origination.go b/aws/resource_aws_chime_voice_connector_origination.go new file mode 100644 index 000000000000..561d60a5bf25 --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_origination.go @@ -0,0 +1,211 @@ +package aws + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func resourceAwsChimeVoiceConnectorOrigination() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsChimeVoiceConnectorOriginationCreate, + ReadWithoutTimeout: resourceAwsChimeVoiceConnectorOriginationRead, + UpdateWithoutTimeout: resourceAwsChimeVoiceConnectorOriginationUpdate, + DeleteWithoutTimeout: resourceAwsChimeVoiceConnectorOriginationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "disabled": { + Type: schema.TypeBool, + Optional: true, + }, + "route": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 20, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsIPAddress, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Default: 5060, + ValidateFunc: validation.IsPortNumber, + }, + "priority": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 99), + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(chime.OriginationRouteProtocol_Values(), false), + }, + "weight": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 99), + }, + }, + }, + }, + "voice_connector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsChimeVoiceConnectorOriginationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + vcId := d.Get("voice_connector_id").(string) + + input := &chime.PutVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(vcId), + Origination: &chime.Origination{ + Routes: expandOriginationRoutes(d.Get("route").(*schema.Set).List()), + }, + } + + if v, ok := d.GetOk("disabled"); ok { + input.Origination.Disabled = aws.Bool(v.(bool)) + } + + if _, err := conn.PutVoiceConnectorOriginationWithContext(ctx, input); err != nil { + return diag.Errorf("error creating Chime Voice Connector (%s) origination: %s", vcId, err) + } + + d.SetId(vcId) + + return resourceAwsChimeVoiceConnectorOriginationRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorOriginationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.GetVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + resp, err := conn.GetVoiceConnectorOriginationWithContext(ctx, input) + + if !d.IsNewResource() && isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice Connector (%s) origination not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error getting Chime Voice Connector (%s) origination: %s", d.Id(), err) + } + + if resp == nil || resp.Origination == nil { + return diag.Errorf("error getting Chime Voice Connector (%s) origination: empty response", d.Id()) + } + + d.Set("disabled", resp.Origination.Disabled) + d.Set("voice_connector_id", d.Id()) + + if err := d.Set("route", flattenOriginationRoutes(resp.Origination.Routes)); err != nil { + return diag.Errorf("error setting Chime Voice Connector (%s) origination routes: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsChimeVoiceConnectorOriginationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + if d.HasChanges("route", "disabled") { + input := &chime.PutVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(d.Id()), + Origination: &chime.Origination{ + Routes: expandOriginationRoutes(d.Get("route").(*schema.Set).List()), + }, + } + + if v, ok := d.GetOk("disabled"); ok { + input.Origination.Disabled = aws.Bool(v.(bool)) + } + + _, err := conn.PutVoiceConnectorOriginationWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error updating Chime Voice Connector (%s) origination: %s", d.Id(), err) + } + } + + return resourceAwsChimeVoiceConnectorOriginationRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorOriginationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.DeleteVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + _, err := conn.DeleteVoiceConnectorOriginationWithContext(ctx, input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + return nil + } + + if err != nil { + return diag.Errorf("error deleting Chime Voice Connector (%s) origination: %s", d.Id(), err) + } + + return nil +} + +func expandOriginationRoutes(data []interface{}) []*chime.OriginationRoute { + var originationRoutes []*chime.OriginationRoute + + for _, rItem := range data { + item := rItem.(map[string]interface{}) + originationRoutes = append(originationRoutes, &chime.OriginationRoute{ + Host: aws.String(item["host"].(string)), + Port: aws.Int64(int64(item["port"].(int))), + Priority: aws.Int64(int64(item["priority"].(int))), + Protocol: aws.String(item["protocol"].(string)), + Weight: aws.Int64(int64(item["weight"].(int))), + }) + } + + return originationRoutes +} + +func flattenOriginationRoutes(routes []*chime.OriginationRoute) []interface{} { + var rawRoutes []interface{} + + for _, route := range routes { + r := map[string]interface{}{ + "host": aws.StringValue(route.Host), + "port": aws.Int64Value(route.Port), + "priority": aws.Int64Value(route.Priority), + "protocol": aws.StringValue(route.Protocol), + "weight": aws.Int64Value(route.Weight), + } + + rawRoutes = append(rawRoutes, r) + } + + return rawRoutes +} diff --git a/aws/resource_aws_chime_voice_connector_origination_test.go b/aws/resource_aws_chime_voice_connector_origination_test.go new file mode 100644 index 000000000000..a31f1b3082bc --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_origination_test.go @@ -0,0 +1,211 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnectorOrigination_basic(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_origination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorOriginationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorOriginationConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorOriginationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "route.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "route.*", map[string]string{ + "protocol": "TCP", + "priority": "1", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorOrigination_disappears(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_origination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorOriginationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorOriginationConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorOriginationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnectorOrigination(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorOrigination_update(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_origination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorOriginationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorOriginationConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorOriginationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "route.#", "1"), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorOriginationUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorOriginationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "route.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "route.*", map[string]string{ + "protocol": "TCP", + "port": "5060", + "priority": "1", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "route.*", map[string]string{ + "protocol": "UDP", + "priority": "2", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSChimeVoiceConnectorOriginationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime voice connector origination ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorOrigination(input) + if err != nil { + return err + } + + if resp == nil || resp.Origination == nil { + return fmt.Errorf("Chime Voice Connector Origination (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAWSChimeVoiceConnectorOriginationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_chime_voice_connector_origination" { + continue + } + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorOriginationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorOrigination(input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + continue + } + + if err != nil { + return err + } + + if resp != nil && resp.Origination != nil { + return fmt.Errorf("error Chime Voice Connector Origination (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAWSChimeVoiceConnectorOriginationConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "test" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_origination" "test" { + route { + host = "200.100.12.1" + port = 5060 + protocol = "TCP" + priority = 1 + weight = 1 + } + voice_connector_id = aws_chime_voice_connector.test.id +} +`, name) +} + +func testAccAWSChimeVoiceConnectorOriginationUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "test" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_origination" "test" { + voice_connector_id = aws_chime_voice_connector.test.id + + route { + host = "200.100.12.1" + port = 5060 + protocol = "TCP" + priority = 1 + weight = 1 + } + + route { + host = "209.166.124.147" + protocol = "UDP" + priority = 2 + weight = 30 + } +} +`, name) +} diff --git a/aws/resource_aws_chime_voice_connector_streaming.go b/aws/resource_aws_chime_voice_connector_streaming.go new file mode 100644 index 000000000000..35eb12b2b73a --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_streaming.go @@ -0,0 +1,190 @@ +package aws + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func resourceAwsChimeVoiceConnectorStreaming() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsChimeVoiceConnectorStreamingCreate, + ReadWithoutTimeout: resourceAwsChimeVoiceConnectorStreamingRead, + UpdateWithoutTimeout: resourceAwsChimeVoiceConnectorStreamingUpdate, + DeleteWithoutTimeout: resourceAwsChimeVoiceConnectorStreamingDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "data_retention": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtLeast(0), + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "streaming_notification_targets": { + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 3, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(chime.NotificationTarget_Values(), false), + }, + }, + "voice_connector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsChimeVoiceConnectorStreamingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + vcId := d.Get("voice_connector_id").(string) + input := &chime.PutVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(vcId), + } + + config := &chime.StreamingConfiguration{ + DataRetentionInHours: aws.Int64(int64(d.Get("data_retention").(int))), + Disabled: aws.Bool(d.Get("disabled").(bool)), + } + + if v, ok := d.GetOk("streaming_notification_targets"); ok && v.(*schema.Set).Len() > 0 { + config.StreamingNotificationTargets = expandStreamingNotificationTargets(v.(*schema.Set).List()) + } + + input.StreamingConfiguration = config + + if _, err := conn.PutVoiceConnectorStreamingConfigurationWithContext(ctx, input); err != nil { + return diag.Errorf("error creating Chime Voice Connector (%s) streaming configuration: %s", vcId, err) + } + + d.SetId(vcId) + + return resourceAwsChimeVoiceConnectorStreamingRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorStreamingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.GetVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + resp, err := conn.GetVoiceConnectorStreamingConfigurationWithContext(ctx, input) + if !d.IsNewResource() && isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice Connector (%s) streaming not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error getting Chime Voice Connector (%s) streaming: %s", d.Id(), err) + } + + if resp == nil || resp.StreamingConfiguration == nil { + return diag.Errorf("error getting Chime Voice Connector (%s) streaming: empty response", d.Id()) + } + + d.Set("disabled", resp.StreamingConfiguration.Disabled) + d.Set("data_retention", resp.StreamingConfiguration.DataRetentionInHours) + d.Set("voice_connector_id", d.Id()) + + if err := d.Set("streaming_notification_targets", flattenStreamingNotificationTargets(resp.StreamingConfiguration.StreamingNotificationTargets)); err != nil { + return diag.Errorf("error setting Chime Voice Connector streaming configuration targets (%s): %s", d.Id(), err) + } + + return nil +} + +func resourceAwsChimeVoiceConnectorStreamingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + vcId := d.Get("voice_connector_id").(string) + + if d.HasChanges("data_retention", "disabled", "streaming_notification_targets") { + input := &chime.PutVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(vcId), + } + + config := &chime.StreamingConfiguration{ + DataRetentionInHours: aws.Int64(int64(d.Get("data_retention").(int))), + Disabled: aws.Bool(d.Get("disabled").(bool)), + } + + if v, ok := d.GetOk("streaming_notification_targets"); ok && v.(*schema.Set).Len() > 0 { + config.StreamingNotificationTargets = expandStreamingNotificationTargets(v.(*schema.Set).List()) + } + + input.StreamingConfiguration = config + + if _, err := conn.PutVoiceConnectorStreamingConfigurationWithContext(ctx, input); err != nil { + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] error getting Chime Voice Connector (%s) streaming configuration", d.Id()) + d.SetId("") + return nil + } + return diag.Errorf("error updating Chime Voice Connector (%s) streaming configuration: %s", d.Id(), err) + } + } + + return resourceAwsChimeVoiceConnectorStreamingRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorStreamingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.DeleteVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + _, err := conn.DeleteVoiceConnectorStreamingConfigurationWithContext(ctx, input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + return nil + } + + if err != nil { + return diag.Errorf("error deleting Chime Voice Connector (%s) streaming configuration: %s", d.Id(), err) + } + + return nil +} + +func expandStreamingNotificationTargets(data []interface{}) []*chime.StreamingNotificationTarget { + var streamingTargets []*chime.StreamingNotificationTarget + + for _, item := range data { + streamingTargets = append(streamingTargets, &chime.StreamingNotificationTarget{ + NotificationTarget: aws.String(item.(string)), + }) + } + + return streamingTargets +} + +func flattenStreamingNotificationTargets(targets []*chime.StreamingNotificationTarget) []*string { + var rawTargets []*string + + for _, t := range targets { + rawTargets = append(rawTargets, t.NotificationTarget) + } + + return rawTargets +} diff --git a/aws/resource_aws_chime_voice_connector_streaming_test.go b/aws/resource_aws_chime_voice_connector_streaming_test.go new file mode 100644 index 000000000000..8834fb7abbaf --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_streaming_test.go @@ -0,0 +1,186 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnectorStreaming_basic(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_streaming.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorStreamingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorStreamingConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorStreamingExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "data_retention", "5"), + resource.TestCheckResourceAttr(resourceName, "disabled", "false"), + resource.TestCheckResourceAttr(resourceName, "streaming_notification_targets.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorStreaming_disappears(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_streaming.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorStreamingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorStreamingConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorStreamingExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnectorStreaming(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorStreaming_update(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_streaming.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorStreamingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorStreamingConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorStreamingExists(resourceName), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorStreamingUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorStreamingExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "data_retention", "2"), + resource.TestCheckResourceAttr(resourceName, "disabled", "false"), + resource.TestCheckResourceAttr(resourceName, "streaming_notification_targets.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSChimeVoiceConnectorStreamingConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_streaming" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + + disabled = false + data_retention = 5 + streaming_notification_targets = ["SQS"] +} +`, name) +} + +func testAccAWSChimeVoiceConnectorStreamingUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_streaming" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + + disabled = false + data_retention = 2 + streaming_notification_targets = ["SQS", "SNS"] +} +`, name) +} + +func testAccCheckAWSChimeVoiceConnectorStreamingExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime Voice Connector streaming configuration ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorStreamingConfiguration(input) + if err != nil { + return err + } + + if resp == nil || resp.StreamingConfiguration == nil { + return fmt.Errorf("no Chime Voice Connector Streaming configuration (%s) found", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAWSChimeVoiceConnectorStreamingDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_chime_voice_connector_termination" { + continue + } + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorStreamingConfigurationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + resp, err := conn.GetVoiceConnectorStreamingConfiguration(input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + continue + } + + if err != nil { + return err + } + + if resp != nil && resp.StreamingConfiguration != nil { + return fmt.Errorf("error Chime Voice Connector streaming configuration still exists") + } + } + + return nil +} diff --git a/aws/resource_aws_chime_voice_connector_termination.go b/aws/resource_aws_chime_voice_connector_termination.go new file mode 100644 index 000000000000..9ae2aff05e14 --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_termination.go @@ -0,0 +1,196 @@ +package aws + +import ( + "context" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func resourceAwsChimeVoiceConnectorTermination() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsChimeVoiceConnectorTerminationCreate, + ReadWithoutTimeout: resourceAwsChimeVoiceConnectorTerminationRead, + UpdateWithoutTimeout: resourceAwsChimeVoiceConnectorTerminationUpdate, + DeleteWithoutTimeout: resourceAwsChimeVoiceConnectorTerminationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "calling_regions": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(2, 2), + }, + }, + "cidr_allow_list": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsCIDRNetwork(27, 32), + }, + }, + "cps_limit": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ValidateFunc: validation.IntAtLeast(1), + }, + "default_phone_number": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^\+?[1-9]\d{1,14}$`), "must match ^\\+?[1-9]\\d{1,14}$"), + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + }, + "voice_connector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsChimeVoiceConnectorTerminationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + vcId := d.Get("voice_connector_id").(string) + + input := &chime.PutVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(vcId), + } + + termination := &chime.Termination{ + CidrAllowedList: expandStringSet(d.Get("cidr_allow_list").(*schema.Set)), + CallingRegions: expandStringSet(d.Get("calling_regions").(*schema.Set)), + } + + if v, ok := d.GetOk("disabled"); ok { + termination.Disabled = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("cps_limit"); ok { + termination.CpsLimit = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("default_phone_number"); ok { + termination.DefaultPhoneNumber = aws.String(v.(string)) + } + + input.Termination = termination + + if _, err := conn.PutVoiceConnectorTerminationWithContext(ctx, input); err != nil { + return diag.Errorf("error creating Chime Voice Connector (%s) termination: %s", vcId, err) + } + + d.SetId(vcId) + + return resourceAwsChimeVoiceConnectorTerminationRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorTerminationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.GetVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + resp, err := conn.GetVoiceConnectorTerminationWithContext(ctx, input) + + if !d.IsNewResource() && isAWSErr(err, chime.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Chime Voice Connector (%s) termination not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error getting Chime Voice Connector (%s) termination: %s", d.Id(), err) + } + + if resp == nil || resp.Termination == nil { + return diag.Errorf("error getting Chime Voice Connector (%s) termination: empty response", d.Id()) + } + + d.Set("cps_limit", resp.Termination.CpsLimit) + d.Set("disabled", resp.Termination.Disabled) + d.Set("default_phone_number", resp.Termination.DefaultPhoneNumber) + + if err := d.Set("calling_regions", flattenStringList(resp.Termination.CallingRegions)); err != nil { + return diag.Errorf("error setting termination calling regions (%s): %s", d.Id(), err) + } + if err := d.Set("cidr_allow_list", flattenStringList(resp.Termination.CidrAllowedList)); err != nil { + return diag.Errorf("error setting termination cidr allow list (%s): %s", d.Id(), err) + } + + d.Set("voice_connector_id", d.Id()) + + return nil +} + +func resourceAwsChimeVoiceConnectorTerminationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + if d.HasChanges("calling_regions", "cidr_allow_list", "disabled", "cps_limit", "default_phone_number") { + termination := &chime.Termination{ + CallingRegions: expandStringSet(d.Get("calling_regions").(*schema.Set)), + CidrAllowedList: expandStringSet(d.Get("cidr_allow_list").(*schema.Set)), + CpsLimit: aws.Int64(int64(d.Get("cps_limit").(int))), + } + + if v, ok := d.GetOk("default_phone_number"); ok { + termination.DefaultPhoneNumber = aws.String(v.(string)) + } + + if v, ok := d.GetOk("disabled"); ok { + termination.Disabled = aws.Bool(v.(bool)) + } + + input := &chime.PutVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(d.Id()), + Termination: termination, + } + + _, err := conn.PutVoiceConnectorTerminationWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error updating Chime Voice Connector (%s) termination: %s", d.Id(), err) + } + } + + return resourceAwsChimeVoiceConnectorTerminationRead(ctx, d, meta) +} + +func resourceAwsChimeVoiceConnectorTerminationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).chimeconn + + input := &chime.DeleteVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(d.Id()), + } + + _, err := conn.DeleteVoiceConnectorTerminationWithContext(ctx, input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + return nil + } + + if err != nil { + return diag.Errorf("error deleting Chime Voice Connector termination (%s): %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_chime_voice_connector_termination_test.go b/aws/resource_aws_chime_voice_connector_termination_test.go new file mode 100644 index 000000000000..901b020d9cce --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_termination_test.go @@ -0,0 +1,187 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnectorTermination_basic(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_termination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorTerminationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorTerminationConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorTerminationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cps_limit", "1"), + resource.TestCheckResourceAttr(resourceName, "calling_regions.#", "2"), + resource.TestCheckResourceAttr(resourceName, "cidr_allow_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "disabled", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorTermination_disappears(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_termination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorTerminationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorTerminationConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorTerminationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnectorTermination(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnectorTermination_update(t *testing.T) { + name := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector_termination.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorTerminationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorTerminationConfig(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorTerminationExists(resourceName), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorTerminationUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorTerminationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cps_limit", "1"), + resource.TestCheckResourceAttr(resourceName, "calling_regions.#", "3"), + resource.TestCheckTypeSetElemAttr(resourceName, "cidr_allow_list.*", "100.35.78.97/32"), + resource.TestCheckResourceAttr(resourceName, "disabled", "false"), + resource.TestCheckResourceAttr(resourceName, "default_phone_number", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSChimeVoiceConnectorTerminationConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_termination" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + + calling_regions = ["US", "RU"] + cidr_allow_list = ["50.35.78.97/32"] +} +`, name) +} + +func testAccAWSChimeVoiceConnectorTerminationUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "chime" { + name = "vc-%[1]s" + require_encryption = true +} + +resource "aws_chime_voice_connector_termination" "test" { + voice_connector_id = aws_chime_voice_connector.chime.id + disabled = false + calling_regions = ["US", "RU", "CA"] + cidr_allow_list = ["100.35.78.97/32"] +} +`, name) +} + +func testAccCheckAWSChimeVoiceConnectorTerminationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime Voice Connector termination ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetVoiceConnectorTermination(input) + if err != nil { + return err + } + + if resp == nil || resp.Termination == nil { + return fmt.Errorf("Chime Voice Connector Termintation (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAWSChimeVoiceConnectorTerminationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_chime_voice_connector_termination" { + continue + } + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorTerminationInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + resp, err := conn.GetVoiceConnectorTermination(input) + + if isAWSErr(err, chime.ErrCodeNotFoundException, "") { + continue + } + + if err != nil { + return err + } + + if resp != nil && resp.Termination != nil { + return fmt.Errorf("error Chime Voice Connector Termination still exists") + } + } + + return nil +} diff --git a/aws/resource_aws_chime_voice_connector_test.go b/aws/resource_aws_chime_voice_connector_test.go new file mode 100644 index 000000000000..061f2d6c4f29 --- /dev/null +++ b/aws/resource_aws_chime_voice_connector_test.go @@ -0,0 +1,166 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/chime" + "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" +) + +func TestAccAWSChimeVoiceConnector_basic(t *testing.T) { + var voiceConnector *chime.VoiceConnector + + vcName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorConfig(vcName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorExists(resourceName, voiceConnector), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("vc-%s", vcName)), + resource.TestCheckResourceAttr(resourceName, "aws_region", chime.VoiceConnectorAwsRegionUsEast1), + resource.TestCheckResourceAttr(resourceName, "require_encryption", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnector_disappears(t *testing.T) { + var voiceConnector *chime.VoiceConnector + + vcName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorConfig(vcName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorExists(resourceName, voiceConnector), + testAccCheckResourceDisappears(testAccProvider, resourceAwsChimeVoiceConnector(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSChimeVoiceConnector_update(t *testing.T) { + var voiceConnector *chime.VoiceConnector + + vcName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_chime_voice_connector.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, chime.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSChimeVoiceConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSChimeVoiceConnectorConfig(vcName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSChimeVoiceConnectorExists(resourceName, voiceConnector), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("vc-%s", vcName)), + resource.TestCheckResourceAttr(resourceName, "aws_region", chime.VoiceConnectorAwsRegionUsEast1), + resource.TestCheckResourceAttr(resourceName, "require_encryption", "true"), + ), + }, + { + Config: testAccAWSChimeVoiceConnectorUpdated(vcName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "require_encryption", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSChimeVoiceConnectorConfig(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "test" { + name = "vc-%s" + require_encryption = true +} +`, name) +} + +func testAccAWSChimeVoiceConnectorUpdated(name string) string { + return fmt.Sprintf(` +resource "aws_chime_voice_connector" "test" { + name = "vc-%s" + require_encryption = false +} +`, name) +} + +func testAccCheckAWSChimeVoiceConnectorExists(name string, vc *chime.VoiceConnector) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Chime voice connector ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + resp, err := conn.GetVoiceConnector(input) + if err != nil { + return err + } + + vc = resp.VoiceConnector + + return nil + } +} + +func testAccCheckAWSChimeVoiceConnectorDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_chime_voice_connector" { + continue + } + conn := testAccProvider.Meta().(*AWSClient).chimeconn + input := &chime.GetVoiceConnectorInput{ + VoiceConnectorId: aws.String(rs.Primary.ID), + } + resp, err := conn.GetVoiceConnector(input) + if err == nil { + if resp.VoiceConnector != nil && aws.StringValue(resp.VoiceConnector.Name) != "" { + return fmt.Errorf("error Chime Voice Connector still exists") + } + } + return nil + } + return nil +} diff --git a/aws/resource_aws_cloud9_environment_ec2.go b/aws/resource_aws_cloud9_environment_ec2.go index 24ddbdf312af..f2530ece4473 100644 --- a/aws/resource_aws_cloud9_environment_ec2.go +++ b/aws/resource_aws_cloud9_environment_ec2.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) func resourceAwsCloud9EnvironmentEc2() *schema.Resource { @@ -62,19 +63,24 @@ func resourceAwsCloud9EnvironmentEc2() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsCloud9EnvironmentEc2Create(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloud9conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) params := &cloud9.CreateEnvironmentEC2Input{ InstanceType: aws.String(d.Get("instance_type").(string)), Name: aws.String(d.Get("name").(string)), ClientRequestToken: aws.String(resource.UniqueId()), - Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().Cloud9Tags(), + Tags: tags.IgnoreAws().Cloud9Tags(), } if v, ok := d.GetOk("automatic_stop_time_minutes"); ok { @@ -91,7 +97,7 @@ func resourceAwsCloud9EnvironmentEc2Create(d *schema.ResourceData, meta interfac } var out *cloud9.CreateEnvironmentEC2Output - err := resource.Retry(1*time.Minute, func() *resource.RetryError { + err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { var err error out, err = conn.CreateEnvironmentEC2(params) if err != nil { @@ -129,13 +135,13 @@ func resourceAwsCloud9EnvironmentEc2Create(d *schema.ResourceData, meta interfac return 42, "", err } - status := *out.Status - var sErr error + status := aws.StringValue(out.Status) + if status == cloud9.EnvironmentStatusError && out.Message != nil { - sErr = fmt.Errorf("Reason: %s", *out.Message) + return out, status, fmt.Errorf("Reason: %s", aws.StringValue(out.Message)) } - return out, status, sErr + return out, status, nil }, } _, err = stateConf.WaitForState() @@ -148,6 +154,7 @@ func resourceAwsCloud9EnvironmentEc2Create(d *schema.ResourceData, meta interfac func resourceAwsCloud9EnvironmentEc2Read(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cloud9conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig log.Printf("[INFO] Reading Cloud9 Environment EC2 %s", d.Id()) @@ -183,8 +190,15 @@ func resourceAwsCloud9EnvironmentEc2Read(d *schema.ResourceData, meta interface{ return fmt.Errorf("error listing tags for Cloud9 EC2 Environment (%s): %s", arn, err) } - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } log.Printf("[DEBUG] Received Cloud9 Environment EC2: %s", env) @@ -210,8 +224,8 @@ func resourceAwsCloud9EnvironmentEc2Update(d *schema.ResourceData, meta interfac log.Printf("[DEBUG] Cloud9 Environment EC2 updated: %s", out) - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") arn := d.Get("arn").(string) if err := keyvaluetags.Cloud9UpdateTags(conn, arn, o, n); err != nil { diff --git a/aws/resource_aws_cloud9_environment_ec2_test.go b/aws/resource_aws_cloud9_environment_ec2_test.go index 2d60717ebec4..de8e3d6c532a 100644 --- a/aws/resource_aws_cloud9_environment_ec2_test.go +++ b/aws/resource_aws_cloud9_environment_ec2_test.go @@ -22,6 +22,7 @@ func TestAccAWSCloud9EnvironmentEc2_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloud9.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloud9.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloud9EnvironmentEc2Destroy, Steps: []resource.TestStep{ @@ -69,6 +70,7 @@ func TestAccAWSCloud9EnvironmentEc2_allFields(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloud9.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloud9.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloud9EnvironmentEc2Destroy, Steps: []resource.TestStep{ @@ -112,6 +114,7 @@ func TestAccAWSCloud9EnvironmentEc2_tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloud9.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloud9.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloud9EnvironmentEc2Destroy, Steps: []resource.TestStep{ @@ -158,6 +161,7 @@ func TestAccAWSCloud9EnvironmentEc2_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(cloud9.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, cloud9.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloud9EnvironmentEc2Destroy, Steps: []resource.TestStep{ diff --git a/aws/resource_aws_cloudcontrolapi_resource.go b/aws/resource_aws_cloudcontrolapi_resource.go new file mode 100644 index 000000000000..c337532af551 --- /dev/null +++ b/aws/resource_aws_cloudcontrolapi_resource.go @@ -0,0 +1,335 @@ +package aws + +import ( + "context" + "encoding/json" + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + cfschema "github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "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/mattbaird/jsonpatch" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudcontrol/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudcontrol/waiter" + cffinder "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsCloudControlApiResource() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsCloudControlApiResourceCreate, + DeleteContext: resourceAwsCloudControlApiResourceDelete, + ReadContext: resourceAwsCloudControlApiResourceRead, + UpdateContext: resourceAwsCloudControlApiResourceUpdate, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Hour), + Delete: schema.DefaultTimeout(2 * time.Hour), + Update: schema.DefaultTimeout(2 * time.Hour), + }, + + Schema: map[string]*schema.Schema{ + "desired_state": { + Type: schema.TypeString, + Required: true, + }, + "properties": { + Type: schema.TypeString, + Computed: true, + }, + "role_arn": { + Type: schema.TypeString, + Optional: true, + }, + "schema": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + }, + "type_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}::[A-Za-z0-9]{2,64}`), "must be three alphanumeric sections separated by double colons (::)"), + }, + "type_version_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + + CustomizeDiff: customdiff.Sequence( + resourceAwsCloudControlApiResourceCustomizeDiffGetSchema, + resourceAwsCloudControlApiResourceCustomizeDiffSchemaDiff, + customdiff.ComputedIf("properties", func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + return diff.HasChange("desired_state") + }), + ), + } +} + +func resourceAwsCloudControlApiResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudcontrolapiconn + + typeName := d.Get("type_name").(string) + input := &cloudcontrolapi.CreateResourceInput{ + ClientToken: aws.String(resource.UniqueId()), + DesiredState: aws.String(d.Get("desired_state").(string)), + TypeName: aws.String(typeName), + } + + if v, ok := d.GetOk("role_arn"); ok { + input.RoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("type_version_id"); ok { + input.TypeVersionId = aws.String(v.(string)) + } + + output, err := conn.CreateResourceWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Cloud Control API Resource (%s): %w", typeName, err)) + } + + if output == nil || output.ProgressEvent == nil { + return diag.FromErr(fmt.Errorf("error creating Cloud Control API Resource (%s): empty result", typeName)) + } + + // Always try to capture the identifier before returning errors + d.SetId(aws.StringValue(output.ProgressEvent.Identifier)) + + output.ProgressEvent, err = waiter.ProgressEventOperationStatusSuccess(ctx, conn, aws.StringValue(output.ProgressEvent.RequestToken), d.Timeout(schema.TimeoutCreate)) + + if err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Cloud Control API Resource (%s) create: %w", d.Id(), err)) + } + + // Some resources do not set the identifier until after creation + if d.Id() == "" { + d.SetId(aws.StringValue(output.ProgressEvent.Identifier)) + } + + return resourceAwsCloudControlApiResourceRead(ctx, d, meta) +} + +func resourceAwsCloudControlApiResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudcontrolapiconn + + resourceDescription, err := finder.ResourceByID(ctx, conn, + d.Id(), + d.Get("type_name").(string), + d.Get("type_version_id").(string), + d.Get("role_arn").(string), + ) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Cloud Control API Resource (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Cloud Control API Resource (%s): %w", d.Id(), err)) + } + + d.Set("properties", resourceDescription.Properties) + + return nil +} + +func resourceAwsCloudControlApiResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudcontrolapiconn + + if d.HasChange("desired_state") { + oldRaw, newRaw := d.GetChange("desired_state") + + patchDocument, err := patchDocument(oldRaw.(string), newRaw.(string)) + + if err != nil { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "JSON Patch Creation Unsuccessful", + Detail: fmt.Sprintf("Creating JSON Patch failed: %s", err.Error()), + }, + } + } + + input := &cloudcontrolapi.UpdateResourceInput{ + ClientToken: aws.String(resource.UniqueId()), + Identifier: aws.String(d.Id()), + PatchDocument: aws.String(patchDocument), + TypeName: aws.String(d.Get("type_name").(string)), + } + + if v, ok := d.GetOk("role_arn"); ok { + input.RoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("type_version_id"); ok { + input.TypeVersionId = aws.String(v.(string)) + } + + output, err := conn.UpdateResourceWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Cloud Control API Resource (%s): %w", d.Id(), err)) + } + + if output == nil || output.ProgressEvent == nil { + return diag.FromErr(fmt.Errorf("error updating Cloud Control API Resource (%s): empty result", d.Id())) + } + + if _, err := waiter.ProgressEventOperationStatusSuccess(ctx, conn, aws.StringValue(output.ProgressEvent.RequestToken), d.Timeout(schema.TimeoutUpdate)); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Cloud Control API Resource (%s) update: %w", d.Id(), err)) + } + } + + return resourceAwsCloudControlApiResourceRead(ctx, d, meta) +} + +func resourceAwsCloudControlApiResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).cloudcontrolapiconn + + input := &cloudcontrolapi.DeleteResourceInput{ + ClientToken: aws.String(resource.UniqueId()), + Identifier: aws.String(d.Id()), + TypeName: aws.String(d.Get("type_name").(string)), + } + + if v, ok := d.GetOk("role_arn"); ok { + input.RoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("type_version_id"); ok { + input.TypeVersionId = aws.String(v.(string)) + } + + output, err := conn.DeleteResourceWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting Cloud Control API Resource (%s): %w", d.Id(), err)) + } + + if output == nil || output.ProgressEvent == nil { + return diag.FromErr(fmt.Errorf("error deleting Cloud Control API Resource (%s): empty result", d.Id())) + } + + progressEvent, err := waiter.ProgressEventOperationStatusSuccess(ctx, conn, aws.StringValue(output.ProgressEvent.RequestToken), d.Timeout(schema.TimeoutDelete)) + + if progressEvent != nil && aws.StringValue(progressEvent.ErrorCode) == cloudcontrolapi.HandlerErrorCodeNotFound { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Cloud Control API Resource (%s) delete: %w", d.Id(), err)) + } + + return nil +} + +func resourceAwsCloudControlApiResourceCustomizeDiffGetSchema(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + + resourceSchema := diff.Get("schema").(string) + + if resourceSchema != "" { + return nil + } + + typeName := diff.Get("type_name").(string) + + output, err := cffinder.TypeByName(ctx, conn, typeName) + + if err != nil { + return fmt.Errorf("error reading CloudFormation Type (%s): %w", typeName, err) + } + + if err := diff.SetNew("schema", output.Schema); err != nil { + return fmt.Errorf("error setting schema diff: %w", err) + } + + return nil +} + +func resourceAwsCloudControlApiResourceCustomizeDiffSchemaDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + oldDesiredStateRaw, newDesiredStateRaw := diff.GetChange("desired_state") + newSchema := diff.Get("schema").(string) + + newDesiredState, ok := newDesiredStateRaw.(string) + + if !ok { + return fmt.Errorf("unexpected new desired_state value type: %T", newDesiredStateRaw) + } + + // desired_state can be empty if unknown + if newDesiredState == "" { + return nil + } + + cfResourceSchema, err := cfschema.NewResourceJsonSchemaDocument(cfschema.Sanitize(newSchema)) + + if err != nil { + return fmt.Errorf("error parsing CloudFormation Resource Schema JSON: %w", err) + } + + if err := cfResourceSchema.ValidateConfigurationDocument(newDesiredState); err != nil { + return fmt.Errorf("error validating desired_state against CloudFormation Resource Schema: %w", err) + } + + // Do nothing further for new resources or if desired state is not changed + if diff.Id() == "" || !diff.HasChange("desired_state") { + return nil + } + + cfResource, err := cfResourceSchema.Resource() + + if err != nil { + return fmt.Errorf("error converting CloudFormation Resource Schema JSON: %w", err) + } + + patches, err := jsonpatch.CreatePatch([]byte(oldDesiredStateRaw.(string)), []byte(newDesiredStateRaw.(string))) + + if err != nil { + return fmt.Errorf("error creating desired_state JSON Patch: %w", err) + } + + for _, patch := range patches { + if cfResource.IsCreateOnlyPropertyPath(patch.Path) { + if err := diff.ForceNew("desired_state"); err != nil { + return fmt.Errorf("error setting desired_state ForceNew: %w", err) + } + + break + } + } + + return nil +} + +// patchDocument returns a JSON Patch document describing the difference between `old` and `new`. +func patchDocument(old, new string) (string, error) { + patch, err := jsonpatch.CreatePatch([]byte(old), []byte(new)) + + if err != nil { + return "", err + } + + b, err := json.Marshal(patch) + + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/aws/resource_aws_cloudcontrolapi_resource_test.go b/aws/resource_aws_cloudcontrolapi_resource_test.go new file mode 100644 index 000000000000..ed181294d677 --- /dev/null +++ b/aws/resource_aws_cloudcontrolapi_resource_test.go @@ -0,0 +1,664 @@ +package aws + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudcontrolapi" + "github.com/aws/aws-sdk-go/service/cloudformation" + "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/terraform-providers/terraform-provider-aws/aws/internal/service/cloudcontrol/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + RegisterServiceErrorCheckFunc(cloudcontrolapi.EndpointsID, testAccErrorCheckSkipCloudControlAPI) +} + +func testAccErrorCheckSkipCloudControlAPI(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "UnsupportedActionException", + ) +} + +func TestAccAwsCloudControlApiResource_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`^\{.*\}$`)), + resource.TestMatchResourceAttr(resourceName, "schema", regexp.MustCompile(`^\{.*`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckResourceDisappears(testAccProvider, resourceAwsCloudControlApiResource(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_BooleanValueAdded(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":false`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValue(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":true`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_BooleanValueRemoved(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValue(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":true`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":false`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_BooleanValueUpdate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValue(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":true`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValue(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Enabled":false`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_CreateOnly(t *testing.T) { + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateCreateOnly(rName1), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"LogGroupName":"`+rName1+`"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateCreateOnly(rName2), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"LogGroupName":"`+rName2+`"`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_IntegerValueAdded(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"LogGroupName":`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValue(rName, 14), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"RetentionInDays":14`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_IntegerValueRemoved(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValue(rName, 14), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"RetentionInDays":14`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"LogGroupName":`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_IntegerValueUpdate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValue(rName, 7), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"RetentionInDays":7`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValue(rName, 14), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"RetentionInDays":14`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_InvalidPropertyName(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateInvalidPropertyName(rName), + ExpectError: regexp.MustCompile(`\(root\): Additional property InvalidName is not allowed`), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_InvalidPropertyValue(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateInvalidPropertyValue(rName), + ExpectError: regexp.MustCompile(`Model validation failed`), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_ObjectValueAdded(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Name":`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Value":"value1"`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_ObjectValueRemoved(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Value":"value1"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Name":`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_ObjectValueUpdate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Value":"value1"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName, "key1", "value1updated"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Value":"value1updated"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName, "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Value":"value2"`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_StringValueAdded(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Name":`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValue(rName, "description1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Description":"description1"`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_StringValueRemoved(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValue(rName, "description1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Description":"description1"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValueRemoved(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Name":`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_DesiredState_StringValueUpdate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValue(rName, "description1"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Description":"description1"`)), + ), + }, + { + Config: testAccAwsCloudControlApiResourceConfigDesiredStateStringValue(rName, "description2"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "properties", regexp.MustCompile(`"Description":"description2"`)), + ), + }, + }, + }) +} + +func TestAccAwsCloudControlApiResource_ResourceSchema(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudcontrolapi_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, cloudcontrolapi.EndpointsID, cloudformation.EndpointsID), + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsCloudControlApiResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsCloudControlApiResourceConfigResourceSchema(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "schema", "data.aws_cloudformation_type.test", "schema"), + ), + }, + }, + }) +} + +func testAccCheckAwsCloudControlApiResourceDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudcontrolapiconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudcontrolapi_resource" { + continue + } + + _, err := finder.ResourceByID(context.TODO(), conn, rs.Primary.ID, rs.Primary.Attributes["type_name"], "", "") + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Cloud Control API Resource %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAwsCloudControlApiResourceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValue(rName string, booleanValue bool) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::ApiGateway::ApiKey" + + desired_state = jsonencode({ + Enabled = %[2]t + Name = %[1]q + Value = %[1]q + }) +} +`, rName, booleanValue) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateBooleanValueRemoved(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::ApiGateway::ApiKey" + + desired_state = jsonencode({ + Name = %[1]q + Value = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateCreateOnly(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValue(rName string, integerValue int) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = %[1]q + RetentionInDays = %[2]d + }) +} +`, rName, integerValue) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateIntegerValueRemoved(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateInvalidPropertyName(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + InvalidName = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateInvalidPropertyValue(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Logs::LogGroup" + + desired_state = jsonencode({ + LogGroupName = "%[1]s!exclamation-not-valid" + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateObjectValue1(rName string, key1 string, value1 string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::ECS::Cluster" + + desired_state = jsonencode({ + ClusterName = %[1]q + Tags = [ + { + Key = %[2]q + Value = %[3]q + } + ] + }) +} +`, rName, key1, value1) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateObjectValueRemoved(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::ECS::Cluster" + + desired_state = jsonencode({ + ClusterName = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateStringValue(rName string, stringValue string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Athena::WorkGroup" + + desired_state = jsonencode({ + Description = %[2]q + Name = %[1]q + }) +} +`, rName, stringValue) +} + +func testAccAwsCloudControlApiResourceConfigDesiredStateStringValueRemoved(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudcontrolapi_resource" "test" { + type_name = "AWS::Athena::WorkGroup" + + desired_state = jsonencode({ + Name = %[1]q + }) +} +`, rName) +} + +func testAccAwsCloudControlApiResourceConfigResourceSchema(rName string) string { + return fmt.Sprintf(` +data "aws_cloudformation_type" "test" { + type = "RESOURCE" + type_name = "AWS::Logs::LogGroup" +} + +resource "aws_cloudcontrolapi_resource" "test" { + schema = data.aws_cloudformation_type.test.schema + type_name = data.aws_cloudformation_type.test.type_name + + desired_state = jsonencode({ + LogGroupName = %[1]q + }) +} +`, rName) +} diff --git a/aws/resource_aws_cloudformation_stack.go b/aws/resource_aws_cloudformation_stack.go index 69d0cfca6c0b..044683076d68 100644 --- a/aws/resource_aws_cloudformation_stack.go +++ b/aws/resource_aws_cloudformation_stack.go @@ -108,17 +108,22 @@ func resourceAwsCloudFormationStack() *schema.Resource { Optional: true, ForceNew: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "iam_role_arn": { Type: schema.TypeString, Optional: true, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) requestToken := resource.UniqueId() input := cloudformation.CreateStackInput{ @@ -160,8 +165,8 @@ func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface if v, ok := d.GetOk("policy_url"); ok { input.StackPolicyURL = aws.String(v.(string)) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudformationTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().CloudformationTags() } if v, ok := d.GetOk("timeout_in_minutes"); ok { m := int64(v.(int)) @@ -198,6 +203,7 @@ func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig input := &cloudformation.DescribeStacksInput{ @@ -255,6 +261,12 @@ func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{} } if stack.DisableRollback != nil { d.Set("disable_rollback", stack.DisableRollback) + + // takes into account that disable_rollback conflicts with on_failure and + // prevents forced new creation if disable_rollback is reset during refresh + if d.Get("on_failure") != nil { + d.Set("disable_rollback", false) + } } if len(stack.NotificationARNs) > 0 { err = d.Set("notification_arns", flattenStringSet(stack.NotificationARNs)) @@ -269,8 +281,15 @@ func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{} return err } - if err := d.Set("tags", keyvaluetags.CloudformationKeyValueTags(stack.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.CloudformationKeyValueTags(stack.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs)) @@ -290,6 +309,8 @@ func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{} func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) requestToken := resource.UniqueId() input := &cloudformation.UpdateStackInput{ @@ -323,8 +344,8 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudformationTags() + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().CloudformationTags() } if d.HasChange("policy_body") { diff --git a/aws/resource_aws_cloudformation_stack_set.go b/aws/resource_aws_cloudformation_stack_set.go index deb79c5dcd7f..2e81251f95e4 100644 --- a/aws/resource_aws_cloudformation_stack_set.go +++ b/aws/resource_aws_cloudformation_stack_set.go @@ -7,11 +7,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsCloudFormationStackSet() *schema.Resource { @@ -31,24 +34,44 @@ func resourceAwsCloudFormationStackSet() *schema.Resource { Schema: map[string]*schema.Schema{ "administration_role_arn": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validateArn, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"auto_deployment"}, + ValidateFunc: validateArn, }, "arn": { Type: schema.TypeString, Computed: true, }, + "auto_deployment": { + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Optional: true, + ForceNew: true, + ConflictsWith: []string{ + "administration_role_arn", + "execution_role_name", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "retain_stacks_on_account_removal": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, "capabilities": { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.StringInSlice([]string{ - cloudformation.CapabilityCapabilityAutoExpand, - cloudformation.CapabilityCapabilityIam, - cloudformation.CapabilityCapabilityNamedIam, - }, false), + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(cloudformation.Capability_Values(), false), }, }, "description": { @@ -57,9 +80,10 @@ func resourceAwsCloudFormationStackSet() *schema.Resource { ValidateFunc: validation.StringLenBetween(0, 1024), }, "execution_role_name": { - Type: schema.TypeString, - Optional: true, - Default: "AWSCloudFormationStackSetExecutionRole", + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"auto_deployment"}, }, "name": { Type: schema.TypeString, @@ -76,11 +100,18 @@ func resourceAwsCloudFormationStackSet() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "permission_model": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cloudformation.PermissionModels_Values(), false), + Default: cloudformation.PermissionModelsSelfManaged, + }, "stack_set_id": { Type: schema.TypeString, Computed: true, }, - "tags": tagsSchema(), + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), "template_body": { Type: schema.TypeString, Optional: true, @@ -95,18 +126,28 @@ func resourceAwsCloudFormationStackSet() *schema.Resource { ConflictsWith: []string{"template_body"}, }, }, + + CustomizeDiff: SetTagsDiff, } } func resourceAwsCloudFormationStackSetCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn - name := d.Get("name").(string) + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + name := d.Get("name").(string) input := &cloudformation.CreateStackSetInput{ - AdministrationRoleARN: aws.String(d.Get("administration_role_arn").(string)), - ClientRequestToken: aws.String(resource.UniqueId()), - ExecutionRoleName: aws.String(d.Get("execution_role_name").(string)), - StackSetName: aws.String(name), + ClientRequestToken: aws.String(resource.UniqueId()), + StackSetName: aws.String(name), + } + + if v, ok := d.GetOk("administration_role_arn"); ok { + input.AdministrationRoleARN = aws.String(v.(string)) + } + + if v, ok := d.GetOk("auto_deployment"); ok { + input.AutoDeployment = expandAutoDeployment(v.([]interface{})) } if v, ok := d.GetOk("capabilities"); ok { @@ -117,12 +158,20 @@ func resourceAwsCloudFormationStackSetCreate(d *schema.ResourceData, meta interf input.Description = aws.String(v.(string)) } + if v, ok := d.GetOk("execution_role_name"); ok { + input.ExecutionRoleName = aws.String(v.(string)) + } + if v, ok := d.GetOk("parameters"); ok { input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudformationTags() + if v, ok := d.GetOk("permission_model"); ok { + input.PermissionModel = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().CloudformationTags() } if v, ok := d.GetOk("template_body"); ok { @@ -137,7 +186,7 @@ func resourceAwsCloudFormationStackSetCreate(d *schema.ResourceData, meta interf _, err := conn.CreateStackSet(input) if err != nil { - return fmt.Errorf("error creating CloudFormation StackSet: %s", err) + return fmt.Errorf("error creating CloudFormation StackSet (%s): %w", name, err) } d.SetId(name) @@ -147,34 +196,28 @@ func resourceAwsCloudFormationStackSetCreate(d *schema.ResourceData, meta interf func resourceAwsCloudFormationStackSetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - input := &cloudformation.DescribeStackSetInput{ - StackSetName: aws.String(d.Id()), - } - - log.Printf("[DEBUG] Reading CloudFormation StackSet: %s", d.Id()) - output, err := conn.DescribeStackSet(input) + stackSet, err := finder.StackSetByName(conn, d.Id()) - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] CloudFormation StackSet (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading CloudFormation StackSet (%s): %s", d.Id(), err) + return fmt.Errorf("error reading CloudFormation StackSet (%s): %w", d.Id(), err) } - if output == nil || output.StackSet == nil { - return fmt.Errorf("error reading CloudFormation StackSet (%s): empty response", d.Id()) - } - - stackSet := output.StackSet - d.Set("administration_role_arn", stackSet.AdministrationRoleARN) d.Set("arn", stackSet.StackSetARN) + if err := d.Set("auto_deployment", flattenStackSetAutoDeploymentResponse(stackSet.AutoDeployment)); err != nil { + return fmt.Errorf("error setting auto_deployment: %s", err) + } + if err := d.Set("capabilities", aws.StringValueSlice(stackSet.Capabilities)); err != nil { return fmt.Errorf("error setting capabilities: %s", err) } @@ -182,6 +225,7 @@ func resourceAwsCloudFormationStackSetRead(d *schema.ResourceData, meta interfac d.Set("description", stackSet.Description) d.Set("execution_role_name", stackSet.ExecutionRoleName) d.Set("name", stackSet.StackSetName) + d.Set("permission_model", stackSet.PermissionModel) if err := d.Set("parameters", flattenAllCloudFormationParameters(stackSet.Parameters)); err != nil { return fmt.Errorf("error setting parameters: %s", err) @@ -189,8 +233,15 @@ func resourceAwsCloudFormationStackSetRead(d *schema.ResourceData, meta interfac d.Set("stack_set_id", stackSet.StackSetId) - if err := d.Set("tags", keyvaluetags.CloudformationKeyValueTags(stackSet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + tags := keyvaluetags.CloudformationKeyValueTags(stackSet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } d.Set("template_body", stackSet.TemplateBody) @@ -200,14 +251,18 @@ func resourceAwsCloudFormationStackSetRead(d *schema.ResourceData, meta interfac func resourceAwsCloudFormationStackSetUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) input := &cloudformation.UpdateStackSetInput{ - AdministrationRoleARN: aws.String(d.Get("administration_role_arn").(string)), - ExecutionRoleName: aws.String(d.Get("execution_role_name").(string)), - OperationId: aws.String(resource.UniqueId()), - StackSetName: aws.String(d.Id()), - Tags: []*cloudformation.Tag{}, - TemplateBody: aws.String(d.Get("template_body").(string)), + OperationId: aws.String(resource.UniqueId()), + StackSetName: aws.String(d.Id()), + Tags: []*cloudformation.Tag{}, + TemplateBody: aws.String(d.Get("template_body").(string)), + } + + if v, ok := d.GetOk("administration_role_arn"); ok { + input.AdministrationRoleARN = aws.String(v.(string)) } if v, ok := d.GetOk("capabilities"); ok { @@ -218,12 +273,20 @@ func resourceAwsCloudFormationStackSetUpdate(d *schema.ResourceData, meta interf input.Description = aws.String(v.(string)) } + if v, ok := d.GetOk("execution_role_name"); ok { + input.ExecutionRoleName = aws.String(v.(string)) + } + if v, ok := d.GetOk("parameters"); ok { input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) } - if v, ok := d.GetOk("tags"); ok { - input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().CloudformationTags() + if v, ok := d.GetOk("permission_model"); ok { + input.PermissionModel = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().CloudformationTags() } if v, ok := d.GetOk("template_url"); ok { @@ -233,15 +296,24 @@ func resourceAwsCloudFormationStackSetUpdate(d *schema.ResourceData, meta interf input.TemplateURL = aws.String(v.(string)) } + // When `auto_deployment` is set, ignore `administration_role_arn` and + // `execution_role_name` fields since it's using the SERVICE_MANAGED + // permission model + if v, ok := d.GetOk("auto_deployment"); ok { + input.AdministrationRoleARN = nil + input.ExecutionRoleName = nil + input.AutoDeployment = expandAutoDeployment(v.([]interface{})) + } + log.Printf("[DEBUG] Updating CloudFormation StackSet: %s", input) output, err := conn.UpdateStackSet(input) if err != nil { - return fmt.Errorf("error updating CloudFormation StackSet (%s): %s", d.Id(), err) + return fmt.Errorf("error updating CloudFormation StackSet (%s): %w", d.Id(), err) } - if err := waiter.StackSetOperationSucceeded(conn, d.Id(), aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutUpdate)); err != nil { - return fmt.Errorf("error waiting for CloudFormation StackSet (%s) update: %s", d.Id(), err) + if _, err := waiter.StackSetOperationSucceeded(conn, d.Id(), aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutUpdate)); err != nil { + return fmt.Errorf("error waiting for CloudFormation StackSet (%s) update: %w", d.Id(), err) } return resourceAwsCloudFormationStackSetRead(d, meta) @@ -257,38 +329,41 @@ func resourceAwsCloudFormationStackSetDelete(d *schema.ResourceData, meta interf log.Printf("[DEBUG] Deleting CloudFormation StackSet: %s", d.Id()) _, err := conn.DeleteStackSet(input) - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { return nil } if err != nil { - return fmt.Errorf("error deleting CloudFormation StackSet (%s): %s", d.Id(), err) + return fmt.Errorf("error deleting CloudFormation StackSet (%s): %w", d.Id(), err) } return nil } -func listCloudFormationStackSets(conn *cloudformation.CloudFormation) ([]*cloudformation.StackSetSummary, error) { - input := &cloudformation.ListStackSetsInput{ - Status: aws.String(cloudformation.StackSetStatusActive), +func expandAutoDeployment(l []interface{}) *cloudformation.AutoDeployment { + if len(l) == 0 { + return nil } - result := make([]*cloudformation.StackSetSummary, 0) - for { - output, err := conn.ListStackSets(input) + m := l[0].(map[string]interface{}) - if err != nil { - return result, err - } + autoDeployment := &cloudformation.AutoDeployment{ + Enabled: aws.Bool(m["enabled"].(bool)), + RetainStacksOnAccountRemoval: aws.Bool(m["retain_stacks_on_account_removal"].(bool)), + } - result = append(result, output.Summaries...) + return autoDeployment +} - if aws.StringValue(output.NextToken) == "" { - break - } +func flattenStackSetAutoDeploymentResponse(autoDeployment *cloudformation.AutoDeployment) []map[string]interface{} { + if autoDeployment == nil { + return []map[string]interface{}{} + } - input.NextToken = output.NextToken + m := map[string]interface{}{ + "enabled": aws.BoolValue(autoDeployment.Enabled), + "retain_stacks_on_account_removal": aws.BoolValue(autoDeployment.RetainStacksOnAccountRemoval), } - return result, nil + return []map[string]interface{}{m} } diff --git a/aws/resource_aws_cloudformation_stack_set_instance.go b/aws/resource_aws_cloudformation_stack_set_instance.go index 9c11dfcea829..b094b472953b 100644 --- a/aws/resource_aws_cloudformation_stack_set_instance.go +++ b/aws/resource_aws_cloudformation_stack_set_instance.go @@ -7,11 +7,15 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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" + tfcloudformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/waiter" iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsCloudFormationStackSetInstance() *schema.Resource { @@ -83,10 +87,8 @@ func resourceAwsCloudFormationStackSetInstanceCreate(d *schema.ResourceData, met } stackSetName := d.Get("stack_set_name").(string) - input := &cloudformation.CreateStackInstancesInput{ Accounts: aws.StringSlice([]string{accountID}), - OperationId: aws.String(resource.UniqueId()), Regions: aws.StringSlice([]string{region}), StackSetName: aws.String(stackSetName), } @@ -96,73 +98,59 @@ func resourceAwsCloudFormationStackSetInstanceCreate(d *schema.ResourceData, met } log.Printf("[DEBUG] Creating CloudFormation StackSet Instance: %s", input) - err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { - output, err := conn.CreateStackInstances(input) + _, err := tfresource.RetryWhen( + iamwaiter.PropagationTimeout, + func() (interface{}, error) { + input.OperationId = aws.String(resource.UniqueId()) - if err != nil { - return resource.NonRetryableError(fmt.Errorf("error creating CloudFormation StackSet Instance: %w", err)) - } + output, err := conn.CreateStackInstances(input) - d.SetId(fmt.Sprintf("%s,%s,%s", stackSetName, accountID, region)) + if err != nil { + return nil, fmt.Errorf("error creating CloudFormation StackSet (%s) Instance: %w", stackSetName, err) + } - err = waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutCreate)) + d.SetId(tfcloudformation.StackSetInstanceCreateResourceID(stackSetName, accountID, region)) + + return waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutCreate)) + }, + func(err error) (bool, error) { + if err == nil { + return false, nil + } + + message := err.Error() - if err != nil { // IAM eventual consistency - if strings.Contains(err.Error(), "AccountGate check failed") { - input.OperationId = aws.String(resource.UniqueId()) - return resource.RetryableError(err) + if strings.Contains(message, "AccountGate check failed") { + return true, err } // IAM eventual consistency // User: XXX is not authorized to perform: cloudformation:CreateStack on resource: YYY - if strings.Contains(err.Error(), "is not authorized") { - input.OperationId = aws.String(resource.UniqueId()) - return resource.RetryableError(err) + if strings.Contains(message, "is not authorized") { + return true, err } // IAM eventual consistency // XXX role has insufficient YYY permissions - if strings.Contains(err.Error(), "role has insufficient") { - input.OperationId = aws.String(resource.UniqueId()) - return resource.RetryableError(err) + if strings.Contains(message, "role has insufficient") { + return true, err } // IAM eventual consistency // Account XXX should have YYY role with trust relationship to Role ZZZ - if strings.Contains(err.Error(), "role with trust relationship") { - input.OperationId = aws.String(resource.UniqueId()) - return resource.RetryableError(err) + if strings.Contains(message, "role with trust relationship") { + return true, err } // IAM eventual consistency - if strings.Contains(err.Error(), "The security token included in the request is invalid") { - input.OperationId = aws.String(resource.UniqueId()) - return resource.RetryableError(err) + if strings.Contains(message, "The security token included in the request is invalid") { + return true, err } - return resource.NonRetryableError(fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) creation: %w", d.Id(), err)) - } - - return nil - }) - - if isResourceTimeoutError(err) { - var output *cloudformation.CreateStackInstancesOutput - output, err = conn.CreateStackInstances(input) - - if err != nil { - return fmt.Errorf("error creating CloudFormation StackSet Instance: %w", err) - } - - d.SetId(fmt.Sprintf("%s,%s,%s", stackSetName, accountID, region)) - - err = waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutCreate)) - - if err != nil { - return fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) creation: %w", d.Id(), err) - } - } + return false, fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) creation: %w", d.Id(), err) + }, + ) if err != nil { return err @@ -174,47 +162,28 @@ func resourceAwsCloudFormationStackSetInstanceCreate(d *schema.ResourceData, met func resourceAwsCloudFormationStackSetInstanceRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn - stackSetName, accountID, region, err := resourceAwsCloudFormationStackSetInstanceParseId(d.Id()) + stackSetName, accountID, region, err := tfcloudformation.StackSetInstanceParseResourceID(d.Id()) if err != nil { return err } - input := &cloudformation.DescribeStackInstanceInput{ - StackInstanceAccount: aws.String(accountID), - StackInstanceRegion: aws.String(region), - StackSetName: aws.String(stackSetName), - } + stackInstance, err := finder.StackInstanceByName(conn, stackSetName, accountID, region) - log.Printf("[DEBUG] Reading CloudFormation StackSet Instance: %s", d.Id()) - output, err := conn.DescribeStackInstance(input) - - if isAWSErr(err, cloudformation.ErrCodeStackInstanceNotFoundException, "") { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { - log.Printf("[WARN] CloudFormation StackSet (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - if err != nil { - return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) - } - - if output == nil || output.StackInstance == nil { - return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): empty response", d.Id()) + return fmt.Errorf("error reading CloudFormation StackSet Instance (%s): %w", d.Id(), err) } - stackInstance := output.StackInstance - d.Set("account_id", stackInstance.Account) if err := d.Set("parameter_overrides", flattenAllCloudFormationParameters(stackInstance.ParameterOverrides)); err != nil { - return fmt.Errorf("error setting parameters: %s", err) + return fmt.Errorf("error setting parameters: %w", err) } d.Set("region", stackInstance.Region) @@ -228,7 +197,7 @@ func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, met conn := meta.(*AWSClient).cfconn if d.HasChange("parameter_overrides") { - stackSetName, accountID, region, err := resourceAwsCloudFormationStackSetInstanceParseId(d.Id()) + stackSetName, accountID, region, err := tfcloudformation.StackSetInstanceParseResourceID(d.Id()) if err != nil { return err @@ -250,10 +219,10 @@ func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, met output, err := conn.UpdateStackInstances(input) if err != nil { - return fmt.Errorf("error updating CloudFormation StackSet Instance (%s): %s", d.Id(), err) + return fmt.Errorf("error updating CloudFormation StackSet Instance (%s): %w", d.Id(), err) } - if err := waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutUpdate)); err != nil { + if _, err := waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutUpdate)); err != nil { return fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) update: %s", d.Id(), err) } } @@ -264,7 +233,7 @@ func resourceAwsCloudFormationStackSetInstanceUpdate(d *schema.ResourceData, met func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cfconn - stackSetName, accountID, region, err := resourceAwsCloudFormationStackSetInstanceParseId(d.Id()) + stackSetName, accountID, region, err := tfcloudformation.StackSetInstanceParseResourceID(d.Id()) if err != nil { return err @@ -281,11 +250,7 @@ func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, met log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) output, err := conn.DeleteStackInstances(input) - if isAWSErr(err, cloudformation.ErrCodeStackInstanceNotFoundException, "") { - return nil - } - - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { + if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackInstanceNotFoundException) || tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { return nil } @@ -293,45 +258,9 @@ func resourceAwsCloudFormationStackSetInstanceDelete(d *schema.ResourceData, met return fmt.Errorf("error deleting CloudFormation StackSet Instance (%s): %s", d.Id(), err) } - if err := waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutDelete)); err != nil { + if _, err := waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), d.Timeout(schema.TimeoutDelete)); err != nil { return fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) deletion: %s", d.Id(), err) } return nil } - -func resourceAwsCloudFormationStackSetInstanceParseId(id string) (string, string, string, error) { - idFormatErr := fmt.Errorf("unexpected format of ID (%s), expected NAME,ACCOUNT,REGION", id) - - parts := strings.SplitN(id, ",", 3) - if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { - return "", "", "", idFormatErr - } - - return parts[0], parts[1], parts[2], nil -} - -func listCloudFormationStackSetInstances(conn *cloudformation.CloudFormation, stackSetName string) ([]*cloudformation.StackInstanceSummary, error) { - input := &cloudformation.ListStackInstancesInput{ - StackSetName: aws.String(stackSetName), - } - result := make([]*cloudformation.StackInstanceSummary, 0) - - for { - output, err := conn.ListStackInstances(input) - - if err != nil { - return result, err - } - - result = append(result, output.Summaries...) - - if aws.StringValue(output.NextToken) == "" { - break - } - - input.NextToken = output.NextToken - } - - return result, nil -} diff --git a/aws/resource_aws_cloudformation_stack_set_instance_test.go b/aws/resource_aws_cloudformation_stack_set_instance_test.go index 928dc49d710c..3ea06a89392f 100644 --- a/aws/resource_aws_cloudformation_stack_set_instance_test.go +++ b/aws/resource_aws_cloudformation_stack_set_instance_test.go @@ -7,11 +7,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/hashicorp/go-multierror" + multierror "github.com/hashicorp/go-multierror" "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/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/waiter" + tfcloudformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -23,75 +25,72 @@ func init() { func testSweepCloudformationStackSetInstances(region string) error { client, err := sharedClientForRegion(region) - if err != nil { - return fmt.Errorf("error getting client: %w", err) + return fmt.Errorf("error getting client: %s", err) } - conn := client.(*AWSClient).cfconn - stackSets, err := listCloudFormationStackSets(conn) - - if testSweepSkipSweepError(err) || isAWSErr(err, "ValidationError", "AWS CloudFormation StackSets is not supported") { - log.Printf("[WARN] Skipping CloudFormation StackSet Instance sweep for %s: %s", region, err) - return nil - } - - if err != nil { - return fmt.Errorf("error listing CloudFormation StackSets: %w", err) + input := &cloudformation.ListStackSetsInput{ + Status: aws.String(cloudformation.StackSetStatusActive), } - var sweeperErrs *multierror.Error + sweepResources := make([]*testSweepResource, 0) - for _, stackSet := range stackSets { - stackSetName := aws.StringValue(stackSet.StackSetName) - instances, err := listCloudFormationStackSetInstances(conn, stackSetName) - - if err != nil { - sweeperErr := fmt.Errorf("error listing CloudFormation StackSet (%s) Instances: %w", stackSetName, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue + err = conn.ListStackSetsPages(input, func(page *cloudformation.ListStackSetsOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - for _, instance := range instances { - accountID := aws.StringValue(instance.Account) - region := aws.StringValue(instance.Region) - id := fmt.Sprintf("%s / %s / %s", stackSetName, accountID, region) - - input := &cloudformation.DeleteStackInstancesInput{ - Accounts: aws.StringSlice([]string{accountID}), - OperationId: aws.String(resource.UniqueId()), - Regions: aws.StringSlice([]string{region}), - RetainStacks: aws.Bool(false), - StackSetName: aws.String(stackSetName), + for _, summary := range page.Summaries { + input := &cloudformation.ListStackInstancesInput{ + StackSetName: summary.StackSetName, } - log.Printf("[INFO] Deleting CloudFormation StackSet Instance: %s", id) - output, err := conn.DeleteStackInstances(input) + err = conn.ListStackInstancesPages(input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - if isAWSErr(err, cloudformation.ErrCodeStackInstanceNotFoundException, "") { - continue - } + for _, summary := range page.Summaries { + r := resourceAwsCloudFormationStackSetInstance() + d := r.Data(nil) + id := tfcloudformation.StackSetInstanceCreateResourceID( + aws.StringValue(summary.StackSetId), + aws.StringValue(summary.Account), + aws.StringValue(summary.Region), + ) + d.SetId(id) - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { - continue - } + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } - if err != nil { - sweeperErr := fmt.Errorf("error deleting CloudFormation StackSet Instance (%s): %w", id, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + return !lastPage + }) + + if testSweepSkipSweepError(err) { continue } - if err := waiter.StackSetOperationSucceeded(conn, stackSetName, aws.StringValue(output.OperationId), waiter.StackSetInstanceDeletedDefaultTimeout); err != nil { - sweeperErr := fmt.Errorf("error waiting for CloudFormation StackSet Instance (%s) deletion: %w", id, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing CloudFormation StackSet Instances (%s): %w", region, err)) } } + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping CloudFormation StackSet Instance sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing CloudFormation StackSets (%s): %w", region, err) + } + + err = testSweepResourceOrchestrator(sweepResources) + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping CloudFormation StackSet Instances (%s): %w", region, err)) } return sweeperErrs.ErrorOrNil() @@ -105,6 +104,7 @@ func TestAccAWSCloudFormationStackSetInstance_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetInstanceDestroy, Steps: []resource.TestStep{ @@ -139,6 +139,7 @@ func TestAccAWSCloudFormationStackSetInstance_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetInstanceDestroy, Steps: []resource.TestStep{ @@ -163,6 +164,7 @@ func TestAccAWSCloudFormationStackSetInstance_disappears_StackSet(t *testing.T) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetInstanceDestroy, Steps: []resource.TestStep{ @@ -187,6 +189,7 @@ func TestAccAWSCloudFormationStackSetInstance_ParameterOverrides(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetInstanceDestroy, Steps: []resource.TestStep{ @@ -250,6 +253,7 @@ func TestAccAWSCloudFormationStackSetInstance_RetainStack(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetInstanceDestroy, Steps: []resource.TestStep{ @@ -296,63 +300,44 @@ func TestAccAWSCloudFormationStackSetInstance_RetainStack(t *testing.T) { }) } -func testAccCheckCloudFormationStackSetInstanceExists(resourceName string, stackInstance *cloudformation.StackInstance) resource.TestCheckFunc { +func testAccCheckCloudFormationStackSetInstanceExists(resourceName string, v *cloudformation.StackInstance) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] - if !ok { return fmt.Errorf("Not found: %s", resourceName) } conn := testAccProvider.Meta().(*AWSClient).cfconn - stackSetName, accountID, region, err := resourceAwsCloudFormationStackSetInstanceParseId(rs.Primary.ID) + stackSetName, accountID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) if err != nil { return err } - input := &cloudformation.DescribeStackInstanceInput{ - StackInstanceAccount: aws.String(accountID), - StackInstanceRegion: aws.String(region), - StackSetName: aws.String(stackSetName), - } - - output, err := conn.DescribeStackInstance(input) + output, err := finder.StackInstanceByName(conn, stackSetName, accountID, region) if err != nil { return err } - if output == nil || output.StackInstance == nil { - return fmt.Errorf("CloudFormation StackSet Instance (%s) not found", rs.Primary.ID) - } - - *stackInstance = *output.StackInstance + *v = *output return nil } } -func testAccCheckCloudFormationStackSetInstanceStackExists(stackInstance *cloudformation.StackInstance, stack *cloudformation.Stack) resource.TestCheckFunc { +func testAccCheckCloudFormationStackSetInstanceStackExists(stackInstance *cloudformation.StackInstance, v *cloudformation.Stack) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).cfconn - input := &cloudformation.DescribeStacksInput{ - StackName: stackInstance.StackId, - } - - output, err := conn.DescribeStacks(input) + output, err := finder.StackByID(conn, aws.StringValue(stackInstance.StackId)) if err != nil { return err } - if len(output.Stacks) == 0 || output.Stacks[0] == nil { - return fmt.Errorf("CloudFormation Stack (%s) not found", aws.StringValue(stackInstance.StackId)) - } - - *stack = *output.Stacks[0] + *v = *output return nil } @@ -366,35 +351,23 @@ func testAccCheckAWSCloudFormationStackSetInstanceDestroy(s *terraform.State) er continue } - stackSetName, accountID, region, err := resourceAwsCloudFormationStackSetInstanceParseId(rs.Primary.ID) + stackSetName, accountID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) if err != nil { return err } - input := &cloudformation.DescribeStackInstanceInput{ - StackInstanceAccount: aws.String(accountID), - StackInstanceRegion: aws.String(region), - StackSetName: aws.String(stackSetName), - } + _, err = finder.StackInstanceByName(conn, stackSetName, accountID, region) - output, err := conn.DescribeStackInstance(input) - - if isAWSErr(err, cloudformation.ErrCodeStackInstanceNotFoundException, "") { - return nil - } - - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { - return nil + if tfresource.NotFound(err) { + continue } if err != nil { return err } - if output != nil && output.StackInstance != nil { - return fmt.Errorf("CloudFormation StackSet Instance (%s) still exists", rs.Primary.ID) - } + return fmt.Errorf("CloudFormation StackSet Instance %s still exists", rs.Primary.ID) } return nil diff --git a/aws/resource_aws_cloudformation_stack_set_test.go b/aws/resource_aws_cloudformation_stack_set_test.go index 3264ce8b4187..28db7f2cbd3d 100644 --- a/aws/resource_aws_cloudformation_stack_set_test.go +++ b/aws/resource_aws_cloudformation_stack_set_test.go @@ -8,10 +8,11 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/hashicorp/go-multierror" "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/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -26,47 +27,47 @@ func init() { func testSweepCloudformationStackSets(region string) error { client, err := sharedClientForRegion(region) - if err != nil { - return fmt.Errorf("error getting client: %w", err) + return fmt.Errorf("error getting client: %s", err) } - conn := client.(*AWSClient).cfconn - stackSets, err := listCloudFormationStackSets(conn) + input := &cloudformation.ListStackSetsInput{ + Status: aws.String(cloudformation.StackSetStatusActive), + } + sweepResources := make([]*testSweepResource, 0) + + err = conn.ListStackSetsPages(input, func(page *cloudformation.ListStackSetsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, summary := range page.Summaries { + r := resourceAwsCloudFormationStackSet() + d := r.Data(nil) + d.SetId(aws.StringValue(summary.StackSetName)) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) - if testSweepSkipSweepError(err) || isAWSErr(err, "ValidationError", "AWS CloudFormation StackSets is not supported") { + if testSweepSkipSweepError(err) { log.Printf("[WARN] Skipping CloudFormation StackSet sweep for %s: %s", region, err) return nil } if err != nil { - return fmt.Errorf("error listing CloudFormation StackSets: %w", err) + return fmt.Errorf("error listing CloudFormation StackSets (%s): %w", region, err) } - var sweeperErrs *multierror.Error - - for _, stackSet := range stackSets { - input := &cloudformation.DeleteStackSetInput{ - StackSetName: stackSet.StackSetName, - } - name := aws.StringValue(stackSet.StackSetName) + err = testSweepResourceOrchestrator(sweepResources) - log.Printf("[INFO] Deleting CloudFormation StackSet: %s", name) - _, err := conn.DeleteStackSet(input) - - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { - continue - } - - if err != nil { - sweeperErr := fmt.Errorf("error deleting CloudFormation StackSet (%s): %w", name, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue - } + if err != nil { + return fmt.Errorf("error sweeping CloudFormation StackSets (%s): %w", region, err) } - return sweeperErrs.ErrorOrNil() + return nil } func TestAccAWSCloudFormationStackSet_basic(t *testing.T) { @@ -77,6 +78,7 @@ func TestAccAWSCloudFormationStackSet_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -91,6 +93,7 @@ func TestAccAWSCloudFormationStackSet_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "execution_role_name", "AWSCloudFormationStackSetExecutionRole"), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "parameters.%", "0"), + resource.TestCheckResourceAttr(resourceName, "permission_model", "SELF_MANAGED"), resource.TestMatchResourceAttr(resourceName, "stack_set_id", regexp.MustCompile(fmt.Sprintf("%s:.+", rName))), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "template_body", testAccAWSCloudFormationStackSetTemplateBodyVpc(rName)+"\n"), @@ -116,6 +119,7 @@ func TestAccAWSCloudFormationStackSet_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -140,6 +144,7 @@ func TestAccAWSCloudFormationStackSet_AdministrationRoleArn(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -177,6 +182,7 @@ func TestAccAWSCloudFormationStackSet_Description(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -214,6 +220,7 @@ func TestAccAWSCloudFormationStackSet_ExecutionRoleName(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -252,6 +259,7 @@ func TestAccAWSCloudFormationStackSet_Name(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -305,6 +313,7 @@ func TestAccAWSCloudFormationStackSet_Parameters(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -364,6 +373,7 @@ func TestAccAWSCloudFormationStackSet_Parameters_Default(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -415,6 +425,7 @@ func TestAccAWSCloudFormationStackSet_Parameters_NoEcho(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -447,6 +458,47 @@ func TestAccAWSCloudFormationStackSet_Parameters_NoEcho(t *testing.T) { }) } +func TestAccAWSCloudFormationStackSet_PermissionModel_ServiceManaged(t *testing.T) { + TestAccSkip(t, "API does not support enabling Organizations access (in particular, creating the Stack Sets IAM Service-Linked Role)") + + var stackSet1 cloudformation.StackSet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cloudformation_stack_set.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAWSCloudFormationStackSet(t) + testAccOrganizationsAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID, "organizations"), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCloudFormationStackSetConfigPermissionModel(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudFormationStackSetExists(resourceName, &stackSet1), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "cloudformation", regexp.MustCompile(`stackset/.+`)), + resource.TestCheckResourceAttr(resourceName, "permission_model", "SERVICE_MANAGED"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_deployment.0.retain_stacks_on_account_removal", "false"), + resource.TestMatchResourceAttr(resourceName, "stack_set_id", regexp.MustCompile(fmt.Sprintf("%s:.+", rName))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "template_url", + }, + }, + }, + }) +} + func TestAccAWSCloudFormationStackSet_Tags(t *testing.T) { var stackSet1, stackSet2 cloudformation.StackSet rName := acctest.RandomWithPrefix("tf-acc-test") @@ -454,6 +506,7 @@ func TestAccAWSCloudFormationStackSet_Tags(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -509,6 +562,7 @@ func TestAccAWSCloudFormationStackSet_TemplateBody(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -546,6 +600,7 @@ func TestAccAWSCloudFormationStackSet_TemplateUrl(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCloudFormationStackSet(t) }, + ErrorCheck: testAccErrorCheck(t, cloudformation.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSCloudFormationStackSetDestroy, Steps: []resource.TestStep{ @@ -578,31 +633,22 @@ func TestAccAWSCloudFormationStackSet_TemplateUrl(t *testing.T) { }) } -func testAccCheckCloudFormationStackSetExists(resourceName string, stackSet *cloudformation.StackSet) resource.TestCheckFunc { +func testAccCheckCloudFormationStackSetExists(resourceName string, v *cloudformation.StackSet) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] - if !ok { return fmt.Errorf("Not found: %s", resourceName) } conn := testAccProvider.Meta().(*AWSClient).cfconn - input := &cloudformation.DescribeStackSetInput{ - StackSetName: aws.String(rs.Primary.ID), - } - - output, err := conn.DescribeStackSet(input) + output, err := finder.StackSetByName(conn, rs.Primary.ID) if err != nil { return err } - if output == nil || output.StackSet == nil { - return fmt.Errorf("CloudFormation StackSet (%s) not found", rs.Primary.ID) - } - - *stackSet = *output.StackSet + *v = *output return nil } @@ -616,23 +662,17 @@ func testAccCheckAWSCloudFormationStackSetDestroy(s *terraform.State) error { continue } - input := cloudformation.DescribeStackSetInput{ - StackSetName: aws.String(rs.Primary.ID), - } - - output, err := conn.DescribeStackSet(&input) + _, err := finder.StackSetByName(conn, rs.Primary.ID) - if isAWSErr(err, cloudformation.ErrCodeStackSetNotFoundException, "") { - return nil + if tfresource.NotFound(err) { + continue } if err != nil { return err } - if output != nil && output.StackSet != nil { - return fmt.Errorf("CloudFormation StackSet (%s) still exists", rs.Primary.ID) - } + return fmt.Errorf("CloudFormation StackSet %s still exists", rs.Primary.ID) } return nil @@ -1428,3 +1468,21 @@ resource "aws_cloudformation_stack_set" "test" { } `, rName, testAccAWSCloudFormationStackSetTemplateBodyVpc(rName+"2")) } + +func testAccAWSCloudFormationStackSetConfigPermissionModel(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudformation_stack_set" "test" { + name = %[1]q + permission_model = "SERVICE_MANAGED" + + auto_deployment { + enabled = true + retain_stacks_on_account_removal = false + } + + template_body = <