From fbd3948340e1df7fe1b7988b5b4a0ec4858a19f6 Mon Sep 17 00:00:00 2001 From: fakecloud-bot Date: Mon, 7 Sep 2026 13:05:46 +0000 Subject: [PATCH 1/3] chore(aws-models): weekly refresh from aws/api-models-aws --- aws-models/bedrock.json | 8 +- aws-models/ec2.json | 155 +- aws-models/ecs.json | 44 + aws-models/eks.json | 6 +- aws-models/elasticloadbalancingv2.json | 4 +- aws-models/kinesis.json | 8562 +++++++++++++++++------- aws-models/lambda.json | 122 +- aws-models/mediaconvert.json | 432 +- aws-models/mwaa.json | 4 +- aws-models/sagemaker.json | 62 +- aws-models/sesv2.json | 448 +- aws-models/sfn.json | 12 +- aws-models/support.json | 812 ++- aws-models/transcribe.json | 110 +- aws-models/transfer.json | 30 +- 15 files changed, 8435 insertions(+), 2376 deletions(-) diff --git a/aws-models/bedrock.json b/aws-models/bedrock.json index 67d845a24..8eab948fc 100644 --- a/aws-models/bedrock.json +++ b/aws-models/bedrock.json @@ -8256,6 +8256,12 @@ "smithy.api#enumValue": "none" } }, + "AWS_REVIEW": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "aws_review" + } + }, "PROVIDER_DATA_SHARE": { "target": "smithy.api#Unit", "traits": { @@ -8270,7 +8276,7 @@ } }, "traits": { - "smithy.api#documentation": "

The data retention mode for the account. Valid values are:

" + "smithy.api#documentation": "

The data retention mode for the account. Valid values are:

" } }, "com.amazonaws.bedrock#DataRetentionResource": { diff --git a/aws-models/ec2.json b/aws-models/ec2.json index b236017f1..d03d4cf5e 100644 --- a/aws-models/ec2.json +++ b/aws-models/ec2.json @@ -5082,6 +5082,9 @@ { "target": "com.amazonaws.ec2#UpdateSecurityGroupRuleDescriptionsIngress" }, + { + "target": "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterface" + }, { "target": "com.amazonaws.ec2#WithdrawByoipCidr" } @@ -11690,18 +11693,6 @@ "com.amazonaws.ec2#Blob": { "type": "blob" }, - "com.amazonaws.ec2#BlobAttributeValue": { - "type": "structure", - "members": { - "Value": { - "target": "com.amazonaws.ec2#Blob", - "traits": { - "aws.protocols#ec2QueryName": "Value", - "smithy.api#xmlName": "value" - } - } - } - }, "com.amazonaws.ec2#BlockDeviceMapping": { "type": "structure", "members": { @@ -14770,6 +14761,14 @@ "smithy.api#documentation": "

\n\t\t\tInformation about the interruption configuration and association with the source reservation for interruptible Capacity Reservations.\n\t\t

", "smithy.api#xmlName": "interruptionInfo" } + }, + "ZeroSizePreference": { + "target": "com.amazonaws.ec2#ZeroSizePreference", + "traits": { + "aws.protocols#ec2QueryName": "ZeroSizePreference", + "smithy.api#documentation": "

\n\t\t\tThe zero-size preference configured for the interruptible Capacity Reservation. A value of retain keeps the interruptible Capacity Reservation active at zero capacity when you reduce its allocation to zero. A value of default cancels the interruptible Capacity Reservation when you reduce its allocation to zero.\n\t\t

", + "smithy.api#xmlName": "zeroSizePreference" + } } }, "traits": { @@ -21531,6 +21530,12 @@ "smithy.api#documentation": "

\n\t\t\tThe tags to apply to the interruptible Capacity Reservation during creation.\n\t\t

", "smithy.api#xmlName": "TagSpecification" } + }, + "ZeroSizePreference": { + "target": "com.amazonaws.ec2#ZeroSizePreference", + "traits": { + "smithy.api#documentation": "

\n\t\t\tSpecifies the behavior for the interruptible Capacity Reservation when you reduce its allocation to zero instances. Specify retain to keep the interruptible Capacity Reservation active at zero capacity so that you can allocate instances to it again later. Specify default to cancel the interruptible Capacity Reservation and return the capacity to your source Capacity Reservation. The default value is default.\n\t\t

" + } } }, "traits": { @@ -40497,7 +40502,7 @@ "target": "com.amazonaws.ec2#String", "traits": { "aws.protocols#ec2QueryName": "ErrorCode", - "smithy.api#documentation": "

The error code that indicates why the instance could not be launched. For more\n information about error codes, see Error codes.

", + "smithy.api#documentation": "

The error code that indicates why the instance could not be launched. For more\n information about error codes, see Error codes.

", "smithy.api#xmlName": "errorCode" } }, @@ -40505,7 +40510,7 @@ "target": "com.amazonaws.ec2#String", "traits": { "aws.protocols#ec2QueryName": "ErrorMessage", - "smithy.api#documentation": "

The error message that describes why the instance could not be launched. For more\n information about error messages, see Error codes.

", + "smithy.api#documentation": "

The error message that describes why the instance could not be launched. For more\n information about error messages, see Error codes.

", "smithy.api#xmlName": "errorMessage" } } @@ -81457,7 +81462,7 @@ "target": "com.amazonaws.ec2#ApplicationStatusSummary", "traits": { "aws.protocols#ec2QueryName": "ApplicationStatus", - "smithy.api#documentation": "

Reports impaired functionality that stems from issues with applications running on the instance.

", + "smithy.api#documentation": "

Reports the application-level health status for the instance.

", "smithy.api#xmlName": "applicationStatus" } } @@ -90920,6 +90925,14 @@ "smithy.api#documentation": "

\n\t\t\tThe type of interruption policy applied to the interruptible reservation.\n\t\t

", "smithy.api#xmlName": "interruptionType" } + }, + "ZeroSizePreference": { + "target": "com.amazonaws.ec2#ZeroSizePreference", + "traits": { + "aws.protocols#ec2QueryName": "ZeroSizePreference", + "smithy.api#documentation": "

\n\t\t\tSpecifies how Amazon EC2 handles the interruptible Capacity Reservation when you reduce its allocation to zero instances. A value of retain keeps the interruptible Capacity Reservation active at zero capacity so that you can allocate instances to it again later. A value of default cancels the interruptible Capacity Reservation and returns the capacity to your source Capacity Reservation.\n\t\t

", + "smithy.api#xmlName": "zeroSizePreference" + } } }, "traits": { @@ -92425,6 +92438,14 @@ "smithy.api#xmlName": "state" } }, + "StateMessage": { + "target": "com.amazonaws.ec2#String", + "traits": { + "aws.protocols#ec2QueryName": "StateMessage", + "smithy.api#documentation": "

A message describing the current state of the internet registry association, including additional details such as the reason for a failure.

", + "smithy.api#xmlName": "stateMessage" + } + }, "ChildRequestXml": { "target": "com.amazonaws.ec2#String", "traits": { @@ -103668,7 +103689,7 @@ } }, "UserData": { - "target": "com.amazonaws.ec2#BlobAttributeValue", + "target": "com.amazonaws.ec2#SecureBlobAttributeValue", "traits": { "aws.protocols#ec2QueryName": "UserData", "smithy.api#documentation": "

Changes the instance's user data to the specified value. User data must be base64-encoded.\n Depending on the tool or SDK that you're using, the base64-encoding might be performed for you.\n For more information, see Work with instance user data.

", @@ -125700,7 +125721,7 @@ } }, "UploadPolicy": { - "target": "com.amazonaws.ec2#Blob", + "target": "com.amazonaws.ec2#SecureBlob", "traits": { "aws.protocols#ec2QueryName": "UploadPolicy", "smithy.api#documentation": "

An Amazon S3 upload policy that gives Amazon EC2 permission to upload items into Amazon S3 on your\n behalf.

", @@ -127577,6 +127598,28 @@ "smithy.api#pattern": "^(?=.{20,2048}$)arn:aws[a-z-]*:secretsmanager:[a-z0-9-]+:\\d{12}:secret:[a-zA-Z0-9/_+=.@-]+$" } }, + "com.amazonaws.ec2#SecureBlob": { + "type": "blob", + "traits": { + "smithy.api#sensitive": {} + } + }, + "com.amazonaws.ec2#SecureBlobAttributeValue": { + "type": "structure", + "members": { + "Value": { + "target": "com.amazonaws.ec2#SecureBlob", + "traits": { + "aws.protocols#ec2QueryName": "Value", + "smithy.api#documentation": "

The attribute value.

", + "smithy.api#xmlName": "value" + } + } + }, + "traits": { + "smithy.api#documentation": "

Describes a value for a resource attribute that is a Base64-encoded binary data object.

" + } + }, "com.amazonaws.ec2#SecurityGroup": { "type": "structure", "members": { @@ -139221,9 +139264,7 @@ "TargetInstanceCount": { "target": "com.amazonaws.ec2#Integer", "traits": { - "smithy.api#clientOptional": {}, - "smithy.api#documentation": "

\n\t\t\tThe new number of instances to allocate. Enter a higher number to add more capacity to share, or a lower number to reclaim capacity to your source Capacity Reservation.\n\t\t

", - "smithy.api#required": {} + "smithy.api#documentation": "

\n\t\t\tThe new number of instances to allocate. Enter a higher number to add more capacity to share, or a lower number to reclaim capacity to your source Capacity Reservation.\n\t\t

" } }, "DryRun": { @@ -139231,6 +139272,12 @@ "traits": { "smithy.api#documentation": "

\n\t\t\tChecks whether you have the required permissions for the action, without actually making the request, and provides an error response.\n\t\t

" } + }, + "ZeroSizePreference": { + "target": "com.amazonaws.ec2#ZeroSizePreference", + "traits": { + "smithy.api#documentation": "

\n\t\t\tSpecifies the updated behavior for the interruptible Capacity Reservation when you reduce its allocation to zero instances. Specify retain to keep the interruptible Capacity Reservation active at zero capacity so that you can allocate instances to it again later. Specify default to cancel the interruptible Capacity Reservation and return the capacity to your source Capacity Reservation.\n\t\t

" + } } }, "traits": { @@ -139789,6 +139836,57 @@ "smithy.api#documentation": "

Describes the vCPU configurations for the instance type.

" } }, + "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterface": { + "type": "operation", + "input": { + "target": "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterfaceRequest" + }, + "output": { + "target": "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterfaceResult" + }, + "traits": { + "smithy.api#documentation": "

Validates whether the specified security groups can be associated with a single\n network interface. The operation checks Amazon Virtual Private Cloud (Amazon VPC)\n quotas for inbound or outbound rules per security group and security groups per\n network interface. Only authorized AWS services can call this operation.

\n

For more information about security group quotas, see Amazon\n VPC quotas in the Amazon VPC User Guide.

" + } + }, + "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterfaceRequest": { + "type": "structure", + "members": { + "SecurityGroupIds": { + "target": "com.amazonaws.ec2#SecurityGroupIdList", + "traits": { + "smithy.api#clientOptional": {}, + "smithy.api#documentation": "

The IDs of the security groups to validate for association with a single network\n interface. You must specify at least one ID, and each ID must be unique. The number\n of IDs cannot exceed the maximum number of security groups allowed per network\n interface.

", + "smithy.api#required": {}, + "smithy.api#xmlName": "SecurityGroupId" + } + }, + "DryRun": { + "target": "com.amazonaws.ec2#Boolean", + "traits": { + "smithy.api#documentation": "

Checks whether you have the required permissions for the action, without actually making the request, \n and provides an error response. If you have the required permissions, the error response is DryRunOperation. \n Otherwise, it is UnauthorizedOperation.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.ec2#ValidateSecurityGroupQuotasForInterfaceResult": { + "type": "structure", + "members": { + "Valid": { + "target": "com.amazonaws.ec2#Boolean", + "traits": { + "aws.protocols#ec2QueryName": "Valid", + "smithy.api#documentation": "

The operation returns true if the specified security groups can be\n associated with a single network interface without exceeding the quotas. It returns\n an error if associating the security groups would exceed a quota.

", + "smithy.api#xmlName": "valid" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, "com.amazonaws.ec2#ValidationError": { "type": "structure", "members": { @@ -145601,6 +145699,23 @@ } } }, + "com.amazonaws.ec2#ZeroSizePreference": { + "type": "enum", + "members": { + "retain": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "retain" + } + }, + "default": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "default" + } + } + } + }, "com.amazonaws.ec2#ZoneIdStringList": { "type": "list", "member": { diff --git a/aws-models/ecs.json b/aws-models/ecs.json index f60c98680..40f612c8a 100644 --- a/aws-models/ecs.json +++ b/aws-models/ecs.json @@ -4484,6 +4484,12 @@ "traits": { "smithy.api#documentation": "

An identifier that you provide to ensure the idempotency of the request. It must be unique and is case sensitive. Up to 36 ASCII characters in the range of 33-126 (inclusive) are allowed.

" } + }, + "critical": { + "target": "com.amazonaws.ecs#BoxedBoolean", + "traits": { + "smithy.api#documentation": "

If the critical parameter of a daemon is true, and the daemon task fails, stops, or becomes unhealthy, Amazon ECS drains the container instance and stops the other tasks running on it. If the critical parameter is false, the daemon task failure doesn't affect the other tasks on the instance. The default value is true.

A non-critical daemon doesn't block instance registration. The container instance becomes active and continues to run your other tasks, whether the daemon task fails during scale-out or during a deployment.

Amazon ECS emits an EventBridge event when a daemon task fails to start, for both critical and non-critical daemons.

Daemon task launch failures during a deployment are still counted by the deployment circuit breaker. The circuit breaker can roll back an unstable target revision.

" + } } }, "traits": { @@ -5497,6 +5503,13 @@ "smithy.api#default": 0, "smithy.api#documentation": "

The number of daemon tasks running on this capacity provider.

" } + }, + "withoutDaemonCount": { + "target": "com.amazonaws.ecs#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The number of instances on this capacity provider that are running without the daemon task. This applies to daemons that aren't critical, where the instance remains available for your other tasks even if the daemon task can't start or stops. These instances aren't included in runningCount.

" + } } }, "traits": { @@ -5892,6 +5905,12 @@ "smithy.api#documentation": "

The number of instances running daemon tasks on this capacity provider.

" } }, + "withoutDaemonInstanceCount": { + "target": "com.amazonaws.ecs#BoxedInteger", + "traits": { + "smithy.api#documentation": "

The number of instances on this capacity provider that are running without the daemon task. This applies to daemons that aren't critical, where the instance remains available for your other tasks even if the daemon task can't start or stops. These instances aren't included in runningInstanceCount.

" + } + }, "drainingInstanceCount": { "target": "com.amazonaws.ecs#BoxedInteger", "traits": { @@ -5999,6 +6018,12 @@ "smithy.api#documentation": "

The total number of instances running daemon tasks for this revision.

" } }, + "totalWithoutDaemonInstanceCount": { + "target": "com.amazonaws.ecs#BoxedInteger", + "traits": { + "smithy.api#documentation": "

The total number of instances running without the daemon task for this revision, across all capacity providers. These instances aren't included in totalRunningInstanceCount.

" + } + }, "totalDrainingInstanceCount": { "target": "com.amazonaws.ecs#BoxedInteger", "traits": { @@ -6457,6 +6482,12 @@ "traits": { "smithy.api#documentation": "

Specifies whether the execute command functionality is turned on for the daemon tasks.

" } + }, + "critical": { + "target": "com.amazonaws.ecs#BoxedBoolean", + "traits": { + "smithy.api#documentation": "

If the critical parameter of this daemon revision is true, and the daemon task fails, stops, or becomes unhealthy, Amazon ECS drains the container instance and stops the other tasks running on it. If the parameter is false, the daemon task failure doesn't affect the other tasks on the instance, and doesn't block instance registration. The default value is true.

" + } } }, "traits": { @@ -6484,6 +6515,13 @@ "smithy.api#default": 0, "smithy.api#documentation": "

The total number of daemon tasks running for this revision.

" } + }, + "totalWithoutDaemonCount": { + "target": "com.amazonaws.ecs#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The total number of instances running without the daemon task for this revision, across all capacity providers. These instances aren't included in totalRunningCount.

" + } } }, "traits": { @@ -24323,6 +24361,12 @@ "smithy.api#default": false, "smithy.api#documentation": "

If true, the execute command functionality is turned on for all tasks in the daemon. If false, the execute command functionality is turned off.

" } + }, + "critical": { + "target": "com.amazonaws.ecs#BoxedBoolean", + "traits": { + "smithy.api#documentation": "

If the critical parameter of a daemon is true, and the daemon task fails, stops, or becomes unhealthy, Amazon ECS drains the container instance and stops the other tasks running on it. If the critical parameter is false, the daemon task failure doesn't affect the other tasks on the instance. The default value is true.

A non-critical daemon doesn't block instance registration. The container instance becomes active and continues to run your other tasks, whether the daemon task fails during scale-out or during a deployment.

Amazon ECS emits an EventBridge event when a daemon task fails to start, for both critical and non-critical daemons.

Daemon task launch failures during a deployment are still counted by the deployment circuit breaker. The circuit breaker can roll back an unstable target revision.

" + } } }, "traits": { diff --git a/aws-models/eks.json b/aws-models/eks.json index 13492e051..267cd6fd4 100644 --- a/aws-models/eks.json +++ b/aws-models/eks.json @@ -8604,7 +8604,11 @@ "resources": { "target": "com.amazonaws.eks#StringList", "traits": { - "smithy.api#documentation": "

Specifies the resources to be encrypted. The only supported value is\n secrets.

" + "smithy.api#deprecated": { + "since": "2025-03-05", + "message": "Deprecated. Amazon EKS encrypts all Kubernetes API data by default, so this value no longer determines which resources are encrypted." + }, + "smithy.api#documentation": "\n

Amazon EKS encrypts all Kubernetes API data with envelope encryption by default for\n clusters running Kubernetes version 1.28 or higher, so this field no longer affects which\n resources are encrypted.

\n
\n

Specifies the resources to be encrypted. The only supported value is\n secrets.

" } }, "provider": { diff --git a/aws-models/elasticloadbalancingv2.json b/aws-models/elasticloadbalancingv2.json index 976981d4c..15f0f70f7 100644 --- a/aws-models/elasticloadbalancingv2.json +++ b/aws-models/elasticloadbalancingv2.json @@ -6429,7 +6429,7 @@ "Key": { "target": "com.amazonaws.elasticloadbalancingv2#ListenerAttributeKey", "traits": { - "smithy.api#documentation": "

The name of the attribute.

\n

The following attribute is supported by Network Load Balancers, and Gateway Load Balancers.

\n \n

The following attributes are only supported by Application Load Balancers.

\n " + "smithy.api#documentation": "

The name of the attribute.

\n

The following attribute is supported by Network Load Balancers, and Gateway Load Balancers.

\n \n

The following attribute is only supported by Gateway Load Balancers:

\n \n

The following attributes are only supported by Application Load Balancers.

\n " } }, "Value": { @@ -9926,7 +9926,7 @@ "Key": { "target": "com.amazonaws.elasticloadbalancingv2#TargetGroupAttributeKey", "traits": { - "smithy.api#documentation": "

The name of the attribute.

\n

The following attributes are supported by all load balancers:

\n \n

The following attributes are supported by Application Load Balancers and \n Network Load Balancers:

\n \n

The following attributes are supported only if the load balancer is an Application Load\n Balancer and the target is an instance or an IP address:

\n \n

The following attribute is supported only if the load balancer is an Application Load\n Balancer and the target is a Lambda function:

\n \n

The following attributes are supported only by Network Load Balancers:

\n \n

The following attributes are supported only by Gateway Load Balancers:

\n " + "smithy.api#documentation": "

The name of the attribute.

\n

The following attributes are supported by all load balancers:

\n \n

The following attributes are supported by Application Load Balancers and \n Network Load Balancers:

\n \n

The following attributes are supported only if the load balancer is an Application Load\n Balancer and the target is an instance or an IP address:

\n \n

The following attribute is supported only if the load balancer is an Application Load\n Balancer and the target is a Lambda function:

\n \n

The following attributes are supported only by Network Load Balancers:

\n \n

The following attributes are supported only by Gateway Load Balancers:

\n " } }, "Value": { diff --git a/aws-models/kinesis.json b/aws-models/kinesis.json index 49ace5bda..04d673c63 100644 --- a/aws-models/kinesis.json +++ b/aws-models/kinesis.json @@ -126,145 +126,213 @@ "com.amazonaws.kinesis#BooleanObject": { "type": "boolean" }, - "com.amazonaws.kinesis#ChildShard": { + "com.amazonaws.kinesis#BucketARN": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws[-a-z0-9]*:s3:::[a-z0-9._-]{3,63}$" + } + }, + "com.amazonaws.kinesis#ChannelARN": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws.*:kinesis:.*:\\d{12}:channel/\\S+$" + } + }, + "com.amazonaws.kinesis#ChannelCountObject": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 0, + "max": 1000000 + } + } + }, + "com.amazonaws.kinesis#ChannelDescription": { "type": "structure", "members": { - "ShardId": { - "target": "com.amazonaws.kinesis#ShardId", + "ChannelName": { + "target": "com.amazonaws.kinesis#ChannelName", "traits": { - "smithy.api#documentation": "

The shard ID of the existing child shard of the current shard.

", + "smithy.api#documentation": "

The name of the channel.

", "smithy.api#required": {} } }, - "ParentShards": { - "target": "com.amazonaws.kinesis#ShardIdList", + "ChannelARN": { + "target": "com.amazonaws.kinesis#ChannelARN", "traits": { - "smithy.api#documentation": "

The current shard that is the parent of the existing child shard.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the channel.

", "smithy.api#required": {} } }, - "HashKeyRange": { - "target": "com.amazonaws.kinesis#HashKeyRange", + "ChannelId": { + "target": "com.amazonaws.kinesis#ChannelId", "traits": { + "smithy.api#documentation": "

The unique identifier of the channel.

", "smithy.api#required": {} } - } - }, - "traits": { - "smithy.api#documentation": "

Output parameter of the GetRecords API. The existing child shard of the current\n shard.

" - } - }, - "com.amazonaws.kinesis#ChildShardList": { - "type": "list", - "member": { - "target": "com.amazonaws.kinesis#ChildShard" - } - }, - "com.amazonaws.kinesis#Consumer": { - "type": "structure", - "members": { - "ConsumerName": { - "target": "com.amazonaws.kinesis#ConsumerName", + }, + "ChannelStatus": { + "target": "com.amazonaws.kinesis#ChannelStatus", "traits": { - "smithy.api#documentation": "

The name of the consumer is something you choose when you register the\n consumer.

", + "smithy.api#documentation": "

The current status of the channel. Valid values:

\n ", "smithy.api#required": {} } }, - "ConsumerARN": { - "target": "com.amazonaws.kinesis#ConsumerARN", + "ChannelStatusReason": { + "target": "com.amazonaws.kinesis#ChannelStatusReason", "traits": { - "smithy.api#documentation": "

When you register a consumer, Kinesis Data Streams generates an ARN for it. You need\n this ARN to be able to call SubscribeToShard.

\n

If you delete a consumer and then create a new one with the same name, it won't have\n the same ARN. That's because consumer ARNs contain the creation timestamp. This is\n important to keep in mind if you have IAM policies that reference consumer ARNs.

", + "smithy.api#documentation": "

A message describing the reason for a FAILED status.

" + } + }, + "ChannelCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", + "traits": { + "smithy.api#documentation": "

The time at which the channel was created.

", "smithy.api#required": {} } }, - "ConsumerStatus": { - "target": "com.amazonaws.kinesis#ConsumerStatus", + "ServiceExecutionRoleARN": { + "target": "com.amazonaws.kinesis#RoleARN", "traits": { - "smithy.api#documentation": "

A consumer can't read data while in the CREATING or DELETING\n states.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the IAM role that Amazon Kinesis Data Streams assumes to write records to the destination.

", "smithy.api#required": {} } }, - "ConsumerCreationTimestamp": { - "target": "com.amazonaws.kinesis#Timestamp", + "StreamConfigurationList": { + "target": "com.amazonaws.kinesis#ChannelStreamDescriptionList", "traits": { - "smithy.api#documentation": "

", + "smithy.api#documentation": "

The source stream configuration for the channel.

", + "smithy.api#required": {} + } + }, + "S3DestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3DestinationDescription", + "traits": { + "smithy.api#documentation": "

The configuration for delivery to a general purpose Amazon S3 bucket. Present only when the channel destination is a general purpose Amazon S3 bucket.

" + } + }, + "S3TablesDestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3TablesDestinationDescription", + "traits": { + "smithy.api#documentation": "

The configuration for delivery to streaming tables on Apache Iceberg in Amazon S3 Tables. Present only when the channel destination is a streaming table.

" + } + }, + "EncryptionConfiguration": { + "target": "com.amazonaws.kinesis#ChannelEncryptionConfiguration", + "traits": { + "smithy.api#documentation": "

The server-side encryption configuration for the channel.

" + } + }, + "LoggingConfiguration": { + "target": "com.amazonaws.kinesis#ChannelLoggingConfiguration", + "traits": { + "smithy.api#documentation": "

The Amazon CloudWatch Logs configuration for the channel.

", "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

An object that represents the details of the consumer you registered. This type of\n object is returned by RegisterStreamConsumer.

" + "smithy.api#documentation": "

Describes the configuration and current status of a channel.

" } }, - "com.amazonaws.kinesis#ConsumerARN": { - "type": "string", - "traits": { - "smithy.api#length": { - "min": 1, - "max": 2048 + "com.amazonaws.kinesis#ChannelDestinationType": { + "type": "enum", + "members": { + "S3": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "S3" + } }, - "smithy.api#pattern": "^(arn):aws.*:kinesis:.*:\\d{12}:.*stream\\/[a-zA-Z0-9_.-]+\\/consumer\\/[a-zA-Z0-9_.-]+:[0-9]+$" - } - }, - "com.amazonaws.kinesis#ConsumerCountObject": { - "type": "integer", - "traits": { - "smithy.api#range": { - "min": 0, - "max": 1000000 + "S3_TABLES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "S3_TABLES" + } } } }, - "com.amazonaws.kinesis#ConsumerDescription": { + "com.amazonaws.kinesis#ChannelEncryptionConfiguration": { "type": "structure", "members": { - "ConsumerName": { - "target": "com.amazonaws.kinesis#ConsumerName", + "EncryptionType": { + "target": "com.amazonaws.kinesis#ChannelEncryptionType", "traits": { - "smithy.api#documentation": "

The name of the consumer is something you choose when you register the\n consumer.

", + "smithy.api#documentation": "

The encryption type. The only valid value is KMS.

", "smithy.api#required": {} } }, - "ConsumerARN": { - "target": "com.amazonaws.kinesis#ConsumerARN", + "KeyId": { + "target": "com.amazonaws.kinesis#KeyId", "traits": { - "smithy.api#documentation": "

When you register a consumer, Kinesis Data Streams generates an ARN for it. You need\n this ARN to be able to call SubscribeToShard.

\n

If you delete a consumer and then create a new one with the same name, it won't have\n the same ARN. That's because consumer ARNs contain the creation timestamp. This is\n important to keep in mind if you have IAM policies that reference consumer ARNs.

", + "smithy.api#documentation": "

The identifier of the customer managed Amazon Web Services KMS key. You cannot use the Amazon Kinesis Data Streams service key (aws/kinesis).

", "smithy.api#required": {} } - }, - "ConsumerStatus": { - "target": "com.amazonaws.kinesis#ConsumerStatus", + } + }, + "traits": { + "smithy.api#documentation": "

Specifies the Amazon Web Services KMS key that Amazon Kinesis Data Streams uses to encrypt data delivered to the channel's destination.

" + } + }, + "com.amazonaws.kinesis#ChannelEncryptionType": { + "type": "enum", + "members": { + "KMS": { + "target": "smithy.api#Unit", "traits": { - "smithy.api#documentation": "

A consumer can't read data while in the CREATING or DELETING\n states.

", - "smithy.api#required": {} + "smithy.api#enumValue": "KMS" } - }, - "ConsumerCreationTimestamp": { - "target": "com.amazonaws.kinesis#Timestamp", + } + } + }, + "com.amazonaws.kinesis#ChannelId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 64 + } + } + }, + "com.amazonaws.kinesis#ChannelLoggingConfiguration": { + "type": "structure", + "members": { + "CloudWatchLogs": { + "target": "com.amazonaws.kinesis#CloudWatchLogs", "traits": { - "smithy.api#documentation": "

", + "smithy.api#documentation": "

The Amazon CloudWatch Logs settings for the channel.

", "smithy.api#required": {} } - }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + } + }, + "traits": { + "smithy.api#documentation": "

The Amazon CloudWatch Logs configuration for a channel.

" + } + }, + "com.amazonaws.kinesis#ChannelLoggingUpdateInput": { + "type": "structure", + "members": { + "CloudWatchLogs": { + "target": "com.amazonaws.kinesis#CloudWatchLogsUpdateInput", "traits": { - "smithy.api#documentation": "

The ARN of the stream with which you registered the consumer.

", + "smithy.api#documentation": "

The updated Amazon CloudWatch Logs settings for the channel.

", "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

An object that represents the details of a registered consumer. This type of object is\n returned by DescribeStreamConsumer.

" - } - }, - "com.amazonaws.kinesis#ConsumerList": { - "type": "list", - "member": { - "target": "com.amazonaws.kinesis#Consumer" + "smithy.api#documentation": "

The updated Amazon CloudWatch Logs configuration for a channel. Used in UpdateChannel.

" } }, - "com.amazonaws.kinesis#ConsumerName": { + "com.amazonaws.kinesis#ChannelName": { "type": "string", "traits": { "smithy.api#length": { @@ -274,7 +342,7 @@ "smithy.api#pattern": "^[a-zA-Z0-9_.-]+$" } }, - "com.amazonaws.kinesis#ConsumerStatus": { + "com.amazonaws.kinesis#ChannelStatus": { "type": "enum", "members": { "CREATING": { @@ -283,498 +351,471 @@ "smithy.api#enumValue": "CREATING" } }, + "ACTIVE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ACTIVE" + } + }, + "UPDATING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "UPDATING" + } + }, "DELETING": { "target": "smithy.api#Unit", "traits": { "smithy.api#enumValue": "DELETING" } }, - "ACTIVE": { + "FAILED": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": "ACTIVE" + "smithy.api#enumValue": "FAILED" } } } }, - "com.amazonaws.kinesis#CreateStream": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#CreateStreamInput" - }, - "output": { - "target": "smithy.api#Unit" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, - { - "target": "com.amazonaws.kinesis#ValidationException" - } - ], + "com.amazonaws.kinesis#ChannelStatusReason": { + "type": "string", "traits": { - "smithy.api#documentation": "

Creates a Kinesis data stream. A stream captures and transports data records that are\n continuously emitted from different data sources or producers.\n Scale-out within a stream is explicitly supported by means of shards, which are uniquely\n identified groups of data records in a stream.

\n

You can create your data stream using either on-demand or provisioned capacity mode. Data streams with an on-demand mode require no capacity planning and automatically scale to handle gigabytes of write and read throughput per minute. With the on-demand mode, Kinesis Data Streams automatically manages the shards in order to provide the necessary throughput.

\n

If you'd still like to proactively scale your on-demand data stream\u2019s capacity, you can unlock the warm throughput feature for on-demand data streams by enabling MinimumThroughputBillingCommitment for your account. Once your account has MinimumThroughputBillingCommitment enabled, you can specify the warm throughput in MiB per second that your stream can support in writes.

\n

For the data streams with a provisioned mode, you must specify the number of shards for the data stream. Each shard can support reads up to five transactions per second, up to a maximum data read total of 2 MiB per second. Each shard can support writes up to 1,000 records per second, up to a maximum data write total of 1 MiB per second. If the amount of data input increases or decreases, you can add or remove shards.

\n

The stream name identifies the stream. The name is scoped to the Amazon Web Services\n account used by the application. It is also scoped by Amazon Web Services Region. That\n is, two streams in two different accounts can have the same name, and two streams in the\n same account, but in two different Regions, can have the same name.

\n

\n CreateStream is an asynchronous operation. Upon receiving a\n CreateStream request, Kinesis Data Streams immediately returns and sets\n the stream status to CREATING. After the stream is created, Kinesis Data\n Streams sets the stream status to ACTIVE. You should perform read and write\n operations only on an ACTIVE stream.

\n

You receive a LimitExceededException when making a\n CreateStream request when you try to do one of the following:

\n \n

For the default shard or on-demand throughput limits for an Amazon Web Services account, see Amazon Kinesis Data Streams Limits in the Amazon Kinesis Data Streams Developer Guide. To increase this limit, contact Amazon Web Services Support.

\n

You can use DescribeStreamSummary to check the stream status, which\n is returned in StreamStatus.

\n

\n CreateStream has a limit of five transactions per second per\n account.

\n

You can add tags to the stream when making a CreateStream request by setting the Tags parameter. If you pass the Tags parameter, in addition to having the kinesis:CreateStream permission, you must also have the kinesis:AddTagsToStream permission for the stream that will be created. The kinesis:TagResource permission won\u2019t work to tag streams on creation. Tags will take effect from the CREATING status of the stream, but you can't make any updates to the tags until the stream is in ACTIVE state.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } + "smithy.api#length": { + "min": 1, + "max": 2048 } } }, - "com.amazonaws.kinesis#CreateStreamInput": { + "com.amazonaws.kinesis#ChannelStreamConfiguration": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", "traits": { - "smithy.api#documentation": "

A name to identify the stream. The stream name is scoped to the Amazon Web Services\n account used by the application that creates the stream. It is also scoped by Amazon Web Services Region. That is, two streams in two different Amazon Web Services accounts\n can have the same name. Two streams in the same Amazon Web Services account but in two\n different Regions can also have the same name.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the source Kinesis data stream.

", "smithy.api#required": {} } }, - "ShardCount": { - "target": "com.amazonaws.kinesis#PositiveIntegerObject", - "traits": { - "smithy.api#documentation": "

The number of shards that the stream will use. The throughput of the stream is a\n function of the number of shards; more shards are required for greater provisioned\n throughput.

" - } - }, - "StreamModeDetails": { - "target": "com.amazonaws.kinesis#StreamModeDetails", - "traits": { - "smithy.api#documentation": "

Indicates the capacity mode of the data stream. Currently, in Kinesis Data Streams,\n you can choose between an on-demand capacity mode and a\n provisioned capacity mode for your data\n streams.

" - } - }, - "Tags": { - "target": "com.amazonaws.kinesis#TagMap", - "traits": { - "smithy.api#documentation": "

A set of up to 50 key-value pairs to use to create the tags. A tag consists of a required key and an optional value.

" - } - }, - "WarmThroughputMiBps": { - "target": "com.amazonaws.kinesis#NaturalIntegerObject", - "traits": { - "smithy.api#documentation": "

The target warm throughput in MB/s that the stream should be scaled to handle. This represents the throughput capacity that will be immediately available for write operations.

" - } - }, - "MaxRecordSizeInKiB": { - "target": "com.amazonaws.kinesis#MaxRecordSizeInKiB", + "RecordConfiguration": { + "target": "com.amazonaws.kinesis#RecordConfiguration", "traits": { - "smithy.api#documentation": "

The maximum record size of a single record in kibibyte (KiB) that you can write to, and read from a stream.

" + "smithy.api#documentation": "

The record format configuration for the source stream.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

Represents the input for CreateStream.

", - "smithy.api#input": {} + "smithy.api#documentation": "

Specifies the source stream and record configuration when creating a channel.

" } }, - "com.amazonaws.kinesis#Data": { - "type": "blob", + "com.amazonaws.kinesis#ChannelStreamConfigurationList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#ChannelStreamConfiguration" + }, "traits": { "smithy.api#length": { - "min": 0, - "max": 10485760 + "min": 1, + "max": 10000 } } }, - "com.amazonaws.kinesis#DecreaseStreamRetentionPeriod": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DecreaseStreamRetentionPeriodInput" - }, - "output": { - "target": "smithy.api#Unit" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#AccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], - "traits": { - "smithy.api#documentation": "

Decreases the Kinesis data stream's retention period, which is the length of time data\n records are accessible after they are added to the stream. The minimum value of a\n stream's retention period is 24 hours.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

This operation may result in lost data. For example, if the stream's retention period\n is 48 hours and is decreased to 24 hours, any data already in the stream that is older\n than 24 hours is inaccessible.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } - } - } - }, - "com.amazonaws.kinesis#DecreaseStreamRetentionPeriodInput": { + "com.amazonaws.kinesis#ChannelStreamDescription": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", - "traits": { - "smithy.api#documentation": "

The name of the stream to modify.

" - } - }, - "RetentionPeriodHours": { - "target": "com.amazonaws.kinesis#RetentionPeriodHours", + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", "traits": { - "smithy.api#documentation": "

The new retention period of the stream, in hours. Must be less than the current\n retention period.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the source Kinesis data stream.

", "smithy.api#required": {} } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "StreamCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

The time at which the source stream was created.

", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "RecordConfiguration": { + "target": "com.amazonaws.kinesis#RecordConfiguration", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The record format configuration for the source stream.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

Represents the input for DecreaseStreamRetentionPeriod.

", - "smithy.api#input": {} + "smithy.api#documentation": "

Describes the source stream of a channel.

" } }, - "com.amazonaws.kinesis#DeleteResourcePolicy": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DeleteResourcePolicyInput" - }, - "output": { - "target": "smithy.api#Unit" + "com.amazonaws.kinesis#ChannelStreamDescriptionList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#ChannelStreamDescription" }, - "errors": [ - { - "target": "com.amazonaws.kinesis#AccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], "traits": { - "smithy.api#documentation": "

Delete a policy for the specified data stream or consumer. Request patterns can be one of the following:

\n ", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } + "smithy.api#length": { + "min": 1, + "max": 10000 } } }, - "com.amazonaws.kinesis#DeleteResourcePolicyInput": { + "com.amazonaws.kinesis#ChannelStreamIdentifier": { "type": "structure", "members": { - "ResourceARN": { - "target": "com.amazonaws.kinesis#ResourceARN", + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", "traits": { - "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", - "smithy.api#required": {}, - "smithy.rules#contextParam": { - "name": "ResourceARN" - } + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the source Kinesis data stream.

", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "StreamCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The time at which the source stream was created.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#input": {} + "smithy.api#documentation": "

Identifies a source stream associated with a channel.

" } }, - "com.amazonaws.kinesis#DeleteStream": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DeleteStreamInput" - }, - "output": { - "target": "smithy.api#Unit" + "com.amazonaws.kinesis#ChannelStreamIdentifierList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#ChannelStreamIdentifier" }, - "errors": [ - { - "target": "com.amazonaws.kinesis#AccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], "traits": { - "smithy.api#documentation": "

Deletes a Kinesis data stream and all its shards and data. You must shut down any\n applications that are operating on the stream before you delete the stream. If an\n application attempts to operate on a deleted stream, it receives the exception\n ResourceNotFoundException.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If the stream is in the ACTIVE state, you can delete it. After a\n DeleteStream request, the specified stream is in the\n DELETING state until Kinesis Data Streams completes the\n deletion.

\n

\n Note: Kinesis Data Streams might continue to accept\n data read and write operations, such as PutRecord, PutRecords, and GetRecords, on a stream in the\n DELETING state until the stream deletion is complete.

\n

When you delete a stream, any shards in that stream are also deleted, and any tags are\n dissociated from the stream.

\n

You can use the DescribeStreamSummary operation to check the state\n of the stream, which is returned in StreamStatus.

\n

\n DeleteStream has a limit of five transactions per second per\n account.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } + "smithy.api#length": { + "min": 1, + "max": 10000 } } }, - "com.amazonaws.kinesis#DeleteStreamInput": { + "com.amazonaws.kinesis#ChannelSummary": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", + "ChannelName": { + "target": "com.amazonaws.kinesis#ChannelName", "traits": { - "smithy.api#documentation": "

The name of the stream to delete.

" + "smithy.api#documentation": "

The name of the channel.

", + "smithy.api#required": {} } }, - "EnforceConsumerDeletion": { - "target": "com.amazonaws.kinesis#BooleanObject", + "ChannelARN": { + "target": "com.amazonaws.kinesis#ChannelARN", "traits": { - "smithy.api#documentation": "

If this parameter is unset (null) or if you set it to false,\n and the stream has registered consumers, the call to DeleteStream fails\n with a ResourceInUseException.

" + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the channel.

", + "smithy.api#required": {} } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "ChannelId": { + "target": "com.amazonaws.kinesis#ChannelId", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

The unique identifier of the channel.

", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "ChannelStatus": { + "target": "com.amazonaws.kinesis#ChannelStatus", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The current status of the channel. Valid values:

\n ", + "smithy.api#required": {} } - } - }, - "traits": { - "smithy.api#documentation": "

Represents the input for DeleteStream.

", - "smithy.api#input": {} - } - }, - "com.amazonaws.kinesis#DeregisterStreamConsumer": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DeregisterStreamConsumerInput" - }, - "output": { - "target": "smithy.api#Unit" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], - "traits": { - "smithy.api#documentation": "

To deregister a consumer, provide its ARN. Alternatively, you can provide the ARN of\n the data stream and the name you gave the consumer when you registered it. You may also\n provide all three parameters, as long as they don't conflict with each other. If you\n don't know the name or ARN of the consumer that you want to deregister, you can use the\n ListStreamConsumers operation to get a list of the descriptions of\n all the consumers that are currently registered with a given data stream. The\n description of a consumer contains its name and ARN.

\n

This operation has a limit of five transactions per second per stream.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } - } - } - }, - "com.amazonaws.kinesis#DeregisterStreamConsumerInput": { - "type": "structure", - "members": { - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "ChannelStatusReason": { + "target": "com.amazonaws.kinesis#ChannelStatusReason", "traits": { - "smithy.api#documentation": "

The ARN of the Kinesis data stream that the consumer is registered with. For more\n information, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

A message describing the reason for a FAILED status.

" } }, - "ConsumerName": { - "target": "com.amazonaws.kinesis#ConsumerName", + "ChannelCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", "traits": { - "smithy.api#documentation": "

The name that you gave to the consumer.

" + "smithy.api#documentation": "

The time at which the channel was created.

", + "smithy.api#required": {} } }, - "ConsumerARN": { - "target": "com.amazonaws.kinesis#ConsumerARN", + "ChannelDestinationType": { + "target": "com.amazonaws.kinesis#ChannelDestinationType", "traits": { - "smithy.api#documentation": "

The ARN returned by Kinesis Data Streams when you registered the consumer. If you\n don't know the ARN of the consumer that you want to deregister, you can use the\n ListStreamConsumers operation to get a list of the descriptions of all the consumers\n that are currently registered with a given data stream. The description of a consumer\n contains its ARN.

", - "smithy.rules#contextParam": { - "name": "ConsumerARN" - } + "smithy.api#documentation": "

The destination type of the channel. Valid values:

\n ", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "Streams": { + "target": "com.amazonaws.kinesis#ChannelStreamIdentifierList", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The source streams associated with the channel.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#input": {} + "smithy.api#documentation": "

A summary of a channel, returned by ListChannels.

" } }, - "com.amazonaws.kinesis#DescribeAccountSettings": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DescribeAccountSettingsInput" - }, - "output": { - "target": "com.amazonaws.kinesis#DescribeAccountSettingsOutput" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#LimitExceededException" - } - ], - "traits": { - "smithy.api#documentation": "

Describes the account-level settings for Amazon Kinesis Data Streams. This operation returns information about the minimum throughput billing commitments and other account-level configurations.

\n

This API has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. TPS over 5 will initiate the LimitExceededException.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } - } + "com.amazonaws.kinesis#ChannelSummaryList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#ChannelSummary" } }, - "com.amazonaws.kinesis#DescribeAccountSettingsInput": { + "com.amazonaws.kinesis#ChildShard": { "type": "structure", - "members": {}, + "members": { + "ShardId": { + "target": "com.amazonaws.kinesis#ShardId", + "traits": { + "smithy.api#documentation": "

The shard ID of the existing child shard of the current shard.

", + "smithy.api#required": {} + } + }, + "ParentShards": { + "target": "com.amazonaws.kinesis#ShardIdList", + "traits": { + "smithy.api#documentation": "

The current shard that is the parent of the existing child shard.

", + "smithy.api#required": {} + } + }, + "HashKeyRange": { + "target": "com.amazonaws.kinesis#HashKeyRange", + "traits": { + "smithy.api#required": {} + } + } + }, "traits": { - "smithy.api#input": {} + "smithy.api#documentation": "

Output parameter of the GetRecords API. The existing child shard of the current\n shard.

" } }, - "com.amazonaws.kinesis#DescribeAccountSettingsOutput": { + "com.amazonaws.kinesis#ChildShardList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#ChildShard" + } + }, + "com.amazonaws.kinesis#CloudWatchLogGroupName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 512 + }, + "smithy.api#pattern": "^[\\.\\-_/#A-Za-z0-9]+$" + } + }, + "com.amazonaws.kinesis#CloudWatchLogStreamName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 512 + }, + "smithy.api#pattern": "^[^:*]*$" + } + }, + "com.amazonaws.kinesis#CloudWatchLogs": { "type": "structure", "members": { - "MinimumThroughputBillingCommitment": { - "target": "com.amazonaws.kinesis#MinimumThroughputBillingCommitmentOutput", + "Enabled": { + "target": "com.amazonaws.kinesis#BooleanObject", "traits": { - "smithy.api#documentation": "

The current configuration of the minimum throughput billing commitment for your Amazon Web Services account.

" + "smithy.api#documentation": "

Specifies whether logging to Amazon CloudWatch Logs is enabled.

", + "smithy.api#required": {} + } + }, + "LogGroupName": { + "target": "com.amazonaws.kinesis#CloudWatchLogGroupName", + "traits": { + "smithy.api#documentation": "

The name of the Amazon CloudWatch Logs log group. Defaults to /aws/kinesis/{channelName}/{channelId}.

" + } + }, + "LogStreamName": { + "target": "com.amazonaws.kinesis#CloudWatchLogStreamName", + "traits": { + "smithy.api#documentation": "

The name of the Amazon CloudWatch Logs log stream. Defaults to DestinationDelivery.

" } } }, "traits": { - "smithy.api#output": {} + "smithy.api#documentation": "

The Amazon CloudWatch Logs settings for channel logging.

" } }, - "com.amazonaws.kinesis#DescribeLimits": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DescribeLimitsInput" - }, - "output": { - "target": "com.amazonaws.kinesis#DescribeLimitsOutput" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#LimitExceededException" + "com.amazonaws.kinesis#CloudWatchLogsUpdateInput": { + "type": "structure", + "members": { + "Enabled": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Specifies whether logging to Amazon CloudWatch Logs is enabled.

", + "smithy.api#required": {} + } + }, + "LogGroupName": { + "target": "com.amazonaws.kinesis#CloudWatchLogGroupName", + "traits": { + "smithy.api#documentation": "

The name of the Amazon CloudWatch Logs log group.

" + } + }, + "LogStreamName": { + "target": "com.amazonaws.kinesis#CloudWatchLogStreamName", + "traits": { + "smithy.api#documentation": "

The name of the Amazon CloudWatch Logs log stream.

" + } } - ], + }, "traits": { - "smithy.api#documentation": "

Describes the shard limits and usage for the account.

\n

If you update your account limits, the old limits might be returned for a few\n minutes.

\n

This operation has a limit of one transaction per second per account.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" + "smithy.api#documentation": "

The updated Amazon CloudWatch Logs settings for a channel.

" + } + }, + "com.amazonaws.kinesis#Consumer": { + "type": "structure", + "members": { + "ConsumerName": { + "target": "com.amazonaws.kinesis#ConsumerName", + "traits": { + "smithy.api#documentation": "

The name of the consumer is something you choose when you register the\n consumer.

", + "smithy.api#required": {} + } + }, + "ConsumerARN": { + "target": "com.amazonaws.kinesis#ConsumerARN", + "traits": { + "smithy.api#documentation": "

When you register a consumer, Kinesis Data Streams generates an ARN for it. You need\n this ARN to be able to call SubscribeToShard.

\n

If you delete a consumer and then create a new one with the same name, it won't have\n the same ARN. That's because consumer ARNs contain the creation timestamp. This is\n important to keep in mind if you have IAM policies that reference consumer ARNs.

", + "smithy.api#required": {} + } + }, + "ConsumerStatus": { + "target": "com.amazonaws.kinesis#ConsumerStatus", + "traits": { + "smithy.api#documentation": "

A consumer can't read data while in the CREATING or DELETING\n states.

", + "smithy.api#required": {} + } + }, + "ConsumerCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", + "traits": { + "smithy.api#documentation": "

", + "smithy.api#required": {} } } + }, + "traits": { + "smithy.api#documentation": "

An object that represents the details of the consumer you registered. This type of\n object is returned by RegisterStreamConsumer.

" } }, - "com.amazonaws.kinesis#DescribeLimitsInput": { - "type": "structure", - "members": {}, + "com.amazonaws.kinesis#ConsumerARN": { + "type": "string", "traits": { - "smithy.api#input": {} + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^(arn):aws.*:kinesis:.*:\\d{12}:.*stream\\/[a-zA-Z0-9_.-]+\\/consumer\\/[a-zA-Z0-9_.-]+:[0-9]+$" } }, - "com.amazonaws.kinesis#DescribeLimitsOutput": { + "com.amazonaws.kinesis#ConsumerCountObject": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 0, + "max": 1000000 + } + } + }, + "com.amazonaws.kinesis#ConsumerDescription": { "type": "structure", "members": { - "ShardLimit": { - "target": "com.amazonaws.kinesis#ShardCountObject", + "ConsumerName": { + "target": "com.amazonaws.kinesis#ConsumerName", "traits": { - "smithy.api#documentation": "

The maximum number of shards.

", + "smithy.api#documentation": "

The name of the consumer is something you choose when you register the\n consumer.

", "smithy.api#required": {} } }, - "OpenShardCount": { - "target": "com.amazonaws.kinesis#ShardCountObject", + "ConsumerARN": { + "target": "com.amazonaws.kinesis#ConsumerARN", "traits": { - "smithy.api#documentation": "

The number of open shards.

", + "smithy.api#documentation": "

When you register a consumer, Kinesis Data Streams generates an ARN for it. You need\n this ARN to be able to call SubscribeToShard.

\n

If you delete a consumer and then create a new one with the same name, it won't have\n the same ARN. That's because consumer ARNs contain the creation timestamp. This is\n important to keep in mind if you have IAM policies that reference consumer ARNs.

", "smithy.api#required": {} } }, - "OnDemandStreamCount": { - "target": "com.amazonaws.kinesis#OnDemandStreamCountObject", + "ConsumerStatus": { + "target": "com.amazonaws.kinesis#ConsumerStatus", "traits": { - "smithy.api#documentation": "

Indicates the number of data streams with the on-demand capacity mode.

", + "smithy.api#documentation": "

A consumer can't read data while in the CREATING or DELETING\n states.

", "smithy.api#required": {} } }, - "OnDemandStreamCountLimit": { - "target": "com.amazonaws.kinesis#OnDemandStreamCountLimitObject", + "ConsumerCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", "traits": { - "smithy.api#documentation": "

The maximum number of data streams with the on-demand capacity mode.

", + "smithy.api#documentation": "

", + "smithy.api#required": {} + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream with which you registered the consumer.

", "smithy.api#required": {} } } }, "traits": { - "smithy.api#output": {} + "smithy.api#documentation": "

An object that represents the details of a registered consumer. This type of object is\n returned by DescribeStreamConsumer.

" } }, - "com.amazonaws.kinesis#DescribeStream": { + "com.amazonaws.kinesis#ConsumerList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#Consumer" + } + }, + "com.amazonaws.kinesis#ConsumerName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 128 + }, + "smithy.api#pattern": "^[a-zA-Z0-9_.-]+$" + } + }, + "com.amazonaws.kinesis#ConsumerStatus": { + "type": "enum", + "members": { + "CREATING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CREATING" + } + }, + "DELETING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DELETING" + } + }, + "ACTIVE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ACTIVE" + } + } + } + }, + "com.amazonaws.kinesis#CreateChannel": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#DescribeStreamInput" + "target": "com.amazonaws.kinesis#CreateChannelInput" }, "output": { - "target": "com.amazonaws.kinesis#DescribeStreamOutput" + "target": "com.amazonaws.kinesis#CreateChannelOutput" }, "errors": [ { @@ -783,86 +824,227 @@ { "target": "com.amazonaws.kinesis#InvalidArgumentException" }, + { + "target": "com.amazonaws.kinesis#KMSAccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#KMSDisabledException" + }, + { + "target": "com.amazonaws.kinesis#KMSInvalidStateException" + }, + { + "target": "com.amazonaws.kinesis#KMSNotFoundException" + }, + { + "target": "com.amazonaws.kinesis#KMSOptInRequired" + }, + { + "target": "com.amazonaws.kinesis#KMSThrottlingException" + }, { "target": "com.amazonaws.kinesis#LimitExceededException" }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, { "target": "com.amazonaws.kinesis#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.kinesis#ValidationException" } ], "traits": { - "smithy.api#documentation": "

Describes the specified Kinesis data stream.

\n \n

This API has been revised. It's highly recommended that you use the DescribeStreamSummary API to get a summarized description of the\n specified Kinesis data stream and the ListShards API to list the\n shards in a specified data stream and obtain information about each shard.

\n
\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

The information returned includes the stream name, Amazon Resource Name (ARN),\n creation time, enhanced metric configuration, and shard map. The shard map is an array\n of shard objects. For each shard object, there is the hash key and sequence number\n ranges that the shard spans, and the IDs of any earlier shards that played in a role in\n creating the shard. Every record ingested in the stream is identified by a sequence\n number, which is assigned when the record is put into the stream.

\n

You can limit the number of shards returned by each call. For more information, see\n Retrieving\n Shards from a Stream in the Amazon Kinesis Data Streams Developer\n Guide.

\n

There are no guarantees about the chronological order shards returned. To process\n shards in chronological order, use the ID of the parent shard to track the lineage to\n the oldest shard.

\n

This operation has a limit of 10 transactions per second per account.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" - } - }, - "smithy.test#smokeTests": [ + "smithy.api#documentation": "

Creates a channel that delivers records from a Kinesis data stream to a destination. A channel reads records from the specified stream and writes them to streaming tables on Apache Iceberg (Amazon S3 Tables) or to a general purpose Amazon S3 bucket.

\n

You must specify either S3DestinationConfiguration or S3TablesDestinationConfiguration, but not both.

\n

To use this operation, you must have permission to pass the specified service execution IAM role to Amazon Kinesis Data Streams (the iam:PassRole permission on that role).

\n

Creating a channel is an asynchronous operation. Upon receiving the request, Amazon Kinesis Data Streams returns immediately with the channel in the CREATING state. After provisioning is complete, Amazon Kinesis Data Streams sets the state to ACTIVE. You can use DescribeChannel to check the current state.

\n

This operation is only supported for data streams with the on-demand capacity mode.

\n

This operation has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. Exceeding 5 TPS results in a LimitExceededException.

", + "smithy.api#examples": [ { - "id": "DescribeStreamFailure", - "params": { - "StreamName": "bogus-stream-name" - }, - "vendorParams": { - "region": "us-west-2" + "title": "To create an S3 channel", + "input": { + "ChannelName": "my-channel-name", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ], + "S3DestinationConfiguration": { + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::my-channel-bucket", + "ExpectedBucketOwner": "123456789012", + "CompressionType": "ZSTD" + }, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012" + } + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" + } + } }, - "vendorParamsShape": "aws.test#AwsVendorParams", - "expect": { - "failure": {} - } - } - ], - "smithy.waiters#waitable": { - "StreamExists": { - "acceptors": [ - { - "state": "success", - "matcher": { - "output": { - "path": "StreamDescription.StreamStatus", - "expected": "ACTIVE", - "comparator": "stringEquals" + "output": { + "ChannelDescription": { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "CREATING", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ], + "S3DestinationConfiguration": { + "DataFreshnessInSeconds": 300, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012", + "ErrorOutputPrefix": "kinesis-channel/errors/my-channel/my-channel-id/" + }, + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::my-channel-bucket", + "ExpectedBucketOwner": "123456789012", + "OutputKeyTemplate": "kinesis-channel/!{channel-name}/!{channel-id}/!{yyyy}/!{MM}/!{dd}/!{HH}/!{channel-name}-!{channel-id}-!{yyyy}-!{MM}-!{dd}-!{HH}-!{mm}!{extension}", + "StorageClass": "STANDARD", + "CompressionType": "ZSTD" + } + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" } } } - ], - "minDelay": 10 + } }, - "StreamNotExists": { - "acceptors": [ - { - "state": "success", - "matcher": { - "errorType": "ResourceNotFoundException" + { + "title": "To create an S3 Tables channel", + "input": { + "ChannelName": "my-channel-name", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "RecordConfiguration": { + "RecordFormatType": "JSON", + "GSRSchemaARN": "arn:aws:glue:us-east-1:123456789012:schema/my-registry/my-schema" + } + } + ], + "S3TablesDestinationConfiguration": { + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012" + }, + "S3TablesConfigurationList": [ + { + "TableBucketARN": "arn:aws:s3tables:us-east-1:123456789012:bucket/my-table-bucket", + "Namespace": "my_namespace", + "TableName": "my_table", + "CompressionType": "ZSTD", + "PartitionSpec": { + "PartitionFields": [ + { + "Transform": "TIME_HOUR", + "SourceName": "creation_ts" + } + ] + } + } + ] + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" } } - ], - "minDelay": 10 + }, + "output": { + "ChannelDescription": { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "CREATING", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z", + "RecordConfiguration": { + "RecordFormatType": "JSON", + "GSRSchemaARN": "arn:aws:glue:us-east-1:123456789012:schema/my-registry/my-schema" + } + } + ], + "S3TablesDestinationConfiguration": { + "DataFreshnessInSeconds": 300, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012", + "ErrorOutputPrefix": "kinesis-channel/errors/my-channel/my-channel-id/" + }, + "S3TablesConfigurationList": [ + { + "TableBucketARN": "arn:aws:s3tables:us-east-1:123456789012:bucket/my-table-bucket", + "Namespace": "my_namespace", + "TableName": "my_table", + "CompressionType": "ZSTD", + "PartitionSpec": { + "PartitionFields": [ + { + "Transform": "TIME_HOUR", + "SourceName": "creation_ts" + } + ] + } + } + ] + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" + } + } + } + } } - } - } - }, - "com.amazonaws.kinesis#DescribeStreamConsumer": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#DescribeStreamConsumerInput" - }, - "output": { - "target": "com.amazonaws.kinesis#DescribeStreamConsumerOutput" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], - "traits": { - "smithy.api#documentation": "

To get the description of a registered consumer, provide the ARN of the consumer.\n Alternatively, you can provide the ARN of the data stream and the name you gave the\n consumer when you registered it. You may also provide all three parameters, as long as\n they don't conflict with each other. If you don't know the name or ARN of the consumer\n that you want to describe, you can use the ListStreamConsumers\n operation to get a list of the descriptions of all the consumers that are currently\n registered with a given data stream.

\n

This operation has a limit of 20 transactions per second per stream.

\n \n

When making a cross-account call with DescribeStreamConsumer, make sure to provide the ARN of the consumer.

\n
", + ], "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -870,40 +1052,58 @@ } } }, - "com.amazonaws.kinesis#DescribeStreamConsumerInput": { + "com.amazonaws.kinesis#CreateChannelInput": { "type": "structure", "members": { - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "ChannelName": { + "target": "com.amazonaws.kinesis#ChannelName", "traits": { - "smithy.api#documentation": "

The ARN of the Kinesis data stream that the consumer is registered with. For more\n information, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

The name of the channel. The name is unique within your Amazon Web Services account and Amazon Web Services Region.

", + "smithy.api#required": {} } }, - "ConsumerName": { - "target": "com.amazonaws.kinesis#ConsumerName", + "ServiceExecutionRoleARN": { + "target": "com.amazonaws.kinesis#RoleARN", "traits": { - "smithy.api#documentation": "

The name that you gave to the consumer.

" + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the IAM role that Amazon Kinesis Data Streams assumes to write records to the destination.

", + "smithy.api#required": {} } }, - "ConsumerARN": { - "target": "com.amazonaws.kinesis#ConsumerARN", + "StreamConfigurationList": { + "target": "com.amazonaws.kinesis#ChannelStreamConfigurationList", "traits": { - "smithy.api#documentation": "

The ARN returned by Kinesis Data Streams when you registered the consumer.

", - "smithy.rules#contextParam": { - "name": "ConsumerARN" - } + "smithy.api#documentation": "

The source stream configuration for the channel. Currently, one stream is supported per channel.

", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "S3DestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3DestinationConfiguration", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The configuration for delivery to a general purpose Amazon S3 bucket. You must specify either S3DestinationConfiguration or S3TablesDestinationConfiguration, but not both.

" + } + }, + "S3TablesDestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3TablesDestinationConfiguration", + "traits": { + "smithy.api#documentation": "

The configuration for delivery to streaming tables on Apache Iceberg in Amazon S3 Tables. You must specify either S3DestinationConfiguration or S3TablesDestinationConfiguration, but not both.

" + } + }, + "EncryptionConfiguration": { + "target": "com.amazonaws.kinesis#ChannelEncryptionConfiguration", + "traits": { + "smithy.api#documentation": "

The server-side encryption configuration that uses an Amazon Web Services KMS key to encrypt data delivered to the destination.

" + } + }, + "Tags": { + "target": "com.amazonaws.kinesis#TagMap", + "traits": { + "smithy.api#documentation": "

A set of key-value pairs to assign to the channel. A tag consists of a required key and an optional value.

" + } + }, + "LoggingConfiguration": { + "target": "com.amazonaws.kinesis#ChannelLoggingConfiguration", + "traits": { + "smithy.api#documentation": "

The Amazon CloudWatch Logs configuration for the channel.

" } } }, @@ -911,13 +1111,13 @@ "smithy.api#input": {} } }, - "com.amazonaws.kinesis#DescribeStreamConsumerOutput": { + "com.amazonaws.kinesis#CreateChannelOutput": { "type": "structure", "members": { - "ConsumerDescription": { - "target": "com.amazonaws.kinesis#ConsumerDescription", + "ChannelDescription": { + "target": "com.amazonaws.kinesis#ChannelDescription", "traits": { - "smithy.api#documentation": "

An object that represents the details of the consumer.

", + "smithy.api#documentation": "

The configuration and current status of the channel.

", "smithy.api#required": {} } } @@ -926,83 +1126,130 @@ "smithy.api#output": {} } }, - "com.amazonaws.kinesis#DescribeStreamInput": { + "com.amazonaws.kinesis#CreateStream": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#CreateStreamInput" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Creates a Kinesis data stream. A stream captures and transports data records that are\n continuously emitted from different data sources or producers.\n Scale-out within a stream is explicitly supported by means of shards, which are uniquely\n identified groups of data records in a stream.

\n

You can create your data stream using either on-demand or provisioned capacity mode. Data streams with an on-demand mode require no capacity planning and automatically scale to handle gigabytes of write and read throughput per minute. With the on-demand mode, Kinesis Data Streams automatically manages the shards in order to provide the necessary throughput.

\n

If you'd still like to proactively scale your on-demand data stream\u2019s capacity, you can unlock the warm throughput feature for on-demand data streams by enabling MinimumThroughputBillingCommitment for your account. Once your account has MinimumThroughputBillingCommitment enabled, you can specify the warm throughput in MiB per second that your stream can support in writes.

\n

For the data streams with a provisioned mode, you must specify the number of shards for the data stream. Each shard can support reads up to five transactions per second, up to a maximum data read total of 2 MiB per second. Each shard can support writes up to 1,000 records per second, up to a maximum data write total of 1 MiB per second. If the amount of data input increases or decreases, you can add or remove shards.

\n

The stream name identifies the stream. The name is scoped to the Amazon Web Services\n account used by the application. It is also scoped by Amazon Web Services Region. That\n is, two streams in two different accounts can have the same name, and two streams in the\n same account, but in two different Regions, can have the same name.

\n

\n CreateStream is an asynchronous operation. Upon receiving a\n CreateStream request, Kinesis Data Streams immediately returns and sets\n the stream status to CREATING. After the stream is created, Kinesis Data\n Streams sets the stream status to ACTIVE. You should perform read and write\n operations only on an ACTIVE stream.

\n

You receive a LimitExceededException when making a\n CreateStream request when you try to do one of the following:

\n \n

For the default shard or on-demand throughput limits for an Amazon Web Services account, see Amazon Kinesis Data Streams Limits in the Amazon Kinesis Data Streams Developer Guide. To increase this limit, contact Amazon Web Services Support.

\n

You can use DescribeStreamSummary to check the stream status, which\n is returned in StreamStatus.

\n

\n CreateStream has a limit of five transactions per second per\n account.

\n

You can add tags to the stream when making a CreateStream request by setting the Tags parameter. If you pass the Tags parameter, in addition to having the kinesis:CreateStream permission, you must also have the kinesis:AddTagsToStream permission for the stream that will be created. The kinesis:TagResource permission won\u2019t work to tag streams on creation. Tags will take effect from the CREATING status of the stream, but you can't make any updates to the tags until the stream is in ACTIVE state.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#CreateStreamInput": { "type": "structure", "members": { "StreamName": { "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The name of the stream to describe.

" + "smithy.api#documentation": "

A name to identify the stream. The stream name is scoped to the Amazon Web Services\n account used by the application that creates the stream. It is also scoped by Amazon Web Services Region. That is, two streams in two different Amazon Web Services accounts\n can have the same name. Two streams in the same Amazon Web Services account but in two\n different Regions can also have the same name.

", + "smithy.api#required": {} } }, - "Limit": { - "target": "com.amazonaws.kinesis#DescribeStreamInputLimit", + "ShardCount": { + "target": "com.amazonaws.kinesis#PositiveIntegerObject", "traits": { - "smithy.api#documentation": "

The maximum number of shards to return in a single call. The default value is 100. If\n you specify a value greater than 100, at most 100 results are returned.

" + "smithy.api#documentation": "

The number of shards that the stream will use. The throughput of the stream is a\n function of the number of shards; more shards are required for greater provisioned\n throughput.

" } }, - "ExclusiveStartShardId": { - "target": "com.amazonaws.kinesis#ShardId", + "StreamModeDetails": { + "target": "com.amazonaws.kinesis#StreamModeDetails", "traits": { - "smithy.api#documentation": "

The shard ID of the shard to start with.

\n

Specify this parameter to indicate that you want to describe the stream starting with\n the shard whose ID immediately follows ExclusiveStartShardId.

\n

If you don't specify this parameter, the default behavior for\n DescribeStream is to describe the stream starting with the first shard\n in the stream.

" + "smithy.api#documentation": "

Indicates the capacity mode of the data stream. Currently, in Kinesis Data Streams,\n you can choose between an on-demand capacity mode and a\n provisioned capacity mode for your data\n streams.

" } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "Tags": { + "target": "com.amazonaws.kinesis#TagMap", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

A set of up to 50 key-value pairs to use to create the tags. A tag consists of a required key and an optional value.

" } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "WarmThroughputMiBps": { + "target": "com.amazonaws.kinesis#NaturalIntegerObject", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The target warm throughput in MB/s that the stream should be scaled to handle. This represents the throughput capacity that will be immediately available for write operations.

" + } + }, + "MaxRecordSizeInKiB": { + "target": "com.amazonaws.kinesis#MaxRecordSizeInKiB", + "traits": { + "smithy.api#documentation": "

The maximum record size of a single record in kibibyte (KiB) that you can write to, and read from a stream.

" } } }, "traits": { - "smithy.api#documentation": "

Represents the input for DescribeStream.

", + "smithy.api#documentation": "

Represents the input for CreateStream.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#DescribeStreamInputLimit": { - "type": "integer", + "com.amazonaws.kinesis#Data": { + "type": "blob", "traits": { - "smithy.api#range": { - "min": 1, - "max": 10000 + "smithy.api#length": { + "min": 0, + "max": 10485760 } } }, - "com.amazonaws.kinesis#DescribeStreamOutput": { + "com.amazonaws.kinesis#DataFreshnessInSeconds": { + "type": "integer" + }, + "com.amazonaws.kinesis#DeadLetterQueueS3Configuration": { "type": "structure", "members": { - "StreamDescription": { - "target": "com.amazonaws.kinesis#StreamDescription", + "BucketARN": { + "target": "com.amazonaws.kinesis#BucketARN", "traits": { - "smithy.api#documentation": "

The current status of the stream, the stream Amazon Resource Name (ARN), an array of\n shard objects that comprise the stream, and whether there are more shards\n available.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the dead-letter queue Amazon S3 bucket.

", + "smithy.api#required": {} + } + }, + "ExpectedBucketOwner": { + "target": "com.amazonaws.kinesis#ExpectedBucketOwner", + "traits": { + "smithy.api#documentation": "

The Amazon Web Services account ID of the expected owner of the dead-letter queue bucket.

", "smithy.api#required": {} } + }, + "ErrorOutputPrefix": { + "target": "com.amazonaws.kinesis#S3ErrorOutputPrefix", + "traits": { + "smithy.api#documentation": "

The Amazon S3 key prefix for error records.

" + } } }, "traits": { - "smithy.api#documentation": "

Represents the output for DescribeStream.

", - "smithy.api#output": {} + "smithy.api#documentation": "

The Amazon S3 dead-letter queue configuration for records that cannot be delivered.

" } }, - "com.amazonaws.kinesis#DescribeStreamSummary": { + "com.amazonaws.kinesis#DecreaseStreamRetentionPeriod": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#DescribeStreamSummaryInput" + "target": "com.amazonaws.kinesis#DecreaseStreamRetentionPeriodInput" }, "output": { - "target": "com.amazonaws.kinesis#DescribeStreamSummaryOutput" + "target": "smithy.api#Unit" }, "errors": [ { @@ -1014,12 +1261,15 @@ { "target": "com.amazonaws.kinesis#LimitExceededException" }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, { "target": "com.amazonaws.kinesis#ResourceNotFoundException" } ], "traits": { - "smithy.api#documentation": "

Provides a summarized description of the specified Kinesis data stream without the\n shard list.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

The information returned includes the stream name, Amazon Resource Name (ARN), status,\n record retention period, approximate creation time, monitoring, encryption details, and\n open shard count.

\n

\n DescribeStreamSummary has a limit of 20 transactions per second per\n account.

", + "smithy.api#documentation": "

Decreases the Kinesis data stream's retention period, which is the length of time data\n records are accessible after they are added to the stream. The minimum value of a\n stream's retention period is 24 hours.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

This operation may result in lost data. For example, if the stream's retention period\n is 48 hours and is decreased to 24 hours, any data already in the stream that is older\n than 24 hours is inaccessible.

", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -1027,13 +1277,20 @@ } } }, - "com.amazonaws.kinesis#DescribeStreamSummaryInput": { + "com.amazonaws.kinesis#DecreaseStreamRetentionPeriodInput": { "type": "structure", "members": { "StreamName": { "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The name of the stream to describe.

" + "smithy.api#documentation": "

The name of the stream to modify.

" + } + }, + "RetentionPeriodHours": { + "target": "com.amazonaws.kinesis#RetentionPeriodHours", + "traits": { + "smithy.api#documentation": "

The new retention period of the stream, in hours. Must be less than the current\n retention period.

", + "smithy.api#required": {} } }, "StreamARN": { @@ -1056,31 +1313,78 @@ } }, "traits": { + "smithy.api#documentation": "

Represents the input for DecreaseStreamRetentionPeriod.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#DescribeStreamSummaryOutput": { + "com.amazonaws.kinesis#DeleteChannel": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#DeleteChannelInput" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.kinesis#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Deletes the specified channel. Deleting a channel stops delivery from the source stream to the destination. Data already delivered to the destination is not deleted.

\n

A stream cannot be deleted while it has active channels. To delete the stream, first delete all channels attached to it. To find them, use ListChannels with a stream filter.

\n

This operation has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. Exceeding 5 TPS results in a LimitExceededException.

", + "smithy.api#examples": [ + { + "title": "To delete a channel", + "input": { + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id" + }, + "output": {} + } + ], + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#DeleteChannelInput": { "type": "structure", "members": { - "StreamDescriptionSummary": { - "target": "com.amazonaws.kinesis#StreamDescriptionSummary", + "ChannelARN": { + "target": "com.amazonaws.kinesis#ChannelARN", "traits": { - "smithy.api#documentation": "

A StreamDescriptionSummary containing information about the\n stream.

", - "smithy.api#required": {} + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the channel to delete.

", + "smithy.api#required": {}, + "smithy.rules#contextParam": { + "name": "ChannelARN" + } } } }, "traits": { - "smithy.api#output": {} + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#DisableEnhancedMonitoring": { + "com.amazonaws.kinesis#DeleteResourcePolicy": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#DisableEnhancedMonitoringInput" + "target": "com.amazonaws.kinesis#DeleteResourcePolicyInput" }, "output": { - "target": "com.amazonaws.kinesis#EnhancedMonitoringOutput" + "target": "smithy.api#Unit" }, "errors": [ { @@ -1100,7 +1404,7 @@ } ], "traits": { - "smithy.api#documentation": "

Disables enhanced monitoring.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
", + "smithy.api#documentation": "

Delete a policy for the specified data stream or consumer. Request patterns can be one of the following:

\n ", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -1108,28 +1412,16 @@ } } }, - "com.amazonaws.kinesis#DisableEnhancedMonitoringInput": { + "com.amazonaws.kinesis#DeleteResourcePolicyInput": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", - "traits": { - "smithy.api#documentation": "

The name of the Kinesis data stream for which to disable enhanced monitoring.

" - } - }, - "ShardLevelMetrics": { - "target": "com.amazonaws.kinesis#MetricsNameList", - "traits": { - "smithy.api#documentation": "

List of shard-level metrics to disable.

\n

The following are the valid shard-level metrics. The value \"ALL\" disables\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

", - "smithy.api#required": {} - } - }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "ResourceARN": { + "target": "com.amazonaws.kinesis#ResourceARN", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", + "smithy.api#required": {}, "smithy.rules#contextParam": { - "name": "StreamARN" + "name": "ResourceARN" } } }, @@ -1144,17 +1436,16 @@ } }, "traits": { - "smithy.api#documentation": "

Represents the input for DisableEnhancedMonitoring.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#EnableEnhancedMonitoring": { + "com.amazonaws.kinesis#DeleteStream": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#EnableEnhancedMonitoringInput" + "target": "com.amazonaws.kinesis#DeleteStreamInput" }, "output": { - "target": "com.amazonaws.kinesis#EnhancedMonitoringOutput" + "target": "smithy.api#Unit" }, "errors": [ { @@ -1174,7 +1465,7 @@ } ], "traits": { - "smithy.api#documentation": "

Enables enhanced Kinesis data stream monitoring for shard-level metrics.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
", + "smithy.api#documentation": "

Deletes a Kinesis data stream and all its shards and data. You must shut down any\n applications that are operating on the stream before you delete the stream. If an\n application attempts to operate on a deleted stream, it receives the exception\n ResourceNotFoundException.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If the stream is in the ACTIVE state, you can delete it. After a\n DeleteStream request, the specified stream is in the\n DELETING state until Kinesis Data Streams completes the\n deletion.

\n

\n Note: Kinesis Data Streams might continue to accept\n data read and write operations, such as PutRecord, PutRecords, and GetRecords, on a stream in the\n DELETING state until the stream deletion is complete.

\n

When you delete a stream, any shards in that stream are also deleted, and any tags are\n dissociated from the stream.

\n

You can use the DescribeStreamSummary operation to check the state\n of the stream, which is returned in StreamStatus.

\n

\n DeleteStream has a limit of five transactions per second per\n account.

", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -1182,20 +1473,19 @@ } } }, - "com.amazonaws.kinesis#EnableEnhancedMonitoringInput": { + "com.amazonaws.kinesis#DeleteStreamInput": { "type": "structure", "members": { "StreamName": { "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The name of the stream for which to enable enhanced monitoring.

" + "smithy.api#documentation": "

The name of the stream to delete.

" } }, - "ShardLevelMetrics": { - "target": "com.amazonaws.kinesis#MetricsNameList", + "EnforceConsumerDeletion": { + "target": "com.amazonaws.kinesis#BooleanObject", "traits": { - "smithy.api#documentation": "

List of shard-level metrics to enable.

\n

The following are the valid shard-level metrics. The value \"ALL\" enables\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

", - "smithy.api#required": {} + "smithy.api#documentation": "

If this parameter is unset (null) or if you set it to false,\n and the stream has registered consumers, the call to DeleteStream fails\n with a ResourceInUseException.

" } }, "StreamARN": { @@ -1218,277 +1508,315 @@ } }, "traits": { - "smithy.api#documentation": "

Represents the input for EnableEnhancedMonitoring.

", + "smithy.api#documentation": "

Represents the input for DeleteStream.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#EncryptionType": { - "type": "enum", - "members": { - "NONE": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": "NONE" - } + "com.amazonaws.kinesis#DeregisterStreamConsumer": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#DeregisterStreamConsumerInput" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, - "KMS": { - "target": "smithy.api#Unit", - "traits": { - "smithy.api#enumValue": "KMS" - } + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" } - } - }, - "com.amazonaws.kinesis#EnhancedMetrics": { - "type": "structure", - "members": { - "ShardLevelMetrics": { - "target": "com.amazonaws.kinesis#MetricsNameList", - "traits": { - "smithy.api#documentation": "

List of shard-level metrics.

\n

The following are the valid shard-level metrics. The value \"ALL\" enhances\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

" + ], + "traits": { + "smithy.api#documentation": "

To deregister a consumer, provide its ARN. Alternatively, you can provide the ARN of\n the data stream and the name you gave the consumer when you registered it. You may also\n provide all three parameters, as long as they don't conflict with each other. If you\n don't know the name or ARN of the consumer that you want to deregister, you can use the\n ListStreamConsumers operation to get a list of the descriptions of\n all the consumers that are currently registered with a given data stream. The\n description of a consumer contains its name and ARN.

\n

This operation has a limit of five transactions per second per stream.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" } } - }, - "traits": { - "smithy.api#documentation": "

Represents enhanced metrics types.

" - } - }, - "com.amazonaws.kinesis#EnhancedMonitoringList": { - "type": "list", - "member": { - "target": "com.amazonaws.kinesis#EnhancedMetrics" } }, - "com.amazonaws.kinesis#EnhancedMonitoringOutput": { + "com.amazonaws.kinesis#DeregisterStreamConsumerInput": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", "traits": { - "smithy.api#documentation": "

The name of the Kinesis data stream.

" + "smithy.api#documentation": "

The ARN of the Kinesis data stream that the consumer is registered with. For more\n information, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } } }, - "CurrentShardLevelMetrics": { - "target": "com.amazonaws.kinesis#MetricsNameList", + "ConsumerName": { + "target": "com.amazonaws.kinesis#ConsumerName", "traits": { - "smithy.api#documentation": "

Represents the current state of the metrics that are in the enhanced state before the\n operation.

" + "smithy.api#documentation": "

The name that you gave to the consumer.

" } }, - "DesiredShardLevelMetrics": { - "target": "com.amazonaws.kinesis#MetricsNameList", + "ConsumerARN": { + "target": "com.amazonaws.kinesis#ConsumerARN", "traits": { - "smithy.api#documentation": "

Represents the list of all the metrics that would be in the enhanced state after the\n operation.

" + "smithy.api#documentation": "

The ARN returned by Kinesis Data Streams when you registered the consumer. If you\n don't know the ARN of the consumer that you want to deregister, you can use the\n ListStreamConsumers operation to get a list of the descriptions of all the consumers\n that are currently registered with a given data stream. The description of a consumer\n contains its ARN.

", + "smithy.rules#contextParam": { + "name": "ConsumerARN" + } } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

" + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } } } }, "traits": { - "smithy.api#documentation": "

Represents the output for EnableEnhancedMonitoring and DisableEnhancedMonitoring.

" + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#ErrorCode": { - "type": "string" - }, - "com.amazonaws.kinesis#ErrorMessage": { - "type": "string" - }, - "com.amazonaws.kinesis#ExpiredIteratorException": { - "type": "structure", - "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", - "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "com.amazonaws.kinesis#DescribeAccountSettings": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#DescribeAccountSettingsInput" + }, + "output": { + "target": "com.amazonaws.kinesis#DescribeAccountSettingsOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#LimitExceededException" + } + ], + "traits": { + "smithy.api#documentation": "

Describes the account-level settings for Amazon Kinesis Data Streams. This operation returns information about the minimum throughput billing commitments and other account-level configurations.

\n

This API has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. TPS over 5 will initiate the LimitExceededException.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" } } - }, + } + }, + "com.amazonaws.kinesis#DescribeAccountSettingsInput": { + "type": "structure", + "members": {}, "traits": { - "smithy.api#documentation": "

The provided iterator exceeds the maximum age allowed.

", - "smithy.api#error": "client" + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#ExpiredNextTokenException": { + "com.amazonaws.kinesis#DescribeAccountSettingsOutput": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage" + "MinimumThroughputBillingCommitment": { + "target": "com.amazonaws.kinesis#MinimumThroughputBillingCommitmentOutput", + "traits": { + "smithy.api#documentation": "

The current configuration of the minimum throughput billing commitment for your Amazon Web Services account.

" + } } }, "traits": { - "smithy.api#documentation": "

The pagination token passed to the operation is expired.

", - "smithy.api#error": "client" + "smithy.api#output": {} } }, - "com.amazonaws.kinesis#GetRecords": { + "com.amazonaws.kinesis#DescribeChannel": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#GetRecordsInput" + "target": "com.amazonaws.kinesis#DescribeChannelInput" }, "output": { - "target": "com.amazonaws.kinesis#GetRecordsOutput" + "target": "com.amazonaws.kinesis#DescribeChannelOutput" }, "errors": [ { "target": "com.amazonaws.kinesis#AccessDeniedException" }, { - "target": "com.amazonaws.kinesis#ExpiredIteratorException" + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, { - "target": "com.amazonaws.kinesis#InternalFailureException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#KMSAccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#KMSDisabledException" - }, - { - "target": "com.amazonaws.kinesis#KMSInvalidStateException" - }, - { - "target": "com.amazonaws.kinesis#KMSNotFoundException" - }, - { - "target": "com.amazonaws.kinesis#KMSOptInRequired" - }, - { - "target": "com.amazonaws.kinesis#KMSThrottlingException" + "target": "com.amazonaws.kinesis#LimitExceededException" }, { - "target": "com.amazonaws.kinesis#ProvisionedThroughputExceededException" + "target": "com.amazonaws.kinesis#ResourceNotFoundException" }, { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" + "target": "com.amazonaws.kinesis#ValidationException" } ], "traits": { - "smithy.api#documentation": "

Gets data records from a Kinesis data stream's shard.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

Specify a shard iterator using the ShardIterator parameter. The shard\n iterator specifies the position in the shard from which you want to start reading data\n records sequentially. If there are no records available in the portion of the shard that\n the iterator points to, GetRecords returns an empty list. It might\n take multiple calls to get to a portion of the shard that contains records.

\n

You can scale by provisioning multiple shards per stream while considering service\n limits (for more information, see Amazon Kinesis Data Streams\n Limits in the Amazon Kinesis Data Streams Developer\n Guide). Your application should have one thread per shard, each reading\n continuously from its stream. To read from a stream continually, call GetRecords in a loop. Use GetShardIterator to get the\n shard iterator to specify in the first GetRecords call. GetRecords returns a new shard iterator in\n NextShardIterator. Specify the shard iterator returned in\n NextShardIterator in subsequent calls to GetRecords.\n If the shard has been closed, the shard iterator can't return more data and GetRecords returns null in NextShardIterator.\n You can terminate the loop when the shard is closed, or when the shard iterator reaches\n the record with the sequence number or other attribute that marks it as the last record\n to process.

\n

Each data record can be up to 1 MiB in size by default. Amazon Kinesis Data Streams supports \n large records up to 10 MiB in size, but the average throughput for your stream cannot exceed \n 1 MiB per second. For more information about how large records are handled, see \n Large records. \n Each shard can read up to 2 MiB per second. You can ensure that your calls don't exceed \n the maximum supported size or throughput by using the Limit parameter to \n specify the maximum number of records that GetRecords can return. \n Consider your average record size when determining this limit. The maximum number of records \n that can be returned per call is 10,000.

\n

The size of the data returned by GetRecords varies depending on the\n utilization of the shard. It is recommended that consumer applications retrieve records\n via the GetRecords command using the 5 TPS limit to remain caught up.\n Retrieving records less frequently can lead to consumer applications falling behind. The\n maximum size of data that GetRecords can return is 10 MiB. If a call\n returns this amount of data, subsequent calls made within the next 5 seconds throw\n ProvisionedThroughputExceededException. If there is insufficient\n provisioned throughput on the stream, subsequent calls made within the next 1 second\n throw ProvisionedThroughputExceededException. GetRecords\n doesn't return any data when it throws an exception. For this reason, we recommend that\n you wait 1 second between calls to GetRecords. However, it's possible\n that the application will get exceptions for longer than 1 second.

\n

To detect whether the application is falling behind in processing, you can use the\n MillisBehindLatest response attribute. You can also monitor the stream\n using CloudWatch metrics and other mechanisms (see Monitoring in the Amazon\n Kinesis Data Streams Developer Guide).

\n

Each Amazon Kinesis record includes a value, ApproximateArrivalTimestamp,\n that is set when a stream successfully receives and stores a record. This is commonly\n referred to as a server-side time stamp, whereas a client-side time stamp is set when a\n data producer creates or sends the record to a stream (a data producer is any data\n source putting data records into a stream, for example with PutRecords). The time stamp has millisecond precision. There are no guarantees about the time\n stamp accuracy, or that the time stamp is always increasing. For example, records in a\n shard or across a stream might have time stamps that are out of order.

\n

This operation has a limit of five transactions per second per shard.

", + "smithy.api#documentation": "

Describes the specified channel, including its configuration and current status.

\n

Use this operation to verify that a channel reached the ACTIVE state after creation, or to diagnose a channel in the FAILED state by reading the ChannelStatusReason.

\n

This operation has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. Exceeding 5 TPS results in a LimitExceededException.

", + "smithy.api#examples": [ + { + "title": "To describe a channel", + "input": { + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id" + }, + "output": { + "ChannelDescription": { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "ACTIVE", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ], + "S3DestinationConfiguration": { + "DataFreshnessInSeconds": 300, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012", + "ErrorOutputPrefix": "kinesis-channel/errors/my-channel/my-channel-id/" + }, + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::my-channel-bucket", + "ExpectedBucketOwner": "123456789012", + "OutputKeyTemplate": "kinesis-channel/!{channel-name}/!{channel-id}/!{yyyy}/!{MM}/!{dd}/!{HH}/!{channel-name}-!{channel-id}-!{yyyy}-!{MM}-!{dd}-!{HH}-!{mm}!{extension}", + "StorageClass": "STANDARD", + "CompressionType": "ZSTD" + } + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" + } + } + } + } + } + ], + "smithy.api#readonly": {}, "smithy.rules#staticContextParams": { "OperationType": { - "value": "data" + "value": "control" + } + }, + "smithy.waiters#waitable": { + "ChannelActive": { + "acceptors": [ + { + "state": "success", + "matcher": { + "output": { + "path": "ChannelDescription.ChannelStatus", + "expected": "ACTIVE", + "comparator": "stringEquals" + } + } + }, + { + "state": "retry", + "matcher": { + "output": { + "path": "ChannelDescription.ChannelStatus", + "expected": "CREATING", + "comparator": "stringEquals" + } + } + }, + { + "state": "retry", + "matcher": { + "output": { + "path": "ChannelDescription.ChannelStatus", + "expected": "UPDATING", + "comparator": "stringEquals" + } + } + }, + { + "state": "failure", + "matcher": { + "output": { + "path": "ChannelDescription.ChannelStatus", + "expected": "DELETING", + "comparator": "stringEquals" + } + } + }, + { + "state": "failure", + "matcher": { + "output": { + "path": "ChannelDescription.ChannelStatus", + "expected": "FAILED", + "comparator": "stringEquals" + } + } + } + ], + "minDelay": 10 } } } }, - "com.amazonaws.kinesis#GetRecordsInput": { + "com.amazonaws.kinesis#DescribeChannelInput": { "type": "structure", "members": { - "ShardIterator": { - "target": "com.amazonaws.kinesis#ShardIterator", - "traits": { - "smithy.api#documentation": "

The position in the shard from which you want to start sequentially reading data\n records. A shard iterator specifies this position using the sequence number of a data\n record in the shard.

", - "smithy.api#required": {} - } - }, - "Limit": { - "target": "com.amazonaws.kinesis#GetRecordsInputLimit", - "traits": { - "smithy.api#documentation": "

The maximum number of records to return. Specify a value of up to 10,000. If you\n specify a value that is greater than 10,000, GetRecords throws\n InvalidArgumentException. The default value is 10,000.

" - } - }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", - "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } - } - }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "ChannelARN": { + "target": "com.amazonaws.kinesis#ChannelARN", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the channel to describe.

", + "smithy.api#required": {}, "smithy.rules#contextParam": { - "name": "StreamId" + "name": "ChannelARN" } } } }, "traits": { - "smithy.api#documentation": "

Represents the input for GetRecords.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#GetRecordsInputLimit": { - "type": "integer", - "traits": { - "smithy.api#range": { - "min": 1, - "max": 10000 - } - } - }, - "com.amazonaws.kinesis#GetRecordsOutput": { + "com.amazonaws.kinesis#DescribeChannelOutput": { "type": "structure", "members": { - "Records": { - "target": "com.amazonaws.kinesis#RecordList", + "ChannelDescription": { + "target": "com.amazonaws.kinesis#ChannelDescription", "traits": { - "smithy.api#documentation": "

The data records retrieved from the shard.

", + "smithy.api#documentation": "

The configuration and current status of the channel.

", "smithy.api#required": {} } - }, - "NextShardIterator": { - "target": "com.amazonaws.kinesis#ShardIterator", - "traits": { - "smithy.api#documentation": "

The next position in the shard from which to start sequentially reading data records.\n If set to null, the shard has been closed and the requested iterator does\n not return any more data.

" - } - }, - "MillisBehindLatest": { - "target": "com.amazonaws.kinesis#MillisBehindLatest", - "traits": { - "smithy.api#documentation": "

The number of milliseconds the GetRecords response is from the tip\n of the stream, indicating how far behind current time the consumer is. A value of zero\n indicates that record processing is caught up, and there are no new records to process\n at this moment.

" - } - }, - "ChildShards": { - "target": "com.amazonaws.kinesis#ChildShardList", - "traits": { - "smithy.api#documentation": "

The list of the current shard's child shards, returned in the GetRecords\n API's response only when the end of the current shard is reached.

" - } } }, "traits": { - "smithy.api#documentation": "

Represents the output for GetRecords.

", "smithy.api#output": {} } }, - "com.amazonaws.kinesis#GetResourcePolicy": { + "com.amazonaws.kinesis#DescribeLimits": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#GetResourcePolicyInput" + "target": "com.amazonaws.kinesis#DescribeLimitsInput" }, "output": { - "target": "com.amazonaws.kinesis#GetResourcePolicyOutput" + "target": "com.amazonaws.kinesis#DescribeLimitsOutput" }, "errors": [ - { - "target": "com.amazonaws.kinesis#AccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, { "target": "com.amazonaws.kinesis#LimitExceededException" - }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" } ], "traits": { - "smithy.api#documentation": "

Returns a policy attached to the specified data stream or consumer. Request patterns can be one of the following:

\n ", + "smithy.api#documentation": "

Describes the shard limits and usage for the account.

\n

If you update your account limits, the old limits might be returned for a few\n minutes.

\n

This operation has a limit of one transaction per second per account.

", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -1496,123 +1824,187 @@ } } }, - "com.amazonaws.kinesis#GetResourcePolicyInput": { + "com.amazonaws.kinesis#DescribeLimitsInput": { "type": "structure", - "members": { - "ResourceARN": { - "target": "com.amazonaws.kinesis#ResourceARN", - "traits": { - "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", - "smithy.api#required": {}, - "smithy.rules#contextParam": { - "name": "ResourceARN" - } - } - }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", - "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } - } - } - }, + "members": {}, "traits": { "smithy.api#input": {} } }, - "com.amazonaws.kinesis#GetResourcePolicyOutput": { + "com.amazonaws.kinesis#DescribeLimitsOutput": { "type": "structure", "members": { - "Policy": { - "target": "com.amazonaws.kinesis#Policy", + "ShardLimit": { + "target": "com.amazonaws.kinesis#ShardCountObject", "traits": { - "smithy.api#documentation": "

Details of the resource policy. This is formatted as a JSON string.

", + "smithy.api#documentation": "

The maximum number of shards.

", + "smithy.api#required": {} + } + }, + "OpenShardCount": { + "target": "com.amazonaws.kinesis#ShardCountObject", + "traits": { + "smithy.api#documentation": "

The number of open shards.

", + "smithy.api#required": {} + } + }, + "OnDemandStreamCount": { + "target": "com.amazonaws.kinesis#OnDemandStreamCountObject", + "traits": { + "smithy.api#documentation": "

Indicates the number of data streams with the on-demand capacity mode.

", + "smithy.api#required": {} + } + }, + "OnDemandStreamCountLimit": { + "target": "com.amazonaws.kinesis#OnDemandStreamCountLimitObject", + "traits": { + "smithy.api#documentation": "

The maximum number of data streams with the on-demand capacity mode.

", "smithy.api#required": {} } + }, + "ChannelCount": { + "target": "com.amazonaws.kinesis#ChannelCountObject", + "traits": { + "smithy.api#documentation": "

The number of channels in the account.

" + } + }, + "ChannelCountLimit": { + "target": "com.amazonaws.kinesis#ChannelCountObject", + "traits": { + "smithy.api#documentation": "

The maximum number of channels allowed in the account.

" + } } }, "traits": { "smithy.api#output": {} } }, - "com.amazonaws.kinesis#GetShardIterator": { + "com.amazonaws.kinesis#DescribeStream": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#GetShardIteratorInput" + "target": "com.amazonaws.kinesis#DescribeStreamInput" }, "output": { - "target": "com.amazonaws.kinesis#GetShardIteratorOutput" + "target": "com.amazonaws.kinesis#DescribeStreamOutput" }, "errors": [ { "target": "com.amazonaws.kinesis#AccessDeniedException" }, - { - "target": "com.amazonaws.kinesis#InternalFailureException" - }, { "target": "com.amazonaws.kinesis#InvalidArgumentException" }, { - "target": "com.amazonaws.kinesis#ProvisionedThroughputExceededException" + "target": "com.amazonaws.kinesis#LimitExceededException" }, { "target": "com.amazonaws.kinesis#ResourceNotFoundException" } ], "traits": { - "smithy.api#documentation": "

Gets an Amazon Kinesis shard iterator. A shard iterator expires 5 minutes after it is\n returned to the requester.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

A shard iterator specifies the shard position from which to start reading data records\n sequentially. The position is specified using the sequence number of a data record in a\n shard. A sequence number is the identifier associated with every record ingested in the\n stream, and is assigned when a record is put into the stream. Each stream has one or\n more shards.

\n

You must specify the shard iterator type. For example, you can set the\n ShardIteratorType parameter to read exactly from the position denoted\n by a specific sequence number by using the AT_SEQUENCE_NUMBER shard\n iterator type. Alternatively, the parameter can read right after the sequence number by\n using the AFTER_SEQUENCE_NUMBER shard iterator type, using sequence numbers\n returned by earlier calls to PutRecord, PutRecords,\n GetRecords, or DescribeStream. In the request,\n you can specify the shard iterator type AT_TIMESTAMP to read records from\n an arbitrary point in time, TRIM_HORIZON to cause\n ShardIterator to point to the last untrimmed record in the shard in the\n system (the oldest data record in the shard), or LATEST so that you always\n read the most recent data in the shard.

\n

When you read repeatedly from a stream, use a GetShardIterator\n request to get the first shard iterator for use in your first GetRecords request and for subsequent reads use the shard iterator returned by the GetRecords request in NextShardIterator. A new shard\n iterator is returned by every GetRecords request in\n NextShardIterator, which you use in the ShardIterator\n parameter of the next GetRecords request.

\n

If a GetShardIterator request is made too often, you receive a\n ProvisionedThroughputExceededException. For more information about\n throughput limits, see GetRecords, and Streams Limits in the\n Amazon Kinesis Data Streams Developer Guide.

\n

If the shard is closed, GetShardIterator returns a valid iterator\n for the last sequence number of the shard. A shard can be closed as a result of using\n SplitShard or MergeShards.

\n

\n GetShardIterator has a limit of five transactions per second per\n account per open shard.

", + "smithy.api#documentation": "

Describes the specified Kinesis data stream.

\n \n

This API has been revised. It's highly recommended that you use the DescribeStreamSummary API to get a summarized description of the\n specified Kinesis data stream and the ListShards API to list the\n shards in a specified data stream and obtain information about each shard.

\n
\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

The information returned includes the stream name, Amazon Resource Name (ARN),\n creation time, enhanced metric configuration, and shard map. The shard map is an array\n of shard objects. For each shard object, there is the hash key and sequence number\n ranges that the shard spans, and the IDs of any earlier shards that played in a role in\n creating the shard. Every record ingested in the stream is identified by a sequence\n number, which is assigned when the record is put into the stream.

\n

You can limit the number of shards returned by each call. For more information, see\n Retrieving\n Shards from a Stream in the Amazon Kinesis Data Streams Developer\n Guide.

\n

There are no guarantees about the chronological order shards returned. To process\n shards in chronological order, use the ID of the parent shard to track the lineage to\n the oldest shard.

\n

This operation has a limit of 10 transactions per second per account.

", "smithy.rules#staticContextParams": { "OperationType": { - "value": "data" - } - } - } - }, - "com.amazonaws.kinesis#GetShardIteratorInput": { - "type": "structure", - "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", - "traits": { - "smithy.api#documentation": "

The name of the Amazon Kinesis data stream.

" + "value": "control" } }, - "ShardId": { - "target": "com.amazonaws.kinesis#ShardId", - "traits": { - "smithy.api#documentation": "

The shard ID of the Kinesis Data Streams shard to get the iterator for.

", - "smithy.api#required": {} + "smithy.test#smokeTests": [ + { + "id": "DescribeStreamFailure", + "params": { + "StreamName": "bogus-stream-name" + }, + "vendorParams": { + "region": "us-west-2" + }, + "vendorParamsShape": "aws.test#AwsVendorParams", + "expect": { + "failure": {} + } } - }, - "ShardIteratorType": { - "target": "com.amazonaws.kinesis#ShardIteratorType", - "traits": { - "smithy.api#documentation": "

Determines how the shard iterator is used to start reading data records from the\n shard.

\n

The following are the valid Amazon Kinesis shard iterator types:

\n ", - "smithy.api#required": {} + ], + "smithy.waiters#waitable": { + "StreamExists": { + "acceptors": [ + { + "state": "success", + "matcher": { + "output": { + "path": "StreamDescription.StreamStatus", + "expected": "ACTIVE", + "comparator": "stringEquals" + } + } + } + ], + "minDelay": 10 + }, + "StreamNotExists": { + "acceptors": [ + { + "state": "success", + "matcher": { + "errorType": "ResourceNotFoundException" + } + } + ], + "minDelay": 10 } + } + } + }, + "com.amazonaws.kinesis#DescribeStreamConsumer": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#DescribeStreamConsumerInput" + }, + "output": { + "target": "com.amazonaws.kinesis#DescribeStreamConsumerOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, - "StartingSequenceNumber": { - "target": "com.amazonaws.kinesis#SequenceNumber", + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

To get the description of a registered consumer, provide the ARN of the consumer.\n Alternatively, you can provide the ARN of the data stream and the name you gave the\n consumer when you registered it. You may also provide all three parameters, as long as\n they don't conflict with each other. If you don't know the name or ARN of the consumer\n that you want to describe, you can use the ListStreamConsumers\n operation to get a list of the descriptions of all the consumers that are currently\n registered with a given data stream.

\n

This operation has a limit of 20 transactions per second per stream.

\n \n

When making a cross-account call with DescribeStreamConsumer, make sure to provide the ARN of the consumer.

\n
", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#DescribeStreamConsumerInput": { + "type": "structure", + "members": { + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", "traits": { - "smithy.api#documentation": "

The sequence number of the data record in the shard from which to start reading. Used\n with shard iterator type AT_SEQUENCE_NUMBER and AFTER_SEQUENCE_NUMBER.

" + "smithy.api#documentation": "

The ARN of the Kinesis data stream that the consumer is registered with. For more\n information, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } } }, - "Timestamp": { - "target": "com.amazonaws.kinesis#Timestamp", + "ConsumerName": { + "target": "com.amazonaws.kinesis#ConsumerName", "traits": { - "smithy.api#documentation": "

The time stamp of the data record from which to start reading. Used with shard\n iterator type AT_TIMESTAMP. A time stamp is the Unix epoch date with precision in\n milliseconds. For example, 2016-04-04T19:58:46.480-00:00 or\n 1459799926.480. If a record with this exact time stamp does not exist,\n the iterator returned is for the next (later) record. If the time stamp is older than\n the current trim horizon, the iterator returned is for the oldest untrimmed data record\n (TRIM_HORIZON).

" + "smithy.api#documentation": "

The name that you gave to the consumer.

" } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "ConsumerARN": { + "target": "com.amazonaws.kinesis#ConsumerARN", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.api#documentation": "

The ARN returned by Kinesis Data Streams when you registered the consumer.

", "smithy.rules#contextParam": { - "name": "StreamARN" + "name": "ConsumerARN" } } }, @@ -1627,60 +2019,101 @@ } }, "traits": { - "smithy.api#documentation": "

Represents the input for GetShardIterator.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#GetShardIteratorOutput": { + "com.amazonaws.kinesis#DescribeStreamConsumerOutput": { "type": "structure", "members": { - "ShardIterator": { - "target": "com.amazonaws.kinesis#ShardIterator", + "ConsumerDescription": { + "target": "com.amazonaws.kinesis#ConsumerDescription", "traits": { - "smithy.api#documentation": "

The position in the shard from which to start reading data records sequentially. A\n shard iterator specifies this position using the sequence number of a data record in a\n shard.

" + "smithy.api#documentation": "

An object that represents the details of the consumer.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

Represents the output for GetShardIterator.

", "smithy.api#output": {} } }, - "com.amazonaws.kinesis#HashKey": { - "type": "string", - "traits": { - "smithy.api#pattern": "^0|([1-9]\\d{0,38})$" - } - }, - "com.amazonaws.kinesis#HashKeyRange": { + "com.amazonaws.kinesis#DescribeStreamInput": { "type": "structure", "members": { - "StartingHashKey": { - "target": "com.amazonaws.kinesis#HashKey", + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The starting hash key of the hash key range.

", - "smithy.api#required": {} + "smithy.api#documentation": "

The name of the stream to describe.

" } }, - "EndingHashKey": { - "target": "com.amazonaws.kinesis#HashKey", + "Limit": { + "target": "com.amazonaws.kinesis#DescribeStreamInputLimit", "traits": { - "smithy.api#documentation": "

The ending hash key of the hash key range.

", + "smithy.api#documentation": "

The maximum number of shards to return in a single call. The default value is 100. If\n you specify a value greater than 100, at most 100 results are returned.

" + } + }, + "ExclusiveStartShardId": { + "target": "com.amazonaws.kinesis#ShardId", + "traits": { + "smithy.api#documentation": "

The shard ID of the shard to start with.

\n

Specify this parameter to indicate that you want to describe the stream starting with\n the shard whose ID immediately follows ExclusiveStartShardId.

\n

If you don't specify this parameter, the default behavior for\n DescribeStream is to describe the stream starting with the first shard\n in the stream.

" + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the input for DescribeStream.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#DescribeStreamInputLimit": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 10000 + } + } + }, + "com.amazonaws.kinesis#DescribeStreamOutput": { + "type": "structure", + "members": { + "StreamDescription": { + "target": "com.amazonaws.kinesis#StreamDescription", + "traits": { + "smithy.api#documentation": "

The current status of the stream, the stream Amazon Resource Name (ARN), an array of\n shard objects that comprise the stream, and whether there are more shards\n available.

", "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

The range of possible hash key values for the shard, which is a set of ordered\n contiguous positive integers.

" + "smithy.api#documentation": "

Represents the output for DescribeStream.

", + "smithy.api#output": {} } }, - "com.amazonaws.kinesis#IncreaseStreamRetentionPeriod": { + "com.amazonaws.kinesis#DescribeStreamSummary": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#IncreaseStreamRetentionPeriodInput" + "target": "com.amazonaws.kinesis#DescribeStreamSummaryInput" }, "output": { - "target": "smithy.api#Unit" + "target": "com.amazonaws.kinesis#DescribeStreamSummaryOutput" }, "errors": [ { @@ -1692,15 +2125,12 @@ { "target": "com.amazonaws.kinesis#LimitExceededException" }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" - }, { "target": "com.amazonaws.kinesis#ResourceNotFoundException" } ], "traits": { - "smithy.api#documentation": "

Increases the Kinesis data stream's retention period, which is the length of time data\n records are accessible after they are added to the stream. The maximum value of a\n stream's retention period is 8760 hours (365 days).

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If you choose a longer stream retention period, this operation increases the time\n period during which records that have not yet expired are accessible. However, it does\n not make previous, expired data (older than the stream's previous retention period)\n accessible after the operation has been called. For example, if a stream's retention\n period is set to 24 hours and is increased to 168 hours, any data that is older than 24\n hours remains inaccessible to consumer applications.

", + "smithy.api#documentation": "

Provides a summarized description of the specified Kinesis data stream without the\n shard list.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

The information returned includes the stream name, Amazon Resource Name (ARN), status,\n record retention period, approximate creation time, monitoring, encryption details, and\n open shard count.

\n

\n DescribeStreamSummary has a limit of 20 transactions per second per\n account.

", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -1708,20 +2138,13 @@ } } }, - "com.amazonaws.kinesis#IncreaseStreamRetentionPeriodInput": { + "com.amazonaws.kinesis#DescribeStreamSummaryInput": { "type": "structure", "members": { "StreamName": { "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The name of the stream to modify.

" - } - }, - "RetentionPeriodHours": { - "target": "com.amazonaws.kinesis#RetentionPeriodHours", - "traits": { - "smithy.api#documentation": "

The new retention period of the stream, in hours. Must be more than the current\n retention period.

", - "smithy.api#required": {} + "smithy.api#documentation": "

The name of the stream to describe.

" } }, "StreamARN": { @@ -1744,113 +2167,270 @@ } }, "traits": { - "smithy.api#documentation": "

Represents the input for IncreaseStreamRetentionPeriod.

", "smithy.api#input": {} } }, - "com.amazonaws.kinesis#InternalFailureException": { - "type": "structure", - "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage" - } - }, - "traits": { - "smithy.api#documentation": "

The processing of the request failed because of an unknown error, exception, or\n failure.

", - "smithy.api#error": "server" - } - }, - "com.amazonaws.kinesis#InvalidArgumentException": { + "com.amazonaws.kinesis#DescribeStreamSummaryOutput": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "StreamDescriptionSummary": { + "target": "com.amazonaws.kinesis#StreamDescriptionSummary", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

A StreamDescriptionSummary containing information about the\n stream.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

A specified parameter exceeds its restrictions, is not supported, or can't be used.\n For more information, see the returned message.

", - "smithy.api#error": "client" + "smithy.api#output": {} } }, - "com.amazonaws.kinesis#KMSAccessDeniedException": { - "type": "structure", - "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", - "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" - } - } + "com.amazonaws.kinesis#DisableEnhancedMonitoring": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#DisableEnhancedMonitoringInput" }, - "traits": { - "smithy.api#documentation": "

The ciphertext references a key that doesn't exist or that you don't have access\n to.

", - "smithy.api#error": "client" - } - }, - "com.amazonaws.kinesis#KMSDisabledException": { - "type": "structure", - "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", - "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "output": { + "target": "com.amazonaws.kinesis#EnhancedMonitoringOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Disables enhanced monitoring.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#DisableEnhancedMonitoringInput": { + "type": "structure", + "members": { + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", + "traits": { + "smithy.api#documentation": "

The name of the Kinesis data stream for which to disable enhanced monitoring.

" + } + }, + "ShardLevelMetrics": { + "target": "com.amazonaws.kinesis#MetricsNameList", + "traits": { + "smithy.api#documentation": "

List of shard-level metrics to disable.

\n

The following are the valid shard-level metrics. The value \"ALL\" disables\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

", + "smithy.api#required": {} + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } } } }, "traits": { - "smithy.api#documentation": "

The request was rejected because the specified customer master key (CMK) isn't\n enabled.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

Represents the input for DisableEnhancedMonitoring.

", + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#KMSInvalidStateException": { + "com.amazonaws.kinesis#DryRunOperationException": { "type": "structure", "members": { "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "target": "com.amazonaws.kinesis#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The request was rejected because the DryRun parameter was specified.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#EnableEnhancedMonitoring": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#EnableEnhancedMonitoringInput" + }, + "output": { + "target": "com.amazonaws.kinesis#EnhancedMonitoringOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Enables enhanced Kinesis data stream monitoring for shard-level metrics.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#EnableEnhancedMonitoringInput": { + "type": "structure", + "members": { + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

The name of the stream for which to enable enhanced monitoring.

" + } + }, + "ShardLevelMetrics": { + "target": "com.amazonaws.kinesis#MetricsNameList", + "traits": { + "smithy.api#documentation": "

List of shard-level metrics to enable.

\n

The following are the valid shard-level metrics. The value \"ALL\" enables\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

", + "smithy.api#required": {} + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } } } }, "traits": { - "smithy.api#documentation": "

The request was rejected because the state of the specified resource isn't valid for\n this request. For more information, see How Key State Affects Use of a\n Customer Master Key in the Amazon Web Services Key Management\n Service Developer Guide.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

Represents the input for EnableEnhancedMonitoring.

", + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#KMSNotFoundException": { + "com.amazonaws.kinesis#EncryptionType": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "KMS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "KMS" + } + } + } + }, + "com.amazonaws.kinesis#EnhancedMetrics": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "ShardLevelMetrics": { + "target": "com.amazonaws.kinesis#MetricsNameList", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

List of shard-level metrics.

\n

The following are the valid shard-level metrics. The value \"ALL\" enhances\n every metric.

\n \n

For more information, see Monitoring the Amazon\n Kinesis Data Streams Service with Amazon CloudWatch in the Amazon\n Kinesis Data Streams Developer Guide.

" } } }, "traits": { - "smithy.api#documentation": "

The request was rejected because the specified entity or resource can't be\n found.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

Represents enhanced metrics types.

" } }, - "com.amazonaws.kinesis#KMSOptInRequired": { + "com.amazonaws.kinesis#EnhancedMonitoringList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#EnhancedMetrics" + } + }, + "com.amazonaws.kinesis#EnhancedMonitoringOutput": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

The name of the Kinesis data stream.

" + } + }, + "CurrentShardLevelMetrics": { + "target": "com.amazonaws.kinesis#MetricsNameList", + "traits": { + "smithy.api#documentation": "

Represents the current state of the metrics that are in the enhanced state before the\n operation.

" + } + }, + "DesiredShardLevelMetrics": { + "target": "com.amazonaws.kinesis#MetricsNameList", + "traits": { + "smithy.api#documentation": "

Represents the list of all the metrics that would be in the enhanced state after the\n operation.

" + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

" } } }, "traits": { - "smithy.api#documentation": "

The Amazon Web Services access key ID needs a subscription for the service.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

Represents the output for EnableEnhancedMonitoring and DisableEnhancedMonitoring.

" } }, - "com.amazonaws.kinesis#KMSThrottlingException": { + "com.amazonaws.kinesis#ErrorCode": { + "type": "string" + }, + "com.amazonaws.kinesis#ErrorMessage": { + "type": "string" + }, + "com.amazonaws.kinesis#ExpectedBucketOwner": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 12, + "max": 12 + }, + "smithy.api#pattern": "^\\d{12}$" + } + }, + "com.amazonaws.kinesis#ExpiredIteratorException": { "type": "structure", "members": { "message": { @@ -1861,155 +2441,751 @@ } }, "traits": { - "smithy.api#documentation": "

The request was denied due to request throttling. For more information about\n throttling, see Limits in\n the Amazon Web Services Key Management Service Developer\n Guide.

", + "smithy.api#documentation": "

The provided iterator exceeds the maximum age allowed.

", "smithy.api#error": "client" } }, - "com.amazonaws.kinesis#KeyId": { + "com.amazonaws.kinesis#ExpiredNextTokenException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The pagination token passed to the operation is expired.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#GSRSchemaARN": { "type": "string", "traits": { "smithy.api#length": { "min": 1, - "max": 2048 - } + "max": 512 + }, + "smithy.api#pattern": "^arn:aws[-a-z0-9]*:glue:[-a-z0-9]+:\\d{12}:schema/[-a-zA-Z0-9_$#.]+/[-a-zA-Z0-9_$#.]+$" } }, - "com.amazonaws.kinesis#Kinesis_20131202": { - "type": "service", - "version": "2013-12-02", - "operations": [ + "com.amazonaws.kinesis#GetRecords": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#GetRecordsInput" + }, + "output": { + "target": "com.amazonaws.kinesis#GetRecordsOutput" + }, + "errors": [ { - "target": "com.amazonaws.kinesis#AddTagsToStream" + "target": "com.amazonaws.kinesis#AccessDeniedException" }, { - "target": "com.amazonaws.kinesis#CreateStream" + "target": "com.amazonaws.kinesis#DryRunOperationException" }, { - "target": "com.amazonaws.kinesis#DecreaseStreamRetentionPeriod" - }, - { - "target": "com.amazonaws.kinesis#DeleteResourcePolicy" - }, - { - "target": "com.amazonaws.kinesis#DeleteStream" - }, - { - "target": "com.amazonaws.kinesis#DeregisterStreamConsumer" - }, - { - "target": "com.amazonaws.kinesis#DescribeAccountSettings" - }, - { - "target": "com.amazonaws.kinesis#DescribeLimits" - }, - { - "target": "com.amazonaws.kinesis#DescribeStream" - }, - { - "target": "com.amazonaws.kinesis#DescribeStreamConsumer" - }, - { - "target": "com.amazonaws.kinesis#DescribeStreamSummary" - }, - { - "target": "com.amazonaws.kinesis#DisableEnhancedMonitoring" + "target": "com.amazonaws.kinesis#ExpiredIteratorException" }, { - "target": "com.amazonaws.kinesis#EnableEnhancedMonitoring" + "target": "com.amazonaws.kinesis#InternalFailureException" }, { - "target": "com.amazonaws.kinesis#GetRecords" + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, { - "target": "com.amazonaws.kinesis#GetResourcePolicy" + "target": "com.amazonaws.kinesis#KMSAccessDeniedException" }, { - "target": "com.amazonaws.kinesis#GetShardIterator" + "target": "com.amazonaws.kinesis#KMSDisabledException" }, { - "target": "com.amazonaws.kinesis#IncreaseStreamRetentionPeriod" + "target": "com.amazonaws.kinesis#KMSInvalidStateException" }, { - "target": "com.amazonaws.kinesis#ListShards" + "target": "com.amazonaws.kinesis#KMSNotFoundException" }, { - "target": "com.amazonaws.kinesis#ListStreamConsumers" + "target": "com.amazonaws.kinesis#KMSOptInRequired" }, { - "target": "com.amazonaws.kinesis#ListStreams" + "target": "com.amazonaws.kinesis#KMSThrottlingException" }, { - "target": "com.amazonaws.kinesis#ListTagsForResource" + "target": "com.amazonaws.kinesis#ProvisionedThroughputExceededException" }, { - "target": "com.amazonaws.kinesis#ListTagsForStream" + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Gets data records from a Kinesis data stream's shard.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

Specify a shard iterator using the ShardIterator parameter. The shard\n iterator specifies the position in the shard from which you want to start reading data\n records sequentially. If there are no records available in the portion of the shard that\n the iterator points to, GetRecords returns an empty list. It might\n take multiple calls to get to a portion of the shard that contains records.

\n

You can scale by provisioning multiple shards per stream while considering service\n limits (for more information, see Amazon Kinesis Data Streams\n Limits in the Amazon Kinesis Data Streams Developer\n Guide). Your application should have one thread per shard, each reading\n continuously from its stream. To read from a stream continually, call GetRecords in a loop. Use GetShardIterator to get the\n shard iterator to specify in the first GetRecords call. GetRecords returns a new shard iterator in\n NextShardIterator. Specify the shard iterator returned in\n NextShardIterator in subsequent calls to GetRecords.\n If the shard has been closed, the shard iterator can't return more data and GetRecords returns null in NextShardIterator.\n You can terminate the loop when the shard is closed, or when the shard iterator reaches\n the record with the sequence number or other attribute that marks it as the last record\n to process.

\n

Each data record can be up to 1 MiB in size by default. Amazon Kinesis Data Streams supports \n large records up to 10 MiB in size, but the average throughput for your stream cannot exceed \n 1 MiB per second. For more information about how large records are handled, see \n Large records. \n Each shard can read up to 2 MiB per second. You can ensure that your calls don't exceed \n the maximum supported size or throughput by using the Limit parameter to \n specify the maximum number of records that GetRecords can return. \n Consider your average record size when determining this limit. The maximum number of records \n that can be returned per call is 10,000.

\n

The size of the data returned by GetRecords varies depending on the\n utilization of the shard. It is recommended that consumer applications retrieve records\n via the GetRecords command using the 5 TPS limit to remain caught up.\n Retrieving records less frequently can lead to consumer applications falling behind. The\n maximum size of data that GetRecords can return is 10 MiB. If a call\n returns this amount of data, subsequent calls made within the next 5 seconds throw\n ProvisionedThroughputExceededException. If there is insufficient\n provisioned throughput on the stream, subsequent calls made within the next 1 second\n throw ProvisionedThroughputExceededException. GetRecords\n doesn't return any data when it throws an exception. For this reason, we recommend that\n you wait 1 second between calls to GetRecords. However, it's possible\n that the application will get exceptions for longer than 1 second.

\n

To detect whether the application is falling behind in processing, you can use the\n MillisBehindLatest response attribute. You can also monitor the stream\n using CloudWatch metrics and other mechanisms (see Monitoring in the Amazon\n Kinesis Data Streams Developer Guide).

\n

Each Amazon Kinesis record includes a value, ApproximateArrivalTimestamp,\n that is set when a stream successfully receives and stores a record. This is commonly\n referred to as a server-side time stamp, whereas a client-side time stamp is set when a\n data producer creates or sends the record to a stream (a data producer is any data\n source putting data records into a stream, for example with PutRecords). The time stamp has millisecond precision. There are no guarantees about the time\n stamp accuracy, or that the time stamp is always increasing. For example, records in a\n shard or across a stream might have time stamps that are out of order.

\n

This operation has a limit of five transactions per second per shard.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "data" + } + } + } + }, + "com.amazonaws.kinesis#GetRecordsInput": { + "type": "structure", + "members": { + "ShardIterator": { + "target": "com.amazonaws.kinesis#ShardIterator", + "traits": { + "smithy.api#documentation": "

The position in the shard from which you want to start sequentially reading data\n records. A shard iterator specifies this position using the sequence number of a data\n record in the shard.

", + "smithy.api#required": {} + } }, - { - "target": "com.amazonaws.kinesis#MergeShards" + "Limit": { + "target": "com.amazonaws.kinesis#GetRecordsInputLimit", + "traits": { + "smithy.api#documentation": "

The maximum number of records to return. Specify a value of up to 10,000. If you\n specify a value that is greater than 10,000, GetRecords throws\n InvalidArgumentException. The default value is 10,000.

" + } }, - { - "target": "com.amazonaws.kinesis#PutRecord" + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } }, - { - "target": "com.amazonaws.kinesis#PutRecords" + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } }, - { - "target": "com.amazonaws.kinesis#PutResourcePolicy" + "DryRun": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Checks if your request will succeed. DryRun is an optional\n parameter.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the input for GetRecords.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#GetRecordsInputLimit": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 10000 + } + } + }, + "com.amazonaws.kinesis#GetRecordsOutput": { + "type": "structure", + "members": { + "Records": { + "target": "com.amazonaws.kinesis#RecordList", + "traits": { + "smithy.api#documentation": "

The data records retrieved from the shard.

", + "smithy.api#required": {} + } }, - { - "target": "com.amazonaws.kinesis#RegisterStreamConsumer" + "NextShardIterator": { + "target": "com.amazonaws.kinesis#ShardIterator", + "traits": { + "smithy.api#documentation": "

The next position in the shard from which to start sequentially reading data records.\n If set to null, the shard has been closed and the requested iterator does\n not return any more data.

" + } }, - { - "target": "com.amazonaws.kinesis#RemoveTagsFromStream" + "MillisBehindLatest": { + "target": "com.amazonaws.kinesis#MillisBehindLatest", + "traits": { + "smithy.api#documentation": "

The number of milliseconds the GetRecords response is from the tip\n of the stream, indicating how far behind current time the consumer is. A value of zero\n indicates that record processing is caught up, and there are no new records to process\n at this moment.

" + } }, + "ChildShards": { + "target": "com.amazonaws.kinesis#ChildShardList", + "traits": { + "smithy.api#documentation": "

The list of the current shard's child shards, returned in the GetRecords\n API's response only when the end of the current shard is reached.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the output for GetRecords.

", + "smithy.api#output": {} + } + }, + "com.amazonaws.kinesis#GetResourcePolicy": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#GetResourcePolicyInput" + }, + "output": { + "target": "com.amazonaws.kinesis#GetResourcePolicyOutput" + }, + "errors": [ { - "target": "com.amazonaws.kinesis#SplitShard" + "target": "com.amazonaws.kinesis#AccessDeniedException" }, { - "target": "com.amazonaws.kinesis#StartStreamEncryption" + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, { - "target": "com.amazonaws.kinesis#StopStreamEncryption" + "target": "com.amazonaws.kinesis#LimitExceededException" }, { - "target": "com.amazonaws.kinesis#SubscribeToShard" + "target": "com.amazonaws.kinesis#ResourceInUseException" }, { - "target": "com.amazonaws.kinesis#TagResource" + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Returns a policy attached to the specified data stream or consumer. Request patterns can be one of the following:

\n ", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#GetResourcePolicyInput": { + "type": "structure", + "members": { + "ResourceARN": { + "target": "com.amazonaws.kinesis#ResourceARN", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", + "smithy.api#required": {}, + "smithy.rules#contextParam": { + "name": "ResourceARN" + } + } }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#GetResourcePolicyOutput": { + "type": "structure", + "members": { + "Policy": { + "target": "com.amazonaws.kinesis#Policy", + "traits": { + "smithy.api#documentation": "

Details of the resource policy. This is formatted as a JSON string.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.kinesis#GetShardIterator": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#GetShardIteratorInput" + }, + "output": { + "target": "com.amazonaws.kinesis#GetShardIteratorOutput" + }, + "errors": [ { - "target": "com.amazonaws.kinesis#UntagResource" + "target": "com.amazonaws.kinesis#AccessDeniedException" }, { - "target": "com.amazonaws.kinesis#UpdateAccountSettings" + "target": "com.amazonaws.kinesis#DryRunOperationException" }, { - "target": "com.amazonaws.kinesis#UpdateMaxRecordSize" + "target": "com.amazonaws.kinesis#InternalFailureException" }, { - "target": "com.amazonaws.kinesis#UpdateShardCount" + "target": "com.amazonaws.kinesis#InvalidArgumentException" }, { - "target": "com.amazonaws.kinesis#UpdateStreamMode" + "target": "com.amazonaws.kinesis#ProvisionedThroughputExceededException" }, { - "target": "com.amazonaws.kinesis#UpdateStreamWarmThroughput" + "target": "com.amazonaws.kinesis#ResourceNotFoundException" } ], "traits": { - "aws.api#service": { - "sdkId": "Kinesis", - "arnNamespace": "kinesis", - "cloudFormationName": "Kinesis", - "cloudTrailEventSource": "kinesis.amazonaws.com", - "endpointPrefix": "kinesis" - }, - "aws.auth#sigv4": { - "name": "kinesis" - }, - "aws.protocols#awsJson1_1": { - "http": [ - "http/1.1", + "smithy.api#documentation": "

Gets an Amazon Kinesis shard iterator. A shard iterator expires 5 minutes after it is\n returned to the requester.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

A shard iterator specifies the shard position from which to start reading data records\n sequentially. The position is specified using the sequence number of a data record in a\n shard. A sequence number is the identifier associated with every record ingested in the\n stream, and is assigned when a record is put into the stream. Each stream has one or\n more shards.

\n

You must specify the shard iterator type. For example, you can set the\n ShardIteratorType parameter to read exactly from the position denoted\n by a specific sequence number by using the AT_SEQUENCE_NUMBER shard\n iterator type. Alternatively, the parameter can read right after the sequence number by\n using the AFTER_SEQUENCE_NUMBER shard iterator type, using sequence numbers\n returned by earlier calls to PutRecord, PutRecords,\n GetRecords, or DescribeStream. In the request,\n you can specify the shard iterator type AT_TIMESTAMP to read records from\n an arbitrary point in time, TRIM_HORIZON to cause\n ShardIterator to point to the last untrimmed record in the shard in the\n system (the oldest data record in the shard), or LATEST so that you always\n read the most recent data in the shard.

\n

When you read repeatedly from a stream, use a GetShardIterator\n request to get the first shard iterator for use in your first GetRecords request and for subsequent reads use the shard iterator returned by the GetRecords request in NextShardIterator. A new shard\n iterator is returned by every GetRecords request in\n NextShardIterator, which you use in the ShardIterator\n parameter of the next GetRecords request.

\n

If a GetShardIterator request is made too often, you receive a\n ProvisionedThroughputExceededException. For more information about\n throughput limits, see GetRecords, and Streams Limits in the\n Amazon Kinesis Data Streams Developer Guide.

\n

If the shard is closed, GetShardIterator returns a valid iterator\n for the last sequence number of the shard. A shard can be closed as a result of using\n SplitShard or MergeShards.

\n

\n GetShardIterator has a limit of five transactions per second per\n account per open shard.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "data" + } + } + } + }, + "com.amazonaws.kinesis#GetShardIteratorInput": { + "type": "structure", + "members": { + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", + "traits": { + "smithy.api#documentation": "

The name of the Amazon Kinesis data stream.

" + } + }, + "ShardId": { + "target": "com.amazonaws.kinesis#ShardId", + "traits": { + "smithy.api#documentation": "

The shard ID of the Kinesis Data Streams shard to get the iterator for.

", + "smithy.api#required": {} + } + }, + "ShardIteratorType": { + "target": "com.amazonaws.kinesis#ShardIteratorType", + "traits": { + "smithy.api#documentation": "

Determines how the shard iterator is used to start reading data records from the\n shard.

\n

The following are the valid Amazon Kinesis shard iterator types:

\n ", + "smithy.api#required": {} + } + }, + "StartingSequenceNumber": { + "target": "com.amazonaws.kinesis#SequenceNumber", + "traits": { + "smithy.api#documentation": "

The sequence number of the data record in the shard from which to start reading. Used\n with shard iterator type AT_SEQUENCE_NUMBER and AFTER_SEQUENCE_NUMBER.

" + } + }, + "Timestamp": { + "target": "com.amazonaws.kinesis#Timestamp", + "traits": { + "smithy.api#documentation": "

The time stamp of the data record from which to start reading. Used with shard\n iterator type AT_TIMESTAMP. A time stamp is the Unix epoch date with precision in\n milliseconds. For example, 2016-04-04T19:58:46.480-00:00 or\n 1459799926.480. If a record with this exact time stamp does not exist,\n the iterator returned is for the next (later) record. If the time stamp is older than\n the current trim horizon, the iterator returned is for the oldest untrimmed data record\n (TRIM_HORIZON).

" + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } + }, + "DryRun": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Checks if your request will succeed. DryRun is an optional\n parameter.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the input for GetShardIterator.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#GetShardIteratorOutput": { + "type": "structure", + "members": { + "ShardIterator": { + "target": "com.amazonaws.kinesis#ShardIterator", + "traits": { + "smithy.api#documentation": "

The position in the shard from which to start reading data records sequentially. A\n shard iterator specifies this position using the sequence number of a data record in a\n shard.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the output for GetShardIterator.

", + "smithy.api#output": {} + } + }, + "com.amazonaws.kinesis#HashKey": { + "type": "string", + "traits": { + "smithy.api#pattern": "^(0|([1-9]\\d{0,38}))$" + } + }, + "com.amazonaws.kinesis#HashKeyRange": { + "type": "structure", + "members": { + "StartingHashKey": { + "target": "com.amazonaws.kinesis#HashKey", + "traits": { + "smithy.api#documentation": "

The starting hash key of the hash key range.

", + "smithy.api#required": {} + } + }, + "EndingHashKey": { + "target": "com.amazonaws.kinesis#HashKey", + "traits": { + "smithy.api#documentation": "

The ending hash key of the hash key range.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The range of possible hash key values for the shard, which is a set of ordered\n contiguous positive integers.

" + } + }, + "com.amazonaws.kinesis#IncreaseStreamRetentionPeriod": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#IncreaseStreamRetentionPeriodInput" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Increases the Kinesis data stream's retention period, which is the length of time data\n records are accessible after they are added to the stream. The maximum value of a\n stream's retention period is 8760 hours (365 days).

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If you choose a longer stream retention period, this operation increases the time\n period during which records that have not yet expired are accessible. However, it does\n not make previous, expired data (older than the stream's previous retention period)\n accessible after the operation has been called. For example, if a stream's retention\n period is set to 24 hours and is increased to 168 hours, any data that is older than 24\n hours remains inaccessible to consumer applications.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#IncreaseStreamRetentionPeriodInput": { + "type": "structure", + "members": { + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", + "traits": { + "smithy.api#documentation": "

The name of the stream to modify.

" + } + }, + "RetentionPeriodHours": { + "target": "com.amazonaws.kinesis#RetentionPeriodHours", + "traits": { + "smithy.api#documentation": "

The new retention period of the stream, in hours. Must be more than the current\n retention period.

", + "smithy.api#required": {} + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the input for IncreaseStreamRetentionPeriod.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#InternalFailureException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The processing of the request failed because of an unknown error, exception, or\n failure.

", + "smithy.api#error": "server" + } + }, + "com.amazonaws.kinesis#InvalidArgumentException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A specified parameter exceeds its restrictions, is not supported, or can't be used.\n For more information, see the returned message.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSAccessDeniedException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The ciphertext references a key that doesn't exist or that you don't have access\n to.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSDisabledException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The request was rejected because the specified customer master key (CMK) isn't\n enabled.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSInvalidStateException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The request was rejected because the state of the specified resource isn't valid for\n this request. For more information, see How Key State Affects Use of a\n Customer Master Key in the Amazon Web Services Key Management\n Service Developer Guide.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSNotFoundException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The request was rejected because the specified entity or resource can't be\n found.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSOptInRequired": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The Amazon Web Services access key ID needs a subscription for the service.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KMSThrottlingException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

A message that provides information about the error.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The request was denied due to request throttling. For more information about\n throttling, see Limits in\n the Amazon Web Services Key Management Service Developer\n Guide.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#KeyId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + } + } + }, + "com.amazonaws.kinesis#Kinesis_20131202": { + "type": "service", + "version": "2013-12-02", + "operations": [ + { + "target": "com.amazonaws.kinesis#AddTagsToStream" + }, + { + "target": "com.amazonaws.kinesis#CreateChannel" + }, + { + "target": "com.amazonaws.kinesis#CreateStream" + }, + { + "target": "com.amazonaws.kinesis#DecreaseStreamRetentionPeriod" + }, + { + "target": "com.amazonaws.kinesis#DeleteChannel" + }, + { + "target": "com.amazonaws.kinesis#DeleteResourcePolicy" + }, + { + "target": "com.amazonaws.kinesis#DeleteStream" + }, + { + "target": "com.amazonaws.kinesis#DeregisterStreamConsumer" + }, + { + "target": "com.amazonaws.kinesis#DescribeAccountSettings" + }, + { + "target": "com.amazonaws.kinesis#DescribeChannel" + }, + { + "target": "com.amazonaws.kinesis#DescribeLimits" + }, + { + "target": "com.amazonaws.kinesis#DescribeStream" + }, + { + "target": "com.amazonaws.kinesis#DescribeStreamConsumer" + }, + { + "target": "com.amazonaws.kinesis#DescribeStreamSummary" + }, + { + "target": "com.amazonaws.kinesis#DisableEnhancedMonitoring" + }, + { + "target": "com.amazonaws.kinesis#EnableEnhancedMonitoring" + }, + { + "target": "com.amazonaws.kinesis#GetRecords" + }, + { + "target": "com.amazonaws.kinesis#GetResourcePolicy" + }, + { + "target": "com.amazonaws.kinesis#GetShardIterator" + }, + { + "target": "com.amazonaws.kinesis#IncreaseStreamRetentionPeriod" + }, + { + "target": "com.amazonaws.kinesis#ListChannels" + }, + { + "target": "com.amazonaws.kinesis#ListShards" + }, + { + "target": "com.amazonaws.kinesis#ListStreamConsumers" + }, + { + "target": "com.amazonaws.kinesis#ListStreams" + }, + { + "target": "com.amazonaws.kinesis#ListTagsForResource" + }, + { + "target": "com.amazonaws.kinesis#ListTagsForStream" + }, + { + "target": "com.amazonaws.kinesis#MergeShards" + }, + { + "target": "com.amazonaws.kinesis#PutRecord" + }, + { + "target": "com.amazonaws.kinesis#PutRecords" + }, + { + "target": "com.amazonaws.kinesis#PutResourcePolicy" + }, + { + "target": "com.amazonaws.kinesis#RegisterStreamConsumer" + }, + { + "target": "com.amazonaws.kinesis#RemoveTagsFromStream" + }, + { + "target": "com.amazonaws.kinesis#SplitShard" + }, + { + "target": "com.amazonaws.kinesis#StartStreamEncryption" + }, + { + "target": "com.amazonaws.kinesis#StopStreamEncryption" + }, + { + "target": "com.amazonaws.kinesis#SubscribeToShard" + }, + { + "target": "com.amazonaws.kinesis#TagResource" + }, + { + "target": "com.amazonaws.kinesis#UntagResource" + }, + { + "target": "com.amazonaws.kinesis#UpdateAccountSettings" + }, + { + "target": "com.amazonaws.kinesis#UpdateChannel" + }, + { + "target": "com.amazonaws.kinesis#UpdateMaxRecordSize" + }, + { + "target": "com.amazonaws.kinesis#UpdateShardCount" + }, + { + "target": "com.amazonaws.kinesis#UpdateStreamMode" + }, + { + "target": "com.amazonaws.kinesis#UpdateStreamWarmThroughput" + } + ], + "traits": { + "aws.api#service": { + "sdkId": "Kinesis", + "arnNamespace": "kinesis", + "cloudFormationName": "Kinesis", + "cloudTrailEventSource": "kinesis.amazonaws.com", + "endpointPrefix": "kinesis" + }, + "aws.auth#sigv4": { + "name": "kinesis" + }, + "aws.protocols#awsJson1_1": { + "http": [ + "http/1.1", "h2" ], "eventStreamHttp": [ @@ -2075,6 +3251,11 @@ "documentation": "The ARN of the Kinesis resource", "type": "string" }, + "ChannelARN": { + "required": false, + "documentation": "The ARN of the Kinesis data channel", + "type": "string" + }, "AccountId": { "builtIn": "AWS::Auth::AccountId", "required": false, @@ -2222,6 +3403,31 @@ } ] }, + { + "fn": "isSet", + "argv": [ + { + "ref": "StreamARN" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "ConsumerARN" + } + ] + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "ConsumerARN" + } + ], + "assign": "arn_ssa_2" + }, { "fn": "substring", "argv": [ @@ -2293,11 +3499,67 @@ "assign": "PlainCustomEndpointSuffixValue" }, { - "fn": "isSet", + "fn": "isValidHostLabel", "argv": [ { - "ref": "StreamARN" - } + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_2" + }, + "accountId" + ] + }, + false + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_2" + }, + "region" + ] + }, + false + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_2" + }, + "service" + ] + }, + "kinesis" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_2" + }, + "resourceId[0]" + ], + "assign": "arnType_ssa_2" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType_ssa_2" + }, + "" ] }, { @@ -2307,7 +3569,7 @@ "ref": "StreamARN" } ], - "assign": "arn_ssa_3" + "assign": "arn_ssa_4" }, { "fn": "isValidHostLabel", @@ -2316,7 +3578,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_3" + "ref": "arn_ssa_4" }, "accountId" ] @@ -2324,6 +3586,15 @@ false ] }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType_ssa_2" + }, + "stream" + ] + }, { "fn": "isValidHostLabel", "argv": [ @@ -2331,7 +3602,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_3" + "ref": "arn_ssa_4" }, "region" ] @@ -2346,40 +3617,114 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_3" + "ref": "PartitionResult" }, - "service" + "name" ] }, - "kinesis" + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_2" + }, + "partition" + ] + } ] }, { - "fn": "isSet", + "fn": "stringEquals", "argv": [ { - "ref": "ConsumerARN" - } + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_4" + }, + "service" + ] + }, + "kinesis" ] }, { "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_3" + "ref": "arn_ssa_4" }, "resourceId[0]" ], "assign": "arnType_ssa_1" }, { - "fn": "aws.parseArn", + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType_ssa_1" + }, + "" + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "ResourceARN" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "ChannelARN" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountIdEndpointMode" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountId" + } + ] + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "ChannelARN" + } + ], + "assign": "arn_ssa_1" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "AccountIdEndpointMode" + }, + "disabled" + ] + }, + { + "fn": "isValidHostLabel", "argv": [ { - "ref": "ConsumerARN" - } - ], - "assign": "arn_ssa_1" + "ref": "AccountId" + }, + false + ] }, { "fn": "isValidHostLabel", @@ -2434,13 +3779,13 @@ }, "resourceId[0]" ], - "assign": "arnType_ssa_2" + "assign": "arnType_ssa_4" }, { "fn": "stringEquals", "argv": [ { - "ref": "arnType_ssa_2" + "ref": "arnType_ssa_4" }, "" ] @@ -2449,9 +3794,9 @@ "fn": "stringEquals", "argv": [ { - "ref": "arnType_ssa_2" + "ref": "arnType_ssa_4" }, - "stream" + "channel" ] }, { @@ -2477,14 +3822,6 @@ } ] }, - { - "fn": "isSet", - "argv": [ - { - "ref": "ResourceARN" - } - ] - }, { "fn": "aws.parseArn", "argv": [ @@ -2492,15 +3829,7 @@ "ref": "ResourceARN" } ], - "assign": "arn_ssa_2" - }, - { - "fn": "isSet", - "argv": [ - { - "ref": "AccountIdEndpointMode" - } - ] + "assign": "arn_ssa_3" }, { "fn": "isValidHostLabel", @@ -2509,7 +3838,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_2" + "ref": "arn_ssa_3" }, "accountId" ] @@ -2524,7 +3853,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_2" + "ref": "arn_ssa_3" }, "region" ] @@ -2539,7 +3868,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_2" + "ref": "arn_ssa_3" }, "service" ] @@ -2551,7 +3880,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_2" + "ref": "arn_ssa_3" }, "resourceId[0]" ], @@ -2575,6 +3904,15 @@ "stream" ] }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType_ssa_3" + }, + "channel" + ] + }, { "fn": "stringEquals", "argv": [ @@ -2591,7 +3929,7 @@ "fn": "getAttr", "argv": [ { - "ref": "arn_ssa_2" + "ref": "arn_ssa_3" }, "partition" ] @@ -2599,20 +3937,35 @@ ] }, { - "fn": "isSet", + "fn": "stringEquals", "argv": [ { - "ref": "AccountId" - } + "ref": "arnType_ssa_1" + }, + "stream" ] }, { "fn": "stringEquals", "argv": [ { - "ref": "AccountIdEndpointMode" + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] }, - "disabled" + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_4" + }, + "partition" + ] + } ] }, { @@ -2624,69 +3977,98 @@ "required" ] }, + { + "fn": "isSet", + "argv": [ + { + "ref": "OperationType" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "arn_ssa_3" + }, + "resourceId[1]" + ], + "assign": "resourceId_ssa_1" + }, { "fn": "isValidHostLabel", "argv": [ { - "ref": "AccountId" + "ref": "resourceId_ssa_1" }, false ] }, { - "fn": "stringEquals", + "fn": "getAttr", "argv": [ { - "ref": "arnType_ssa_1" + "ref": "arn_ssa_1" }, - "" + "resourceId[1]" + ], + "assign": "resourceId_ssa_2" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "resourceId_ssa_2" + }, + false ] }, { "fn": "stringEquals", "argv": [ { - "ref": "arnType_ssa_1" + "ref": "OperationType" }, - "stream" + "data" ] }, { "fn": "stringEquals", "argv": [ { - "fn": "getAttr", + "fn": "coalesce", "argv": [ { - "ref": "PartitionResult" + "fn": "substring", + "argv": [ + { + "ref": "Endpoint" + }, + 7, + 8, + false + ] }, - "name" + "" ] }, - { - "fn": "getAttr", - "argv": [ - { - "ref": "arn_ssa_3" - }, - "partition" - ] - } + "-" ] }, { - "fn": "isSet", + "fn": "booleanEquals", "argv": [ { - "ref": "OperationType" - } + "ref": "UseFIPS" + }, + true ] }, { "fn": "booleanEquals", "argv": [ { - "ref": "UseFIPS" + "ref": "UseDualStack" }, true ] @@ -2706,15 +4088,6 @@ true ] }, - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseDualStack" - }, - true - ] - }, { "fn": "booleanEquals", "argv": [ @@ -2767,29 +4140,6 @@ }, "." ] - }, - { - "fn": "stringEquals", - "argv": [ - { - "fn": "coalesce", - "argv": [ - { - "fn": "substring", - "argv": [ - { - "ref": "Endpoint" - }, - 7, - 8, - false - ] - }, - "" - ] - }, - "-" - ] } ], "results": [ @@ -2929,7 +4279,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{arn_ssa_4#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -2938,7 +4288,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{arn_ssa_4#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -2947,7 +4297,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{arn_ssa_4#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -2956,7 +4306,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{arn_ssa_4#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -2964,7 +4314,7 @@ }, { "conditions": [], - "error": "Partition: {arn_ssa_3#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "error": "Partition: {arn_ssa_4#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", "type": "error" }, { @@ -2979,28 +4329,120 @@ }, { "conditions": [], - "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_3#service}.", + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_4#service}.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Invalid region.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Invalid account id.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Failed to parse ARN.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Partition: {arn_ssa_2#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Kinesis ARNs don't support `{arnType_ssa_2}` arn types.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_2#service}.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Unsupported resource type `{arnType_ssa_3}`. Expected: stream or channel", "type": "error" }, { "conditions": [], - "error": "Invalid ARN: Invalid region.", - "type": "error" + "endpoint": { + "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" }, { "conditions": [], - "error": "Invalid ARN: Invalid account id.", - "type": "error" + "endpoint": { + "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" }, { "conditions": [], - "error": "Invalid ARN: Failed to parse ARN.", - "type": "error" + "endpoint": { + "url": "https://{arn_ssa_3#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" }, { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_1#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{resourceId_ssa_1}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3009,7 +4451,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_1#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{resourceId_ssa_1}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -3018,7 +4460,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_1#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{resourceId_ssa_1}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3027,7 +4469,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_1#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{resourceId_ssa_1}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -3035,23 +4477,28 @@ }, { "conditions": [], - "error": "Partition: {arn_ssa_1#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "error": "Invalid ARN: Invalid channel id.", "type": "error" }, { "conditions": [], - "error": "Invalid ARN: Kinesis ARNs don't support `{arnType_ssa_2}` arn types.", + "error": "Invalid ARN: Missing channel id.", "type": "error" }, { "conditions": [], - "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_1#service}.", + "error": "Partition: {arn_ssa_3#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_3#service}.", "type": "error" }, { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{resourceId_ssa_2}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3060,7 +4507,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{resourceId_ssa_2}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -3069,7 +4516,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{resourceId_ssa_2}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3078,7 +4525,7 @@ { "conditions": [], "endpoint": { - "url": "https://{arn_ssa_2#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{resourceId_ssa_2}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -3086,17 +4533,22 @@ }, { "conditions": [], - "error": "Partition: {arn_ssa_2#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "error": "ChannelARN does not support the `data` operation type.", "type": "error" }, { "conditions": [], - "error": "Invalid ARN: Kinesis ARNs don't support `{arnType_ssa_3}` arn types.", + "error": "Partition: {arn_ssa_1#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", "type": "error" }, { "conditions": [], - "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_2#service}.", + "error": "Invalid ARN: ChannelARN only supports `channel` arn types, found: `{arnType_ssa_4}`.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn_ssa_1#service}.", "type": "error" }, { @@ -3244,8 +4696,8 @@ } ], "root": 2, - "nodeCount": 120, - "nodes": "/////wAAAAH/////AAAAAAAAAAMAAAAEAAAAAQAAAAUAAAAEAAAACQAAAHcF9eE7AAAAAgAAAGwAAAAGAAAAAwAAAGwAAAAHAAAABAAAAAgAAAAMAAAABQAAAAkAAAAMAAAABgAAAAoAAAAMAAAABwAAAAsAAAAMAAAACAAAAEkAAAAMAAAACQAAAHcAAAANAAAADgAAADoAAAAOAAAAEwAAACsAAAAPAAAAHQAAABwAAAAQAAAAHwAAABEAAABvAAAAJwAAABIAAAAbAAAAKAAAABsAAAATAAAAKgAAABQF9eEvAAAALgAAABUF9eERAAAALwAAABgAAAAWAAAAMQAAABcF9eEuAAAAMgX14S0F9eEHAAAAMAAAABkAAABpAAAAMQAAABoF9eEsAAAAMgX14SsF9eECAAAAKQX14TAAAABvAAAAHgAAAB0F9eEcAAAAIAAAAB4F9eEbAAAAIQAAAB8F9eEaAAAAIgAAACAF9eEqAAAAIwAAACEF9eEYAAAAJAX14RgAAAAiAAAAJQAAACMF9eEpAAAAJgAAACQF9eEoAAAALgAAACUF9eERAAAALwAAACgAAAAmAAAAMQAAACcF9eEnAAAAMgX14SYF9eEHAAAAMAAAACkAAABpAAAAMQAAACoF9eElAAAAMgX14SQF9eECAAAAFQAAACwF9eEcAAAAFgAAAC0F9eEbAAAAFwAAAC4F9eEaAAAAGAAAAC8F9eEjAAAAGQAAADAF9eEYAAAAGgX14RgAAAAxAAAAGwAAADIF9eEiAAAAHAAAADMF9eEhAAAALgAAADQF9eERAAAALwAAADcAAAA1AAAAMQAAADYF9eEgAAAAMgX14R8F9eEHAAAAMAAAADgAAABpAAAAMQAAADkF9eEeAAAAMgX14R0F9eECAAAADwAAADsF9eEcAAAAEAAAADwF9eEbAAAAEQAAAD0F9eEaAAAAEgAAAD4F9eEZAAAAFAAAAD8F9eEYAAAAKwX14RgAAABAAAAALAAAAEEF9eEXAAAALQAAAEIF9eEWAAAALgAAAEMF9eERAAAALwAAAEYAAABEAAAAMQAAAEUF9eEVAAAAMgX14RQF9eEHAAAAMAAAAEcAAABpAAAAMQAAAEgF9eETAAAAMgX14RIF9eECAAAACQAAAEoAAABOAAAACgAAAEsAAABNAAAACwAAAEwAAABNAAAADAAAAGQAAABNAAAADQAAAFUAAABOAAAALgAAAE8F9eERAAAALwAAAFIAAABQAAAAMQAAAFEF9eEQAAAAMgX14Q8F9eEHAAAAMAAAAFMAAABpAAAAMQAAAFQF9eEOAAAAMgX14Q0F9eECAAAALgAAAFYF9eERAAAALwAAAF0AAABXAAAAMQAAAFoAAABYAAAANAAAAFkF9eEQAAAANQX14QwF9eEQAAAAMgAAAFsF9eEHAAAANAAAAFwF9eEPAAAANQX14QsF9eEPAAAAMAAAAF4AAABpAAAAMQAAAGEAAABfAAAANAAAAGAF9eEOAAAANQX14QoF9eEOAAAAMgAAAGIF9eECAAAANAAAAGMF9eENAAAANQX14QkF9eENAAAALgAAAGUF9eERAAAALwAAAGgAAABmAAAAMQAAAGcF9eEIAAAAMgX14QYF9eEHAAAAMAAAAGoAAABpAAAAMQX14QMF9eEFAAAAMQAAAGsF9eEEAAAAMgX14QEF9eECAAAACQAAAHcAAABtAAAAHwAAAG4AAABvAAAAKQX14TEAAABvAAAALwAAAHIAAABwAAAAMQAAAHEF9eE6AAAAMgX14TkF9eEHAAAAMAAAAHQAAABzAAAAMQX14TYF9eEFAAAAMQAAAHYAAAB1AAAAMwX14TcF9eE4AAAAMgX14TUF9eE2AAAALwX14TIAAAB4AAAAMQX14TMF9eE0" + "nodeCount": 155, + "nodes": "/////wAAAAH/////AAAAAAAAAAMAAAAEAAAAAQAAAAUAAAAEAAAACQAAAJoF9eFJAAAAAgAAAI8AAAAGAAAAAwAAAI8AAAAHAAAABAAAAAgAAAAMAAAABQAAAAkAAAAMAAAABgAAAAoAAAAMAAAABwAAAAsAAAAMAAAACAAAAG0AAAAMAAAACQAAAJoAAAANAAAACgAAAF0AAAAOAAAACwAAAE0AAAAPAAAAHgAAADEAAAAQAAAAHwAAAB4AAAARAAAAIAAAABIAAACSAAAAIQAAABMAAAAdAAAAIwAAAB0AAAAUAAAAJAAAABUF9eE9AAAAOAAAABYF9eERAAAAPwAAABkAAAAXAAAAQAAAABgF9eE8AAAAQgX14TsF9eEHAAAAQAAAABsAAAAaAAAAQQX14ToF9eEFAAAAQQAAABwF9eEDAAAAQgX14TkF9eECAAAANwX14T4AAACSAAAAIgAAAB8F9eEcAAAAJQAAACAF9eEbAAAAJgAAACEF9eEaAAAAJwAAACIF9eE4AAAAKAAAACMF9eEYAAAAKQX14RgAAAAkAAAAKgAAACUF9eE3AAAAKwAAACYF9eE2AAAAOAAAACcF9eERAAAAOwAAACgF9eEuAAAAPAAAACkF9eEtAAAAPQX14TUAAAAqAAAAPwAAAC0AAAArAAAAQAAAACwF9eE0AAAAQgX14TMF9eEHAAAAQAAAAC8AAAAuAAAAQQX14TIF9eEFAAAAQQAAADAF9eEDAAAAQgX14TEF9eECAAAALAAAADIF9eEcAAAALQAAADMF9eEbAAAALgAAADQF9eEaAAAALwAAADUF9eEwAAAAMAAAADYF9eEYAAAAMQX14RgAAAA3AAAAMgAAAEQAAAA4AAAAMwAAADkF9eEkAAAANAAAADoF9eEvAAAAOAAAADsF9eERAAAAOQAAADwF9eEuAAAAOgAAAD0F9eEtAAAAPwAAAEAAAAA+AAAAQAAAAD8F9eEsAAAAQgX14SsF9eEHAAAAQAAAAEIAAABBAAAAQQX14SoF9eEFAAAAQQAAAEMF9eEDAAAAQgX14SkF9eECAAAANAAAAEUF9eEvAAAAOAAAAEYF9eERAAAAPwAAAEkAAABHAAAAQAAAAEgF9eEoAAAAQgX14ScF9eEHAAAAQAAAAEsAAABKAAAAQQX14SYF9eEFAAAAQQAAAEwF9eEDAAAAQgX14SUF9eECAAAADAAAAE4F9eEcAAAAEQAAAE8F9eEbAAAAEgAAAFAF9eEaAAAAEwAAAFEF9eEjAAAAFAAAAFIF9eEYAAAAFQX14RgAAABTAAAAGAAAAFQF9eEiAAAAGgAAAFUF9eEhAAAAOAAAAFYF9eERAAAAPwAAAFkAAABXAAAAQAAAAFgF9eEgAAAAQgX14R8F9eEHAAAAQAAAAFsAAABaAAAAQQX14R4F9eEFAAAAQQAAAFwF9eEDAAAAQgX14R0F9eECAAAAFgAAAF4F9eEcAAAAFwAAAF8F9eEbAAAAGQAAAGAF9eEaAAAAGwAAAGEF9eEZAAAAHAAAAGIF9eEYAAAAHQX14RgAAABjAAAANQAAAGQF9eEXAAAANgAAAGUF9eEWAAAAOAAAAGYF9eERAAAAPwAAAGkAAABnAAAAQAAAAGgF9eEVAAAAQgX14RQF9eEHAAAAQAAAAGsAAABqAAAAQQX14RMF9eEFAAAAQQAAAGwF9eEDAAAAQgX14RIF9eECAAAACQAAAG4AAAByAAAADQAAAG8AAABxAAAADgAAAHAAAABxAAAADwAAAIcAAABxAAAAEAAAAHMAAAByAAAAOAAAAHUF9eERAAAAOAAAAHQF9eERAAAAPgAAAHwAAAB1AAAAPwAAAHgAAAB2AAAAQAAAAHcF9eEQAAAAQgX14Q8F9eEHAAAAQAAAAHoAAAB5AAAAQQX14Q4F9eEFAAAAQQAAAHsF9eEDAAAAQgX14Q0F9eECAAAAPwAAAIEAAAB9AAAAQAAAAH8AAAB+AAAARAX14QwF9eEQAAAAQgAAAIAF9eEHAAAARAX14QsF9eEPAAAAQAAAAIQAAACCAAAAQQAAAIMF9eEFAAAARAX14QoF9eEOAAAAQQAAAIUF9eEDAAAAQgAAAIYF9eECAAAARAX14QkF9eENAAAAOAAAAIgF9eERAAAAPwAAAIsAAACJAAAAQAAAAIoF9eEIAAAAQgX14QYF9eEHAAAAQAAAAI0AAACMAAAAQQX14QQF9eEFAAAAQQAAAI4F9eEDAAAAQgX14QEF9eECAAAACQAAAJoAAACQAAAAIAAAAJEAAACSAAAANwX14T8AAACSAAAAPwAAAJUAAACTAAAAQAAAAJQF9eFIAAAAQgX14UcF9eEHAAAAQAAAAJgAAACWAAAAQQAAAJcF9eEFAAAAQwX14UUF9eFGAAAAQQAAAJkF9eFEAAAAQgX14UMF9eFEAAAAPwX14UAAAACbAAAAQAX14UEF9eFC" }, "smithy.rules#endpointRuleSet": { "version": "1.0", @@ -3301,6 +4753,11 @@ "documentation": "The ARN of the Kinesis resource", "type": "string" }, + "ChannelARN": { + "required": false, + "documentation": "The ARN of the Kinesis data channel", + "type": "string" + }, "AccountId": { "builtIn": "AWS::Auth::AccountId", "required": false, @@ -3418,50 +4875,320 @@ "fn": "getAttr", "argv": [ { - "ref": "PartitionResult" + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-iso" + ] + } + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-iso-b" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "OperationType" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Endpoint" + }, + 15, + 16, + false + ], + "assign": "HttpsCustomEndpointDelimiterValue" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "HttpsCustomEndpointDelimiterValue" + }, + "-" + ] + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Endpoint" + }, + 20, + 21, + false + ], + "assign": "HttpsEndpointDelimiterValue" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "HttpsEndpointDelimiterValue" + }, + "." + ] + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Endpoint" + }, + 15, + 20, + false + ], + "assign": "HttpsCustomEndpointSuffixValue" + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled, but this partition does not support DualStack.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled, but this partition does not support FIPS.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, - "name" - ] + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" }, - "aws-iso" - ] - } - ] - }, - { - "fn": "not", - "argv": [ - { - "fn": "stringEquals", - "argv": [ { - "fn": "getAttr", - "argv": [ + "conditions": [ { - "ref": "PartitionResult" + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, - "name" - ] + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" }, - "aws-iso-b" - ] - } - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "isSet", - "argv": [ { - "ref": "OperationType" + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" } - ] - } - ], - "rules": [ + ], + "type": "tree" + }, { "conditions": [ { @@ -3478,17 +5205,17 @@ { "ref": "Endpoint" }, - 15, - 16, + 7, + 8, false ], - "assign": "HttpsCustomEndpointDelimiterValue" + "assign": "PlainCustomEndpointDelimiterValue" }, { "fn": "stringEquals", "argv": [ { - "ref": "HttpsCustomEndpointDelimiterValue" + "ref": "PlainCustomEndpointDelimiterValue" }, "-" ] @@ -3499,17 +5226,17 @@ { "ref": "Endpoint" }, - 20, - 21, + 12, + 13, false ], - "assign": "HttpsEndpointDelimiterValue" + "assign": "PlainEndpointDelimiterValue" }, { "fn": "stringEquals", "argv": [ { - "ref": "HttpsEndpointDelimiterValue" + "ref": "PlainEndpointDelimiterValue" }, "." ] @@ -3520,11 +5247,11 @@ { "ref": "Endpoint" }, - 15, - 20, + 7, + 12, false ], - "assign": "HttpsCustomEndpointSuffixValue" + "assign": "PlainCustomEndpointSuffixValue" } ], "rules": [ @@ -3591,7 +5318,7 @@ { "conditions": [], "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3651,7 +5378,7 @@ { "conditions": [], "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dnsSuffix}", + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -3703,7 +5430,99 @@ { "conditions": [], "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", "properties": {}, "headers": {} }, @@ -3714,7 +5533,7 @@ }, { "conditions": [], - "error": "DualStack is enabled but this partition does not support DualStack", + "error": "DualStack is enabled, but this partition does not support DualStack.", "type": "error" } ], @@ -3722,12 +5541,8 @@ }, { "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{HttpsCustomEndpointSuffixValue}.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "FIPS is enabled, but this partition does not support FIPS.", + "type": "error" } ], "type": "tree" @@ -3735,87 +5550,256 @@ { "conditions": [ { - "fn": "isSet", + "fn": "booleanEquals", "argv": [ { - "ref": "Endpoint" - } + "ref": "UseFIPS" + }, + true ] - }, + } + ], + "rules": [ { - "fn": "substring", - "argv": [ + "conditions": [ { - "ref": "Endpoint" - }, - 7, - 8, - false + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } ], - "assign": "PlainCustomEndpointDelimiterValue" + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, { - "fn": "stringEquals", + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", "argv": [ { - "ref": "PlainCustomEndpointDelimiterValue" + "ref": "UseDualStack" }, - "-" + true ] - }, + } + ], + "rules": [ { - "fn": "substring", - "argv": [ + "conditions": [ { - "ref": "Endpoint" - }, - 12, - 13, - false + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } ], - "assign": "PlainEndpointDelimiterValue" + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, { - "fn": "stringEquals", + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "Operation Type is not set. Please contact service team for resolution.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "StreamARN" + } + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + }, + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", "argv": [ { - "ref": "PlainEndpointDelimiterValue" + "ref": "PartitionResult" }, - "." + "name" ] }, + "aws-iso" + ] + } + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", + "argv": [ { - "fn": "substring", + "fn": "getAttr", "argv": [ { - "ref": "Endpoint" + "ref": "PartitionResult" }, - 7, - 12, - false - ], - "assign": "PlainCustomEndpointSuffixValue" + "name" + ] + }, + "aws-iso-b" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "StreamARN" } ], - "rules": [ + "assign": "arn" + } + ], + "rules": [ + { + "conditions": [ { - "conditions": [ + "fn": "isValidHostLabel", + "argv": [ { - "fn": "booleanEquals", + "fn": "getAttr", "argv": [ { - "ref": "UseFIPS" + "ref": "arn" }, - true + "accountId" ] }, + false + ] + } + ], + "rules": [ + { + "conditions": [ { - "fn": "booleanEquals", + "fn": "isValidHostLabel", "argv": [ { - "ref": "UseDualStack" + "fn": "getAttr", + "argv": [ + { + "ref": "arn" + }, + "region" + ] }, - true + false ] } ], @@ -3823,18 +5807,18 @@ { "conditions": [ { - "fn": "booleanEquals", + "fn": "stringEquals", "argv": [ { "fn": "getAttr", "argv": [ { - "ref": "PartitionResult" + "ref": "arn" }, - "supportsFIPS" + "service" ] }, - true + "kinesis" ] } ], @@ -3842,241 +5826,314 @@ { "conditions": [ { - "fn": "booleanEquals", + "fn": "getAttr", "argv": [ { - "fn": "getAttr", + "ref": "arn" + }, + "resourceId[0]" + ], + "assign": "arnType" + }, + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", "argv": [ { - "ref": "PartitionResult" + "ref": "arnType" + }, + "" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "stream" + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "{arn#partition}" + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "OperationType" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled, but this partition does not support DualStack.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled, but this partition does not support FIPS.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, - "supportsDualStack" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "DualStack is enabled, but this partition does not support DualStack.", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "FIPS is enabled, but this partition does not support FIPS.", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseFIPS" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" + { + "conditions": [], + "error": "Operation Type is not set. Please contact service team for resolution.", + "type": "error" + } + ], + "type": "tree" }, - "supportsFIPS" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}-fips.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "FIPS is enabled but this partition does not support FIPS", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseDualStack" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ { - "ref": "PartitionResult" - }, - "supportsDualStack" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "DualStack is enabled but this partition does not support DualStack", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis{PlainCustomEndpointSuffixValue}.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseFIPS" - }, - true - ] - }, - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseDualStack" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" + "conditions": [], + "error": "Partition: {arn#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "type": "error" + } + ], + "type": "tree" }, - "supportsFIPS" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "supportsDualStack" - ] - }, - true - ] - } - ], - "rules": [ + "conditions": [], + "error": "Invalid ARN: Kinesis ARNs don't support `{arnType}` arn types.", + "type": "error" + } + ], + "type": "tree" + }, { "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "Invalid ARN: No ARN type specified", + "type": "error" } ], "type": "tree" }, { "conditions": [], - "error": "DualStack is enabled, but this partition does not support DualStack.", + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: {arn#service}.", "type": "error" } ], @@ -4084,111 +6141,7 @@ }, { "conditions": [], - "error": "FIPS is enabled, but this partition does not support FIPS.", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseFIPS" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "supportsFIPS" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "FIPS is enabled but this partition does not support FIPS", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseDualStack" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "supportsDualStack" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "DualStack is enabled but this partition does not support DualStack", + "error": "Invalid ARN: Invalid region.", "type": "error" } ], @@ -4196,19 +6149,15 @@ }, { "conditions": [], - "endpoint": { - "url": "https://{StreamIdPrefixValue}.{StreamIdSuffixValue}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "Invalid ARN: Invalid account id.", + "type": "error" } ], "type": "tree" }, { "conditions": [], - "error": "Operation Type is not set. Please contact service team for resolution.", + "error": "Invalid ARN: Failed to parse ARN.", "type": "error" } ], @@ -4220,7 +6169,7 @@ "fn": "isSet", "argv": [ { - "ref": "StreamARN" + "ref": "ConsumerARN" } ] }, @@ -4302,7 +6251,7 @@ "fn": "aws.parseArn", "argv": [ { - "ref": "StreamARN" + "ref": "ConsumerARN" } ], "assign": "arn" @@ -4712,7 +6661,7 @@ "fn": "isSet", "argv": [ { - "ref": "ConsumerARN" + "ref": "ResourceARN" } ] }, @@ -4794,7 +6743,7 @@ "fn": "aws.parseArn", "argv": [ { - "ref": "ConsumerARN" + "ref": "ResourceARN" } ], "assign": "arn" @@ -4886,15 +6835,55 @@ } ], "rules": [ + { + "conditions": [ + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "stream" + ] + } + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "channel" + ] + } + ] + } + ], + "error": "Invalid ARN: Unsupported resource type `{arnType}`. Expected: stream or channel", + "type": "error" + }, { "conditions": [ { "fn": "stringEquals", "argv": [ { - "ref": "arnType" + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] }, - "stream" + "{arn#partition}" ] } ], @@ -4902,18 +6891,11 @@ { "conditions": [ { - "fn": "stringEquals", + "fn": "isSet", "argv": [ { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "name" - ] - }, - "{arn#partition}" + "ref": "OperationType" + } ] } ], @@ -4921,11 +6903,12 @@ { "conditions": [ { - "fn": "isSet", + "fn": "stringEquals", "argv": [ { - "ref": "OperationType" - } + "ref": "arnType" + }, + "stream" ] } ], @@ -5073,39 +7056,303 @@ { "conditions": [ { - "fn": "booleanEquals", + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "channel" + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "getAttr", "argv": [ { - "ref": "UseDualStack" + "ref": "arn" }, - true - ] + "resourceId[1]" + ], + "assign": "resourceId" } ], "rules": [ { "conditions": [ { - "fn": "booleanEquals", + "fn": "isValidHostLabel", "argv": [ { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "supportsDualStack" - ] + "ref": "resourceId" }, - true + false ] } ], "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{resourceId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled, but this partition does not support DualStack.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled, but this partition does not support FIPS.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{resourceId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{resourceId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, { "conditions": [], "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "url": "https://{resourceId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", "properties": {}, "headers": {} }, @@ -5116,7 +7363,7 @@ }, { "conditions": [], - "error": "DualStack is enabled but this partition does not support DualStack", + "error": "Invalid ARN: Invalid channel id.", "type": "error" } ], @@ -5124,27 +7371,18 @@ }, { "conditions": [], - "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "Invalid ARN: Missing channel id.", + "type": "error" } ], "type": "tree" - }, - { - "conditions": [], - "error": "Operation Type is not set. Please contact service team for resolution.", - "type": "error" } ], "type": "tree" }, { "conditions": [], - "error": "Partition: {arn#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", + "error": "Operation Type is not set. Please contact service team for resolution.", "type": "error" } ], @@ -5152,7 +7390,7 @@ }, { "conditions": [], - "error": "Invalid ARN: Kinesis ARNs don't support `{arnType}` arn types.", + "error": "Partition: {arn#partition} from ARN doesn't match with partition name: {PartitionResult#name}.", "type": "error" } ], @@ -5204,7 +7442,7 @@ "fn": "isSet", "argv": [ { - "ref": "ResourceARN" + "ref": "ChannelARN" } ] }, @@ -5286,7 +7524,7 @@ "fn": "aws.parseArn", "argv": [ { - "ref": "ResourceARN" + "ref": "ChannelARN" } ], "assign": "arn" @@ -5386,7 +7624,7 @@ { "ref": "arnType" }, - "stream" + "channel" ] } ], @@ -5425,40 +7663,26 @@ { "conditions": [ { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseFIPS" - }, - true - ] - }, - { - "fn": "booleanEquals", + "fn": "getAttr", "argv": [ { - "ref": "UseDualStack" + "ref": "arn" }, - true - ] + "resourceId[1]" + ], + "assign": "channelId" } ], "rules": [ { "conditions": [ { - "fn": "booleanEquals", + "fn": "isValidHostLabel", "argv": [ { - "fn": "getAttr", - "argv": [ - { - "ref": "PartitionResult" - }, - "supportsFIPS" - ] + "ref": "channelId" }, - true + false ] } ], @@ -5466,149 +7690,236 @@ { "conditions": [ { - "fn": "booleanEquals", + "fn": "not", "argv": [ { - "fn": "getAttr", + "fn": "stringEquals", "argv": [ { - "ref": "PartitionResult" + "ref": "OperationType" }, - "supportsDualStack" + "data" ] - }, - true + } ] } ], "rules": [ { - "conditions": [], - "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "DualStack is enabled, but this partition does not support DualStack.", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "FIPS is enabled, but this partition does not support FIPS.", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseFIPS" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "fn": "getAttr", - "argv": [ + "conditions": [ { - "ref": "PartitionResult" + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] }, - "supportsFIPS" - ] - }, - true - ] - } - ], - "rules": [ - { - "conditions": [], - "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" - } - ], - "type": "tree" - }, - { - "conditions": [], - "error": "FIPS is enabled but this partition does not support FIPS", - "type": "error" - } - ], - "type": "tree" - }, - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ - { - "ref": "UseDualStack" - }, - true - ] - } - ], - "rules": [ - { - "conditions": [ - { - "fn": "booleanEquals", - "argv": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{channelId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled, but this partition does not support DualStack.", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled, but this partition does not support FIPS.", + "type": "error" + } + ], + "type": "tree" + }, { - "fn": "getAttr", - "argv": [ + "conditions": [ { - "ref": "PartitionResult" + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{channelId}.{OperationType}-kinesis-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" }, - "supportsDualStack" - ] + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" }, - true - ] - } - ], - "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://{channelId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{channelId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, { "conditions": [], - "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dualStackDnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "ChannelARN does not support the `data` operation type.", + "type": "error" } ], "type": "tree" }, { "conditions": [], - "error": "DualStack is enabled but this partition does not support DualStack", + "error": "Invalid ARN: Invalid channel id.", "type": "error" } ], @@ -5616,12 +7927,8 @@ }, { "conditions": [], - "endpoint": { - "url": "https://{arn#accountId}.{OperationType}-kinesis.{Region}.{PartitionResult#dnsSuffix}", - "properties": {}, - "headers": {} - }, - "type": "endpoint" + "error": "Invalid ARN: Missing channel id.", + "type": "error" } ], "type": "tree" @@ -5644,7 +7951,7 @@ }, { "conditions": [], - "error": "Invalid ARN: Kinesis ARNs don't support `{arnType}` arn types.", + "error": "Invalid ARN: ChannelARN only supports `channel` arn types, found: `{arnType}`.", "type": "error" } ], @@ -7963,7 +10270,7 @@ { "documentation": "ResourceARN as StreamARN test: Invalid ARN: Kinesis ARNs only support stream arn types", "expect": { - "error": "Invalid ARN: Kinesis ARNs don't support `accesspoint` arn types." + "error": "Invalid ARN: Unsupported resource type `accesspoint`. Expected: stream or channel" }, "params": { "Region": "us-east-1", @@ -8314,7 +10621,7 @@ { "documentation": "ResourceARN as ConsumerARN test: Invalid ARN: Kinesis ARNs only support stream arn/consumer arn types", "expect": { - "error": "Invalid ARN: Kinesis ARNs don't support `accesspoint` arn types." + "error": "Invalid ARN: Unsupported resource type `accesspoint`. Expected: stream or channel" }, "params": { "Region": "us-east-1", @@ -8547,33 +10854,261 @@ } }, { - "documentation": "ResourceARN as ConsumerARN test: Account endpoint with fips targeting control operation type in ADC regions", + "documentation": "ResourceARN as ConsumerARN test: Account endpoint with fips targeting control operation type in ADC regions", + "expect": { + "endpoint": { + "url": "https://kinesis-fips.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": true, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws-iso:kinesis:us-iso-east-1:123:stream/test-stream/consumer/test-consumer:1525898737" + } + }, + { + "documentation": "ResourceARN as ConsumerARN test: Account endpoint with fips targeting data operation type in ADC regions", + "expect": { + "endpoint": { + "url": "https://kinesis-fips.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": true, + "UseDualStack": false, + "OperationType": "data", + "ResourceARN": "arn:aws-iso-b:kinesis:us-isob-east-1:123:stream/test-stream/consumer/test-consumer:1525898737" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Invalid ARN: unsupported resource type", + "expect": { + "error": "Invalid ARN: Unsupported resource type `accesspoint`. Expected: stream or channel" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:accesspoint/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Invalid ARN: Not Kinesis", + "expect": { + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: s3." + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:s3:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Invalid ARN: partitions mismatch", + "expect": { + "error": "Partition: aws from ARN doesn't match with partition name: aws-us-gov." + }, + "params": { + "Region": "us-gov-west-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-west-2:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: OperationType not set", + "expect": { + "error": "Operation Type is not set. Please contact service team for resolution." + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Missing channel id", + "expect": { + "error": "Invalid ARN: Missing channel id." + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Invalid channel id (subdomains not allowed)", + "expect": { + "error": "Invalid ARN: Invalid channel id." + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8.ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Custom Endpoint is specified", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe", + "Endpoint": "https://example.com" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: endpoint targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: endpoint with fips targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: endpoint with Dual Stack enabled", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: endpoint with Dual Stack and FIPS enabled", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: RegionMismatch: client region should be used for endpoint region", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws:kinesis:us-west-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Account endpoint with FIPS enabled for cn regions", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.cn-northwest-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-northwest-1", + "UseFIPS": true, + "UseDualStack": false, + "OperationType": "control", + "ResourceARN": "arn:aws-cn:kinesis:cn-northwest-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Account endpoint with FIPS and DualStack enabled for cn regions", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.cn-northwest-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-northwest-1", + "UseFIPS": true, + "UseDualStack": true, + "OperationType": "control", + "ResourceARN": "arn:aws-cn:kinesis:cn-northwest-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ResourceARN as ChannelARN test: Account endpoint targeting control operation type in ADC regions", "expect": { "endpoint": { - "url": "https://kinesis-fips.us-iso-east-1.c2s.ic.gov" + "url": "https://kinesis.us-iso-east-1.c2s.ic.gov" } }, "params": { "Region": "us-iso-east-1", - "UseFIPS": true, + "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "ResourceARN": "arn:aws-iso:kinesis:us-iso-east-1:123:stream/test-stream/consumer/test-consumer:1525898737" + "ResourceARN": "arn:aws-iso:kinesis:us-iso-east-1:298091445058:channel/apu0zt8ge6utbndxe" } }, { - "documentation": "ResourceARN as ConsumerARN test: Account endpoint with fips targeting data operation type in ADC regions", + "documentation": "ResourceARN as ChannelARN test: Account endpoint with fips targeting control operation type in ADC regions", "expect": { "endpoint": { - "url": "https://kinesis-fips.us-isob-east-1.sc2s.sgov.gov" + "url": "https://kinesis-fips.us-iso-east-1.c2s.ic.gov" } }, "params": { - "Region": "us-isob-east-1", + "Region": "us-iso-east-1", "UseFIPS": true, "UseDualStack": false, - "OperationType": "data", - "ResourceARN": "arn:aws-iso-b:kinesis:us-isob-east-1:123:stream/test-stream/consumer/test-consumer:1525898737" + "OperationType": "control", + "ResourceARN": "arn:aws-iso:kinesis:us-iso-east-1:298091445058:channel/apu0zt8ge6utbndxe" } }, { @@ -9013,147 +11548,384 @@ } }, { - "documentation": "StreamId test: Stream endpoint with ResourceARN targeting data operation type", + "documentation": "StreamId test: Stream endpoint with ResourceARN targeting data operation type", + "expect": { + "endpoint": { + "url": "https://af4lwng4k01746835071.xyz.data-kinesis.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "data", + "StreamId": "af4lwng4k01746835071-xyz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid StreamId with ARN", + "expect": { + "endpoint": { + "url": "https://123.data-kinesis.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "data", + "StreamId": "af4lwng4k01746835071=xyz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid streamId with custom endpoint", + "expect": { + "endpoint": { + "url": "https://kinesis-pod2.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071=xyz", + "Endpoint": "https://kinesis-pod2.us-west-2.amazonaws.com" + } + }, + { + "documentation": "StreamId test: Invalid streamId", + "expect": { + "endpoint": { + "url": "https://kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071=xyz" + } + }, + { + "documentation": "StreamId test: Invalid streamId with custom endpoint and ARN", + "expect": { + "endpoint": { + "url": "https://kinesis-pod2.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071=xyz", + "Endpoint": "https://kinesis-pod2.us-west-2.amazonaws.com", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid streamId with longer prefix", + "expect": { + "endpoint": { + "url": "https://123.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k0174683507123-xyz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid streamId with shorter prefix", + "expect": { + "endpoint": { + "url": "https://123.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835-xyz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid streamId with longer suffix", + "expect": { + "endpoint": { + "url": "https://123.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071-wxyz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "StreamId test: Invalid streamId with shorter suffix", + "expect": { + "endpoint": { + "url": "https://123.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071-yz", + "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + } + }, + { + "documentation": "ChannelARN: endpoint targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis.us-east-1.amazonaws.com" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1" + }, + "operationName": "DescribeChannel", + "operationParams": { + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ChannelARN: endpoint with FIPS targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.us-east-1.amazonaws.com" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::UseFIPS": true + }, + "operationName": "DeleteChannel", + "operationParams": { + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false, + "OperationType": "control", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ChannelARN: endpoint with DualStack targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis.us-east-1.api.aws" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::UseDualStack": true + }, + "operationName": "UpdateChannel", + "operationParams": { + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true, + "OperationType": "control", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "ChannelARN: endpoint with FIPS and DualStack targeting control operation type", + "expect": { + "endpoint": { + "url": "https://apu0zt8ge6utbndxe.control-kinesis-fips.us-east-1.api.aws" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::UseFIPS": true, + "AWS::UseDualStack": true + }, + "operationName": "DescribeChannel", + "operationParams": { + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true, + "OperationType": "control", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" + } + }, + { + "documentation": "Invalid ChannelARN: ChannelARN only supports channel arn types", + "expect": { + "error": "Invalid ARN: ChannelARN only supports `channel` arn types, found: `stream`." + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "data", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:stream/test-stream" + } + }, + { + "documentation": "Invalid ChannelARN: ARN was not for the Kinesis service", "expect": { - "endpoint": { - "url": "https://af4lwng4k01746835071.xyz.data-kinesis.us-east-1.amazonaws.com" - } + "error": "Invalid ARN: The ARN was not for the Kinesis service, found: s3." }, "params": { "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "data", - "StreamId": "af4lwng4k01746835071-xyz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:s3:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid StreamId with ARN", + "documentation": "Invalid ChannelARN: OperationType not set", "expect": { - "endpoint": { - "url": "https://123.data-kinesis.us-east-1.amazonaws.com" - } + "error": "Operation Type is not set. Please contact service team for resolution." }, "params": { "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, - "OperationType": "data", - "StreamId": "af4lwng4k01746835071=xyz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid streamId with custom endpoint", + "documentation": "Invalid ChannelARN: partitions mismatch", "expect": { - "endpoint": { - "url": "https://kinesis-pod2.us-west-2.amazonaws.com" - } + "error": "Partition: aws from ARN doesn't match with partition name: aws-us-gov." }, "params": { - "Region": "us-west-2", + "Region": "us-gov-west-1", "UseFIPS": false, "UseDualStack": false, - "OperationType": "control", - "StreamId": "af4lwng4k01746835071=xyz", - "Endpoint": "https://kinesis-pod2.us-west-2.amazonaws.com" + "OperationType": "data", + "ChannelARN": "arn:aws:kinesis:us-west-2:298091445058:channel/apu0zt8ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid streamId", + "documentation": "Invalid ChannelARN: missing channel id", "expect": { - "endpoint": { - "url": "https://kinesis.us-west-2.amazonaws.com" - } + "error": "Invalid ARN: Missing channel id." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, - "OperationType": "control", - "StreamId": "af4lwng4k01746835071=xyz" + "OperationType": "data", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel" } }, { - "documentation": "StreamId test: Invalid streamId with custom endpoint and ARN", + "documentation": "Invalid ChannelARN: channel id contains a period (subdomains not allowed)", "expect": { - "endpoint": { - "url": "https://kinesis-pod2.us-west-2.amazonaws.com" - } + "error": "Invalid ARN: Invalid channel id." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamId": "af4lwng4k01746835071=xyz", - "Endpoint": "https://kinesis-pod2.us-west-2.amazonaws.com", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8.ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid streamId with longer prefix", + "documentation": "Invalid ChannelARN: channel id exceeds 63 character host label limit", "expect": { - "endpoint": { - "url": "https://123.control-kinesis.us-west-2.amazonaws.com" - } + "error": "Invalid ARN: Invalid channel id." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamId": "af4lwng4k0174683507123-xyz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } }, { - "documentation": "StreamId test: Invalid streamId with shorter prefix", + "documentation": "Invalid ChannelARN: channel id starts with a hyphen", "expect": { - "endpoint": { - "url": "https://123.control-kinesis.us-west-2.amazonaws.com" - } + "error": "Invalid ARN: Invalid channel id." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamId": "af4lwng4k01746835-xyz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/-pu0zt8ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid streamId with longer suffix", + "documentation": "Invalid ChannelARN: channel id contains an invalid character", "expect": { - "endpoint": { - "url": "https://123.control-kinesis.us-west-2.amazonaws.com" - } + "error": "Invalid ARN: Invalid channel id." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamId": "af4lwng4k01746835071-wxyz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8_ge6utbndxe" } }, { - "documentation": "StreamId test: Invalid streamId with shorter suffix", + "documentation": "Invalid ChannelARN: data operation type is not supported for channel", "expect": { - "endpoint": { - "url": "https://123.control-kinesis.us-west-2.amazonaws.com" - } + "error": "ChannelARN does not support the `data` operation type." }, "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, - "OperationType": "control", - "StreamId": "af4lwng4k01746835071-yz", - "ResourceARN": "arn:aws:kinesis:us-east-1:123:stream/test-stream" + "OperationType": "data", + "ChannelARN": "arn:aws:kinesis:us-east-1:298091445058:channel/apu0zt8ge6utbndxe" } }, { @@ -9396,33 +12168,194 @@ "url": "https://kinesis.us-west-2.amazonaws.com" } }, - "operationInputs": [ - { - "builtInParams": { - "AWS::Region": "us-west-2", - "AWS::Auth::AccountId": "123", - "AWS::Auth::AccountIdEndpointMode": "disabled" - }, - "operationName": "ListShards", - "operationParams": { - "StreamName": "testStream" - } - } - ], + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123", + "AWS::Auth::AccountIdEndpointMode": "disabled" + }, + "operationName": "ListShards", + "operationParams": { + "StreamName": "testStream" + } + } + ], + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123", + "AccountIdEndpointMode": "disabled" + } + }, + { + "documentation": "Account Id and StreamArn with account id endpoint mode disabled", + "expect": { + "endpoint": { + "url": "https://456.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamARN": "arn:aws:kinesis:us-west-2:456:stream/testStream", + "AccountId": "123", + "AccountIdEndpointMode": "disabled" + } + }, + { + "documentation": "Account Id missing with account id endpoint mode required", + "expect": { + "error": "AccountIdEndpointMode is required but no AccountID was provided or able to be loaded" + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountIdEndpointMode": "required" + }, + "operationName": "ListShards", + "operationParams": { + "StreamName": "testStream" + } + } + ], + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id missing with account id endpoint mode required, fips and dual stack enabled", + "expect": { + "error": "AccountIdEndpointMode is required but no AccountID was provided or able to be loaded" + }, + "params": { + "Region": "us-west-2", + "UseFIPS": true, + "UseDualStack": true, + "OperationType": "control", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id missing with account id endpoint mode required in ADC region", + "expect": { + "error": "Invalid Configuration: AccountIdEndpointMode is required but account endpoints are not supported in this partition" + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id present with account id endpoint mode required in ADC region", + "expect": { + "error": "Invalid Configuration: AccountIdEndpointMode is required but account endpoints are not supported in this partition" + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123456789012", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id present with account id endpoint mode preferred in ADC region", + "expect": { + "endpoint": { + "url": "https://kinesis.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" + } + }, + { + "documentation": "Account Id missing with account id endpoint mode required and endpoint override", + "expect": { + "endpoint": { + "url": "https://kinesis-pod1.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "Endpoint": "https://kinesis-pod1.us-west-2.amazonaws.com", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id missing with StreamArn and account id endpoint mode required", + "expect": { + "endpoint": { + "url": "https://456.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamARN": "arn:aws:kinesis:us-west-2:456:stream/testStream", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id missing with StreamId and account id endpoint mode required", + "expect": { + "endpoint": { + "url": "https://af4lwng4k01746835071.xyz.control-kinesis.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "StreamId": "af4lwng4k01746835071-xyz", + "AccountIdEndpointMode": "required" + } + }, + { + "documentation": "Account Id missing with account id endpoint mode preferred", + "expect": { + "endpoint": { + "url": "https://kinesis.us-west-2.amazonaws.com" + } + }, "params": { "Region": "us-west-2", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "AccountId": "123", - "AccountIdEndpointMode": "disabled" + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id and StreamArn with account id endpoint mode disabled", + "documentation": "Account Id missing with account id endpoint mode disabled", "expect": { "endpoint": { - "url": "https://456.control-kinesis.us-west-2.amazonaws.com" + "url": "https://kinesis.us-west-2.amazonaws.com" } }, "params": { @@ -9430,85 +12363,104 @@ "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamARN": "arn:aws:kinesis:us-west-2:456:stream/testStream", - "AccountId": "123", "AccountIdEndpointMode": "disabled" } }, { - "documentation": "Account Id missing with account id endpoint mode required", + "documentation": "CreateStream: control operation type with AccountId", "expect": { - "error": "AccountIdEndpointMode is required but no AccountID was provided or able to be loaded" + "endpoint": { + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-east-1.amazonaws.com" + } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-west-2", - "AWS::Auth::AccountIdEndpointMode": "required" + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred" }, - "operationName": "ListShards", + "operationName": "CreateStream", "operationParams": { - "StreamName": "testStream" + "StreamName": "test-stream", + "ShardCount": 1 } } ], "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "AccountIdEndpointMode": "required" + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id missing with account id endpoint mode required, fips and dual stack enabled", + "documentation": "CreateStream: control operation type with FIPS and AccountId", "expect": { - "error": "AccountIdEndpointMode is required but no AccountID was provided or able to be loaded" + "endpoint": { + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-east-1.amazonaws.com" + } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseFIPS": true + }, + "operationName": "CreateStream", + "operationParams": { + "StreamName": "test-stream", + "ShardCount": 1 + } + } + ], "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": true, - "UseDualStack": true, - "OperationType": "control", - "AccountIdEndpointMode": "required" - } - }, - { - "documentation": "Account Id missing with account id endpoint mode required in ADC region", - "expect": { - "error": "Invalid Configuration: AccountIdEndpointMode is required but account endpoints are not supported in this partition" - }, - "params": { - "Region": "us-iso-east-1", - "UseFIPS": false, - "UseDualStack": false, - "OperationType": "control", - "AccountIdEndpointMode": "required" - } - }, - { - "documentation": "Account Id present with account id endpoint mode required in ADC region", - "expect": { - "error": "Invalid Configuration: AccountIdEndpointMode is required but account endpoints are not supported in this partition" - }, - "params": { - "Region": "us-iso-east-1", - "UseFIPS": false, "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "required" + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id present with account id endpoint mode preferred in ADC region", + "documentation": "ListStreams: control operation type with AccountId", "expect": { "endpoint": { - "url": "https://kinesis.us-iso-east-1.c2s.ic.gov" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-west-2.amazonaws.com" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred" + }, + "operationName": "ListStreams" + } + ], "params": { - "Region": "us-iso-east-1", + "Region": "us-west-2", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", @@ -9517,85 +12469,167 @@ } }, { - "documentation": "Account Id missing with account id endpoint mode required and endpoint override", + "documentation": "ListStreams: control operation type with FIPS and DualStack", "expect": { "endpoint": { - "url": "https://kinesis-pod1.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-west-2.api.aws" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseFIPS": true, + "AWS::UseDualStack": true + }, + "operationName": "ListStreams" + } + ], "params": { "Region": "us-west-2", - "UseFIPS": false, - "UseDualStack": false, + "UseFIPS": true, + "UseDualStack": true, "OperationType": "control", - "Endpoint": "https://kinesis-pod1.us-west-2.amazonaws.com", - "AccountIdEndpointMode": "required" + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id missing with StreamArn and account id endpoint mode required", + "documentation": "DescribeLimits: control operation type with AccountId", "expect": { "endpoint": { - "url": "https://456.control-kinesis.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-east-1.amazonaws.com" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred" + }, + "operationName": "DescribeLimits" + } + ], "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", - "StreamARN": "arn:aws:kinesis:us-west-2:456:stream/testStream", - "AccountIdEndpointMode": "required" + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id missing with StreamId and account id endpoint mode required", + "documentation": "DescribeLimits: control operation type with FIPS", "expect": { "endpoint": { - "url": "https://af4lwng4k01746835071.xyz.control-kinesis.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-east-1.amazonaws.com" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseFIPS": true + }, + "operationName": "DescribeLimits" + } + ], "params": { - "Region": "us-west-2", - "UseFIPS": false, + "Region": "us-east-1", + "UseFIPS": true, "UseDualStack": false, "OperationType": "control", - "StreamId": "af4lwng4k01746835071-xyz", - "AccountIdEndpointMode": "required" + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id missing with account id endpoint mode preferred", + "documentation": "DescribeAccountSettings: control operation type with AccountId", "expect": { "endpoint": { - "url": "https://kinesis.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-west-2.amazonaws.com" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred" + }, + "operationName": "DescribeAccountSettings" + } + ], "params": { "Region": "us-west-2", "UseFIPS": false, "UseDualStack": false, "OperationType": "control", + "AccountId": "123456789012", "AccountIdEndpointMode": "preferred" } }, { - "documentation": "Account Id missing with account id endpoint mode disabled", + "documentation": "DescribeAccountSettings: control operation type with FIPS and DualStack", "expect": { "endpoint": { - "url": "https://kinesis.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-west-2.api.aws" } }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseFIPS": true, + "AWS::UseDualStack": true + }, + "operationName": "DescribeAccountSettings" + } + ], "params": { "Region": "us-west-2", - "UseFIPS": false, - "UseDualStack": false, + "UseFIPS": true, + "UseDualStack": true, "OperationType": "control", - "AccountIdEndpointMode": "disabled" + "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "CreateStream: control operation type with AccountId", + "documentation": "UpdateAccountSettings: control operation type with AccountId", "expect": { "endpoint": { "properties": { @@ -9613,10 +12647,11 @@ "AWS::Auth::AccountId": "123456789012", "AWS::Auth::AccountIdEndpointMode": "preferred" }, - "operationName": "CreateStream", + "operationName": "UpdateAccountSettings", "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 + "MinimumThroughputBillingCommitment": { + "Status": "ENABLED" + } } } ], @@ -9630,7 +12665,7 @@ } }, { - "documentation": "CreateStream: control operation type with FIPS and AccountId", + "documentation": "UpdateAccountSettings: control operation type with FIPS", "expect": { "endpoint": { "properties": { @@ -9649,10 +12684,11 @@ "AWS::Auth::AccountIdEndpointMode": "preferred", "AWS::UseFIPS": true }, - "operationName": "CreateStream", + "operationName": "UpdateAccountSettings", "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 + "MinimumThroughputBillingCommitment": { + "Status": "ENABLED" + } } } ], @@ -9666,15 +12702,40 @@ } }, { - "documentation": "ListStreams: control operation type with AccountId", + "documentation": "CreateStream: account id endpoint mode disabled falls back to regional endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] + "url": "https://kinesis.us-east-1.amazonaws.com" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "disabled" }, - "url": "https://123456789012.control-kinesis.us-west-2.amazonaws.com" + "operationName": "CreateStream", + "operationParams": { + "StreamName": "test-stream", + "ShardCount": 1 + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123456789012", + "AccountIdEndpointMode": "disabled" + } + }, + { + "documentation": "ListStreams: account id endpoint mode disabled falls back to regional endpoint", + "expect": { + "endpoint": { + "url": "https://kinesis.us-west-2.amazonaws.com" } }, "operationInputs": [ @@ -9682,7 +12743,7 @@ "builtInParams": { "AWS::Region": "us-west-2", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred" + "AWS::Auth::AccountIdEndpointMode": "disabled" }, "operationName": "ListStreams" } @@ -9693,19 +12754,40 @@ "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "ListStreams: control operation type with FIPS and DualStack", + "documentation": "DescribeLimits: account id endpoint mode disabled falls back to regional endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] + "url": "https://kinesis.us-east-1.amazonaws.com" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "disabled" }, - "url": "https://123456789012.control-kinesis-fips.us-west-2.api.aws" + "operationName": "DescribeLimits" + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123456789012", + "AccountIdEndpointMode": "disabled" + } + }, + { + "documentation": "DescribeAccountSettings: account id endpoint mode disabled falls back to regional endpoint", + "expect": { + "endpoint": { + "url": "https://kinesis.us-west-2.amazonaws.com" } }, "operationInputs": [ @@ -9713,32 +12795,25 @@ "builtInParams": { "AWS::Region": "us-west-2", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred", - "AWS::UseFIPS": true, - "AWS::UseDualStack": true + "AWS::Auth::AccountIdEndpointMode": "disabled" }, - "operationName": "ListStreams" + "operationName": "DescribeAccountSettings" } ], "params": { "Region": "us-west-2", - "UseFIPS": true, - "UseDualStack": true, + "UseFIPS": false, + "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "DescribeLimits: control operation type with AccountId", + "documentation": "UpdateAccountSettings: account id endpoint mode disabled falls back to regional endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] - }, - "url": "https://123456789012.control-kinesis.us-east-1.amazonaws.com" + "url": "https://kinesis.us-east-1.amazonaws.com" } }, "operationInputs": [ @@ -9746,9 +12821,14 @@ "builtInParams": { "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred" + "AWS::Auth::AccountIdEndpointMode": "disabled" }, - "operationName": "DescribeLimits" + "operationName": "UpdateAccountSettings", + "operationParams": { + "MinimumThroughputBillingCommitment": { + "Status": "ENABLED" + } + } } ], "params": { @@ -9757,19 +12837,14 @@ "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "DescribeLimits: control operation type with FIPS", + "documentation": "CreateStream: account id endpoint mode disabled with FIPS falls back to regional FIPS endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] - }, - "url": "https://123456789012.control-kinesis-fips.us-east-1.amazonaws.com" + "url": "https://kinesis-fips.us-east-1.amazonaws.com" } }, "operationInputs": [ @@ -9777,10 +12852,14 @@ "builtInParams": { "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::Auth::AccountIdEndpointMode": "disabled", "AWS::UseFIPS": true }, - "operationName": "DescribeLimits" + "operationName": "CreateStream", + "operationParams": { + "StreamName": "test-stream", + "ShardCount": 1 + } } ], "params": { @@ -9789,75 +12868,74 @@ "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "DescribeAccountSettings: control operation type with AccountId", + "documentation": "CreateStream: account id endpoint mode disabled with DualStack falls back to regional DualStack endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] - }, - "url": "https://123456789012.control-kinesis.us-west-2.amazonaws.com" + "url": "https://kinesis.us-east-1.api.aws" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-west-2", + "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred" + "AWS::Auth::AccountIdEndpointMode": "disabled", + "AWS::UseDualStack": true }, - "operationName": "DescribeAccountSettings" + "operationName": "CreateStream", + "operationParams": { + "StreamName": "test-stream", + "ShardCount": 1 + } } ], "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": false, - "UseDualStack": false, + "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "DescribeAccountSettings: control operation type with FIPS and DualStack", + "documentation": "CreateStream: account id endpoint mode disabled with FIPS and DualStack falls back to regional FIPS DualStack endpoint", "expect": { "endpoint": { - "properties": { - "metricValues": [ - "O" - ] - }, - "url": "https://123456789012.control-kinesis-fips.us-west-2.api.aws" + "url": "https://kinesis-fips.us-east-1.api.aws" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-west-2", + "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::Auth::AccountIdEndpointMode": "disabled", "AWS::UseFIPS": true, "AWS::UseDualStack": true }, - "operationName": "DescribeAccountSettings" + "operationName": "CreateStream", + "operationParams": { + "StreamName": "test-stream", + "ShardCount": 1 + } } ], "params": { - "Region": "us-west-2", + "Region": "us-east-1", "UseFIPS": true, "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "preferred" + "AccountIdEndpointMode": "disabled" } }, { - "documentation": "UpdateAccountSettings: control operation type with AccountId", + "documentation": "CreateChannel: control operation type with AccountId", "expect": { "endpoint": { "properties": { @@ -9875,11 +12953,18 @@ "AWS::Auth::AccountId": "123456789012", "AWS::Auth::AccountIdEndpointMode": "preferred" }, - "operationName": "UpdateAccountSettings", + "operationName": "CreateChannel", "operationParams": { - "MinimumThroughputBillingCommitment": { - "Status": "ENABLED" - } + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] } } ], @@ -9893,7 +12978,7 @@ } }, { - "documentation": "UpdateAccountSettings: control operation type with FIPS", + "documentation": "CreateChannel: control operation type with FIPS and AccountId", "expect": { "endpoint": { "properties": { @@ -9912,11 +12997,18 @@ "AWS::Auth::AccountIdEndpointMode": "preferred", "AWS::UseFIPS": true }, - "operationName": "UpdateAccountSettings", + "operationName": "CreateChannel", "operationParams": { - "MinimumThroughputBillingCommitment": { - "Status": "ENABLED" - } + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] } } ], @@ -9930,10 +13022,15 @@ } }, { - "documentation": "CreateStream: account id endpoint mode disabled falls back to regional endpoint", + "documentation": "CreateChannel: control operation type with DualStack and AccountId", "expect": { "endpoint": { - "url": "https://kinesis.us-east-1.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-east-1.api.aws" } }, "operationInputs": [ @@ -9941,52 +13038,80 @@ "builtInParams": { "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled" + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseDualStack": true }, - "operationName": "CreateStream", + "operationName": "CreateChannel", "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] } } ], "params": { "Region": "us-east-1", "UseFIPS": false, - "UseDualStack": false, + "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "disabled" + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "ListStreams: account id endpoint mode disabled falls back to regional endpoint", + "documentation": "CreateChannel: control operation type with FIPS and DualStack and AccountId", "expect": { "endpoint": { - "url": "https://kinesis.us-west-2.amazonaws.com" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-east-1.api.aws" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-west-2", + "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled" + "AWS::Auth::AccountIdEndpointMode": "preferred", + "AWS::UseFIPS": true, + "AWS::UseDualStack": true }, - "operationName": "ListStreams" + "operationName": "CreateChannel", + "operationParams": { + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] + } } ], "params": { - "Region": "us-west-2", - "UseFIPS": false, - "UseDualStack": false, + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "disabled" + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "DescribeLimits: account id endpoint mode disabled falls back to regional endpoint", + "documentation": "CreateChannel: account id endpoint mode disabled falls back to regional endpoint", "expect": { "endpoint": { "url": "https://kinesis.us-east-1.amazonaws.com" @@ -9999,7 +13124,19 @@ "AWS::Auth::AccountId": "123456789012", "AWS::Auth::AccountIdEndpointMode": "disabled" }, - "operationName": "DescribeLimits" + "operationName": "CreateChannel", + "operationParams": { + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] + } } ], "params": { @@ -10012,25 +13149,38 @@ } }, { - "documentation": "DescribeAccountSettings: account id endpoint mode disabled falls back to regional endpoint", + "documentation": "CreateChannel: account id endpoint mode disabled with FIPS falls back to regional FIPS endpoint", "expect": { "endpoint": { - "url": "https://kinesis.us-west-2.amazonaws.com" + "url": "https://kinesis-fips.us-east-1.amazonaws.com" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-west-2", + "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled" + "AWS::Auth::AccountIdEndpointMode": "disabled", + "AWS::UseFIPS": true }, - "operationName": "DescribeAccountSettings" + "operationName": "CreateChannel", + "operationParams": { + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] + } } ], "params": { - "Region": "us-west-2", - "UseFIPS": false, + "Region": "us-east-1", + "UseFIPS": true, "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", @@ -10038,10 +13188,10 @@ } }, { - "documentation": "UpdateAccountSettings: account id endpoint mode disabled falls back to regional endpoint", + "documentation": "CreateChannel: account id endpoint mode disabled with DualStack falls back to regional DualStack endpoint", "expect": { "endpoint": { - "url": "https://kinesis.us-east-1.amazonaws.com" + "url": "https://kinesis.us-east-1.api.aws" } }, "operationInputs": [ @@ -10049,30 +13199,38 @@ "builtInParams": { "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled" + "AWS::Auth::AccountIdEndpointMode": "disabled", + "AWS::UseDualStack": true }, - "operationName": "UpdateAccountSettings", + "operationName": "CreateChannel", "operationParams": { - "MinimumThroughputBillingCommitment": { - "Status": "ENABLED" - } + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] } } ], "params": { "Region": "us-east-1", "UseFIPS": false, - "UseDualStack": false, + "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", "AccountIdEndpointMode": "disabled" } }, { - "documentation": "CreateStream: account id endpoint mode disabled with FIPS falls back to regional FIPS endpoint", + "documentation": "CreateChannel: account id endpoint mode disabled with FIPS and DualStack falls back to regional FIPS DualStack endpoint", "expect": { "endpoint": { - "url": "https://kinesis-fips.us-east-1.amazonaws.com" + "url": "https://kinesis-fips.us-east-1.api.aws" } }, "operationInputs": [ @@ -10081,84 +13239,120 @@ "AWS::Region": "us-east-1", "AWS::Auth::AccountId": "123456789012", "AWS::Auth::AccountIdEndpointMode": "disabled", - "AWS::UseFIPS": true + "AWS::UseFIPS": true, + "AWS::UseDualStack": true }, - "operationName": "CreateStream", + "operationName": "CreateChannel", "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 + "ChannelName": "test-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/test-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ] } } ], "params": { "Region": "us-east-1", "UseFIPS": true, - "UseDualStack": false, + "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", "AccountIdEndpointMode": "disabled" } }, { - "documentation": "CreateStream: account id endpoint mode disabled with DualStack falls back to regional DualStack endpoint", + "documentation": "ListChannels: control operation type with AccountId", "expect": { "endpoint": { - "url": "https://kinesis.us-east-1.api.aws" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis.us-west-2.amazonaws.com" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-east-1", + "AWS::Region": "us-west-2", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled", - "AWS::UseDualStack": true + "AWS::Auth::AccountIdEndpointMode": "preferred" }, - "operationName": "CreateStream", - "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 - } + "operationName": "ListChannels" } ], "params": { - "Region": "us-east-1", + "Region": "us-west-2", "UseFIPS": false, - "UseDualStack": true, + "UseDualStack": false, "OperationType": "control", "AccountId": "123456789012", - "AccountIdEndpointMode": "disabled" + "AccountIdEndpointMode": "preferred" } }, { - "documentation": "CreateStream: account id endpoint mode disabled with FIPS and DualStack falls back to regional FIPS DualStack endpoint", + "documentation": "ListChannels: control operation type with FIPS and DualStack", "expect": { "endpoint": { - "url": "https://kinesis-fips.us-east-1.api.aws" + "properties": { + "metricValues": [ + "O" + ] + }, + "url": "https://123456789012.control-kinesis-fips.us-west-2.api.aws" } }, "operationInputs": [ { "builtInParams": { - "AWS::Region": "us-east-1", + "AWS::Region": "us-west-2", "AWS::Auth::AccountId": "123456789012", - "AWS::Auth::AccountIdEndpointMode": "disabled", + "AWS::Auth::AccountIdEndpointMode": "preferred", "AWS::UseFIPS": true, "AWS::UseDualStack": true }, - "operationName": "CreateStream", - "operationParams": { - "StreamName": "test-stream", - "ShardCount": 1 - } + "operationName": "ListChannels" } ], "params": { - "Region": "us-east-1", + "Region": "us-west-2", "UseFIPS": true, "UseDualStack": true, "OperationType": "control", "AccountId": "123456789012", + "AccountIdEndpointMode": "preferred" + } + }, + { + "documentation": "ListChannels: account id endpoint mode disabled falls back to regional endpoint", + "expect": { + "endpoint": { + "url": "https://kinesis.us-west-2.amazonaws.com" + } + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-west-2", + "AWS::Auth::AccountId": "123456789012", + "AWS::Auth::AccountIdEndpointMode": "disabled" + }, + "operationName": "ListChannels" + } + ], + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false, + "OperationType": "control", + "AccountId": "123456789012", "AccountIdEndpointMode": "disabled" } } @@ -10182,6 +13376,156 @@ "smithy.api#error": "client" } }, + "com.amazonaws.kinesis#ListChannels": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#ListChannelsInput" + }, + "output": { + "target": "com.amazonaws.kinesis#ListChannelsOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#ExpiredNextTokenException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Lists the channels in your account. You can filter the results by source stream. The results are paginated. Use the NextToken value returned in the response to retrieve additional results.

\n

Use this operation to find channels before deleting a stream, or to audit the channels configured in an Amazon Web Services Region.

\n

This operation has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. Exceeding 5 TPS results in a LimitExceededException.

", + "smithy.api#examples": [ + { + "title": "To list channels", + "output": { + "ChannelSummaries": [ + { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "ACTIVE", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ChannelDestinationType": "S3", + "Streams": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z" + } + ] + } + ] + } + }, + { + "title": "To list channels filtered by stream", + "input": { + "StreamFilter": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name" + } + ], + "MaxResults": 10 + }, + "output": { + "ChannelSummaries": [ + { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "ACTIVE", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ChannelDestinationType": "S3", + "Streams": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z" + } + ] + } + ], + "NextToken": "AAAAAgAAAAEAAAABbXktbmV4dC1wYWdlLXRva2Vu" + } + } + ], + "smithy.api#paginated": { + "inputToken": "NextToken", + "outputToken": "NextToken", + "items": "ChannelSummaries", + "pageSize": "MaxResults" + }, + "smithy.api#readonly": {}, + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#ListChannelsInput": { + "type": "structure", + "members": { + "StreamFilter": { + "target": "com.amazonaws.kinesis#StreamFilterList", + "traits": { + "smithy.api#documentation": "

Filters the results to channels associated with the specified streams.

" + } + }, + "MaxResults": { + "target": "com.amazonaws.kinesis#ListChannelsInputLimit", + "traits": { + "smithy.api#documentation": "

The maximum number of channels to return in a single call. The default value is 100. If you specify a value greater than 100, at most 100 results are returned.

" + } + }, + "NextToken": { + "target": "com.amazonaws.kinesis#NextToken", + "traits": { + "smithy.api#documentation": "

The pagination token returned by a previous call. Specify this token to retrieve the next page of results. This value is null when there are no more results to return.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#ListChannelsInputLimit": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 10000 + } + } + }, + "com.amazonaws.kinesis#ListChannelsOutput": { + "type": "structure", + "members": { + "ChannelSummaries": { + "target": "com.amazonaws.kinesis#ChannelSummaryList", + "traits": { + "smithy.api#documentation": "

A list of channel summaries.

", + "smithy.api#required": {} + } + }, + "NextToken": { + "target": "com.amazonaws.kinesis#NextToken", + "traits": { + "smithy.api#documentation": "

The pagination token to use in a subsequent call to retrieve the next page of results. This value is null when there are no more results to return.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, "com.amazonaws.kinesis#ListShards": { "type": "operation", "input": { @@ -11012,12 +14356,82 @@ } } }, - "com.amazonaws.kinesis#PartitionKey": { - "type": "string", - "traits": { - "smithy.api#length": { - "min": 1, - "max": 256 + "com.amazonaws.kinesis#PartitionField": { + "type": "structure", + "members": { + "Transform": { + "target": "com.amazonaws.kinesis#PartitionTransform", + "traits": { + "smithy.api#documentation": "

The partition transform to apply. The only valid value is TIME_HOUR.

", + "smithy.api#required": {} + } + }, + "SourceName": { + "target": "com.amazonaws.kinesis#PartitionSourceName", + "traits": { + "smithy.api#documentation": "

The name of the source column used for partitioning. This column must be of the timestamptz type.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies a single partition field.

" + } + }, + "com.amazonaws.kinesis#PartitionFieldList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#PartitionField" + }, + "traits": { + "smithy.api#length": { + "min": 1, + "max": 10 + } + } + }, + "com.amazonaws.kinesis#PartitionKey": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + } + } + }, + "com.amazonaws.kinesis#PartitionSourceName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 255 + }, + "smithy.api#pattern": "^[a-zA-Z0-9\\.\\_]+$" + } + }, + "com.amazonaws.kinesis#PartitionSpec": { + "type": "structure", + "members": { + "PartitionFields": { + "target": "com.amazonaws.kinesis#PartitionFieldList", + "traits": { + "smithy.api#documentation": "

The list of partition fields.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies how the destination table is partitioned.

" + } + }, + "com.amazonaws.kinesis#PartitionTransform": { + "type": "enum", + "members": { + "TIME_HOUR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "TIME_HOUR" + } } } }, @@ -11059,6 +14473,9 @@ { "target": "com.amazonaws.kinesis#AccessDeniedException" }, + { + "target": "com.amazonaws.kinesis#DryRunOperationException" + }, { "target": "com.amazonaws.kinesis#InternalFailureException" }, @@ -11151,6 +14568,12 @@ "name": "StreamId" } } + }, + "DryRun": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Checks if your request will succeed. DryRun is an optional\n parameter.

" + } } }, "traits": { @@ -11199,6 +14622,9 @@ { "target": "com.amazonaws.kinesis#AccessDeniedException" }, + { + "target": "com.amazonaws.kinesis#DryRunOperationException" + }, { "target": "com.amazonaws.kinesis#InternalFailureException" }, @@ -11272,6 +14698,12 @@ "name": "StreamId" } } + }, + "DryRun": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Checks if your request will succeed. DryRun is an optional\n parameter.

" + } } }, "traits": { @@ -11365,36 +14797,287 @@ "ErrorCode": { "target": "com.amazonaws.kinesis#ErrorCode", "traits": { - "smithy.api#documentation": "

The error code for an individual record result. ErrorCodes can be either\n ProvisionedThroughputExceededException or\n InternalFailure.

" + "smithy.api#documentation": "

The error code for an individual record result. ErrorCodes can be either\n ProvisionedThroughputExceededException or\n InternalFailure.

" + } + }, + "ErrorMessage": { + "target": "com.amazonaws.kinesis#ErrorMessage", + "traits": { + "smithy.api#documentation": "

The error message for an individual record result. An ErrorCode value of\n ProvisionedThroughputExceededException has an error message that\n includes the account ID, stream name, and shard ID. An ErrorCode value of\n InternalFailure has the error message \"Internal Service\n Failure\".

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the result of an individual record from a PutRecords request.\n A record that is successfully added to a stream includes SequenceNumber and\n ShardId in the result. A record that fails to be added to the stream\n includes ErrorCode and ErrorMessage in the result.

" + } + }, + "com.amazonaws.kinesis#PutRecordsResultEntryList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#PutRecordsResultEntry" + }, + "traits": { + "smithy.api#length": { + "min": 1, + "max": 500 + } + } + }, + "com.amazonaws.kinesis#PutResourcePolicy": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#PutResourcePolicyInput" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Attaches a resource-based policy to a data stream or registered consumer. If you are using an identity other than the root user of \n the Amazon Web Services account that owns the resource, the calling identity must have the PutResourcePolicy permissions on the \n specified Kinesis Data Streams resource and belong to the owner's account in order to use this operation.\n If you don't have PutResourcePolicy permissions, Amazon Kinesis Data Streams returns a 403 Access Denied error. \n If you receive a ResourceNotFoundException, check to see if you passed a valid stream or consumer resource.\n

\n

Request patterns can be one of the following:

\n \n

For more information, see Controlling Access to Amazon Kinesis Data Streams Resources Using IAM.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#PutResourcePolicyInput": { + "type": "structure", + "members": { + "ResourceARN": { + "target": "com.amazonaws.kinesis#ResourceARN", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", + "smithy.api#required": {}, + "smithy.rules#contextParam": { + "name": "ResourceARN" + } + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } + } + }, + "Policy": { + "target": "com.amazonaws.kinesis#Policy", + "traits": { + "smithy.api#documentation": "

Details of the resource policy. It must include the identity of the principal and the actions allowed on this resource. This is formatted as a JSON string.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#Record": { + "type": "structure", + "members": { + "SequenceNumber": { + "target": "com.amazonaws.kinesis#SequenceNumber", + "traits": { + "smithy.api#documentation": "

The unique identifier of the record within its shard.

", + "smithy.api#required": {} + } + }, + "ApproximateArrivalTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", + "traits": { + "smithy.api#documentation": "

The approximate time that the record was inserted into the stream.

" + } + }, + "Data": { + "target": "com.amazonaws.kinesis#Data", + "traits": { + "smithy.api#documentation": "

The data blob. The data in the blob is both opaque and immutable to Kinesis Data\n Streams, which does not inspect, interpret, or change the data in the blob in any way.\n When the data blob (the payload before base64-encoding) is added to the partition key\n size, the total size must not exceed the maximum record size (10 MiB).

", + "smithy.api#required": {} + } + }, + "PartitionKey": { + "target": "com.amazonaws.kinesis#PartitionKey", + "traits": { + "smithy.api#documentation": "

Identifies which shard in the stream the data record is assigned to.

", + "smithy.api#required": {} + } + }, + "EncryptionType": { + "target": "com.amazonaws.kinesis#EncryptionType", + "traits": { + "smithy.api#documentation": "

The encryption type used on the record. This parameter can be one of the following\n values:

\n " + } + } + }, + "traits": { + "smithy.api#documentation": "

The unit of data of the Kinesis data stream, which is composed of a sequence number, a\n partition key, and a data blob.

" + } + }, + "com.amazonaws.kinesis#RecordConfiguration": { + "type": "structure", + "members": { + "RecordFormatType": { + "target": "com.amazonaws.kinesis#RecordFormatType", + "traits": { + "smithy.api#documentation": "

The format of records on the source stream. Valid values:

\n ", + "smithy.api#required": {} + } + }, + "GSRSchemaARN": { + "target": "com.amazonaws.kinesis#GSRSchemaARN", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the Amazon Web Services Glue Schema Registry schema used to validate records. Required when the channel destination is a streaming table (Amazon S3 Tables), for both the JSON and GSR_JSON record formats.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies the format of records read from the source stream.

" + } + }, + "com.amazonaws.kinesis#RecordFormatType": { + "type": "enum", + "members": { + "GSR_JSON": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "GSR_JSON" + } + }, + "JSON": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "JSON" + } + }, + "STRING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "STRING" + } + }, + "BYTE_ARRAY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BYTE_ARRAY" + } + } + } + }, + "com.amazonaws.kinesis#RecordList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#Record" + } + }, + "com.amazonaws.kinesis#RegisterStreamConsumer": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#RegisterStreamConsumerInput" + }, + "output": { + "target": "com.amazonaws.kinesis#RegisterStreamConsumerOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + } + ], + "traits": { + "smithy.api#documentation": "

Registers a consumer with a Kinesis data stream. When you use this operation, the\n consumer you register can then call SubscribeToShard to receive data\n from the stream using enhanced fan-out, at a rate of up to 2 MiB per second for every\n shard you subscribe to. This rate is unaffected by the total number of consumers that\n read from the same stream.

\n

You can add tags to the registered consumer when making a RegisterStreamConsumer request by setting the Tags parameter. If you pass the Tags parameter, in addition to having the kinesis:RegisterStreamConsumer permission, you must also have the kinesis:TagResource permission for the consumer that will be registered. Tags will take effect from the CREATING status of the consumer.

\n

With On-demand Advantage streams, you can register up to 50 consumers per stream to use Enhanced Fan-out. With On-demand Standard and Provisioned streams, you can register up to 20 consumers per stream to use Enhanced Fan-out. A given consumer can only be \n registered with one stream at a time.

\n

For an example of how to use this operation, see Enhanced Fan-Out\n Using the Kinesis Data Streams API.

\n

The use of this operation has a limit of five transactions per second per account.\n Also, only 5 consumers can be created simultaneously. In other words, you cannot have\n more than 5 consumers in a CREATING status at the same time. Registering a\n 6th consumer while there are 5 in a CREATING status results in a\n LimitExceededException.

", + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#RegisterStreamConsumerInput": { + "type": "structure", + "members": { + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the Kinesis data stream that you want to register the consumer with. For\n more info, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", + "smithy.api#required": {}, + "smithy.rules#contextParam": { + "name": "StreamARN" + } + } + }, + "ConsumerName": { + "target": "com.amazonaws.kinesis#ConsumerName", + "traits": { + "smithy.api#documentation": "

For a given Kinesis data stream, each consumer must have a unique name. However,\n consumer names don't have to be unique across data streams.

", + "smithy.api#required": {} + } + }, + "StreamId": { + "target": "com.amazonaws.kinesis#StreamId", + "traits": { + "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", + "smithy.rules#contextParam": { + "name": "StreamId" + } } }, - "ErrorMessage": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "Tags": { + "target": "com.amazonaws.kinesis#TagMap", "traits": { - "smithy.api#documentation": "

The error message for an individual record result. An ErrorCode value of\n ProvisionedThroughputExceededException has an error message that\n includes the account ID, stream name, and shard ID. An ErrorCode value of\n InternalFailure has the error message \"Internal Service\n Failure\".

" + "smithy.api#documentation": "

A set of up to 50 key-value pairs. A tag consists of a required key and an optional value.

" } } }, "traits": { - "smithy.api#documentation": "

Represents the result of an individual record from a PutRecords request.\n A record that is successfully added to a stream includes SequenceNumber and\n ShardId in the result. A record that fails to be added to the stream\n includes ErrorCode and ErrorMessage in the result.

" + "smithy.api#input": {} } }, - "com.amazonaws.kinesis#PutRecordsResultEntryList": { - "type": "list", - "member": { - "target": "com.amazonaws.kinesis#PutRecordsResultEntry" + "com.amazonaws.kinesis#RegisterStreamConsumerOutput": { + "type": "structure", + "members": { + "Consumer": { + "target": "com.amazonaws.kinesis#Consumer", + "traits": { + "smithy.api#documentation": "

An object that represents the details of the consumer you registered. When you\n register a consumer, it gets an ARN that is generated by Kinesis Data Streams.

", + "smithy.api#required": {} + } + } }, "traits": { - "smithy.api#length": { - "min": 1, - "max": 500 - } + "smithy.api#output": {} } }, - "com.amazonaws.kinesis#PutResourcePolicy": { + "com.amazonaws.kinesis#RemoveTagsFromStream": { "type": "operation", "input": { - "target": "com.amazonaws.kinesis#PutResourcePolicyInput" + "target": "com.amazonaws.kinesis#RemoveTagsFromStreamInput" }, "output": { "target": "smithy.api#Unit" @@ -11417,7 +15100,7 @@ } ], "traits": { - "smithy.api#documentation": "

Attaches a resource-based policy to a data stream or registered consumer. If you are using an identity other than the root user of \n the Amazon Web Services account that owns the resource, the calling identity must have the PutResourcePolicy permissions on the \n specified Kinesis Data Streams resource and belong to the owner's account in order to use this operation.\n If you don't have PutResourcePolicy permissions, Amazon Kinesis Data Streams returns a 403 Access Denied error. \n If you receive a ResourceNotFoundException, check to see if you passed a valid stream or consumer resource.\n

\n

Request patterns can be one of the following:

\n \n

For more information, see Controlling Access to Amazon Kinesis Data Streams Resources Using IAM.

", + "smithy.api#documentation": "

Removes tags from the specified Kinesis data stream. Removed tags are deleted and\n cannot be recovered after this operation successfully completes.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If you specify a tag that does not exist, it is ignored.

\n

\n RemoveTagsFromStream has a limit of five transactions per second per\n account.

", "smithy.rules#staticContextParams": { "OperationType": { "value": "control" @@ -11425,16 +15108,28 @@ } } }, - "com.amazonaws.kinesis#PutResourcePolicyInput": { + "com.amazonaws.kinesis#RemoveTagsFromStreamInput": { "type": "structure", "members": { - "ResourceARN": { - "target": "com.amazonaws.kinesis#ResourceARN", + "StreamName": { + "target": "com.amazonaws.kinesis#StreamName", "traits": { - "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the data stream or consumer.

", - "smithy.api#required": {}, + "smithy.api#documentation": "

The name of the stream.

" + } + }, + "TagKeys": { + "target": "com.amazonaws.kinesis#TagKeyList", + "traits": { + "smithy.api#documentation": "

A list of tag keys. Each corresponding tag is removed from the stream.

", + "smithy.api#required": {} + } + }, + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The ARN of the stream.

", "smithy.rules#contextParam": { - "name": "ResourceARN" + "name": "StreamARN" } } }, @@ -11446,268 +15141,412 @@ "name": "StreamId" } } + } + }, + "traits": { + "smithy.api#documentation": "

Represents the input for RemoveTagsFromStream.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#ResourceARN": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 }, - "Policy": { - "target": "com.amazonaws.kinesis#Policy", + "smithy.api#pattern": "^arn:aws.*:kinesis:.*:\\d{12}:.*(stream|channel)/\\S+$" + } + }, + "com.amazonaws.kinesis#ResourceInUseException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", "traits": { - "smithy.api#documentation": "

Details of the resource policy. It must include the identity of the principal and the actions allowed on this resource. This is formatted as a JSON string.

", - "smithy.api#required": {} + "smithy.api#documentation": "

A message that provides information about the error.

" } } }, "traits": { - "smithy.api#input": {} + "smithy.api#documentation": "

The resource is not available for this operation. For successful operation, the\n resource must be in the ACTIVE state.

", + "smithy.api#error": "client" } }, - "com.amazonaws.kinesis#Record": { + "com.amazonaws.kinesis#ResourceNotFoundException": { "type": "structure", "members": { - "SequenceNumber": { - "target": "com.amazonaws.kinesis#SequenceNumber", + "message": { + "target": "com.amazonaws.kinesis#ErrorMessage", "traits": { - "smithy.api#documentation": "

The unique identifier of the record within its shard.

", - "smithy.api#required": {} + "smithy.api#documentation": "

A message that provides information about the error.

" } + } + }, + "traits": { + "smithy.api#documentation": "

The requested resource could not be found. The stream might not be specified\n correctly.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.kinesis#RetentionPeriodHours": { + "type": "integer" + }, + "com.amazonaws.kinesis#RoleARN": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 512 }, - "ApproximateArrivalTimestamp": { - "target": "com.amazonaws.kinesis#Timestamp", + "smithy.api#pattern": "^arn:aws[-a-z0-9]*:iam::\\d{12}:role/[a-zA-Z_0-9+=,.@\\-_/]+$" + } + }, + "com.amazonaws.kinesis#S3CompressionType": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", "traits": { - "smithy.api#documentation": "

The approximate time that the record was inserted into the stream.

" + "smithy.api#enumValue": "NONE" } }, - "Data": { - "target": "com.amazonaws.kinesis#Data", + "GZIP": { + "target": "smithy.api#Unit", "traits": { - "smithy.api#documentation": "

The data blob. The data in the blob is both opaque and immutable to Kinesis Data\n Streams, which does not inspect, interpret, or change the data in the blob in any way.\n When the data blob (the payload before base64-encoding) is added to the partition key\n size, the total size must not exceed the maximum record size (10 MiB).

", + "smithy.api#enumValue": "GZIP" + } + }, + "ZSTD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ZSTD" + } + } + } + }, + "com.amazonaws.kinesis#S3DestinationConfiguration": { + "type": "structure", + "members": { + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", + "traits": { + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data. Valid range is 300 to 900 seconds (5 to 15 minutes). The default value is 300 seconds.

" + } + }, + "DeadLetterQueueS3Configuration": { + "target": "com.amazonaws.kinesis#DeadLetterQueueS3Configuration", + "traits": { + "smithy.api#documentation": "

The dead-letter queue configuration for records that cannot be delivered. Optional for general purpose Amazon S3 destinations. If not specified, it defaults to the destination bucket with an error prefix.

" + } + }, + "StorageConfiguration": { + "target": "com.amazonaws.kinesis#S3StorageConfiguration", + "traits": { + "smithy.api#documentation": "

The Amazon S3 storage configuration for the channel.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The configuration for delivery to a general purpose Amazon S3 bucket. Used in CreateChannel.

" + } + }, + "com.amazonaws.kinesis#S3DestinationDescription": { + "type": "structure", + "members": { + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", + "traits": { + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data.

", "smithy.api#required": {} } }, - "PartitionKey": { - "target": "com.amazonaws.kinesis#PartitionKey", + "DeadLetterQueueS3Configuration": { + "target": "com.amazonaws.kinesis#DeadLetterQueueS3Configuration", "traits": { - "smithy.api#documentation": "

Identifies which shard in the stream the data record is assigned to.

", + "smithy.api#documentation": "

The dead-letter queue configuration for records that cannot be delivered.

", "smithy.api#required": {} } }, - "EncryptionType": { - "target": "com.amazonaws.kinesis#EncryptionType", + "StorageConfiguration": { + "target": "com.amazonaws.kinesis#S3StorageConfiguration", "traits": { - "smithy.api#documentation": "

The encryption type used on the record. This parameter can be one of the following\n values:

\n " + "smithy.api#documentation": "

The Amazon S3 storage configuration for the channel.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

The unit of data of the Kinesis data stream, which is composed of a sequence number, a\n partition key, and a data blob.

" + "smithy.api#documentation": "

The configuration for delivery to a general purpose Amazon S3 bucket. Returned in ChannelDescription.

" } }, - "com.amazonaws.kinesis#RecordList": { - "type": "list", - "member": { - "target": "com.amazonaws.kinesis#Record" + "com.amazonaws.kinesis#S3DestinationUpdateInput": { + "type": "structure", + "members": { + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", + "traits": { + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data. Valid range is 300 to 900 seconds (5 to 15 minutes).

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The updated configuration for a general purpose Amazon S3 destination. Used in UpdateChannel. Only DataFreshnessInSeconds can be updated.

" } }, - "com.amazonaws.kinesis#RegisterStreamConsumer": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#RegisterStreamConsumerInput" - }, - "output": { - "target": "com.amazonaws.kinesis#RegisterStreamConsumerOutput" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" + "com.amazonaws.kinesis#S3ErrorOutputPrefix": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 512 }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" + "smithy.api#pattern": "^[0-9A-Za-z!\\-_'.*()\\/]+$" + } + }, + "com.amazonaws.kinesis#S3OutputKeyTemplate": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 1024 }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" + "smithy.api#pattern": "^[0-9A-Za-z!\\-_'.*()\\/=:{}]+$" + } + }, + "com.amazonaws.kinesis#S3StorageClass": { + "type": "enum", + "members": { + "STANDARD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "STANDARD" + } }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], - "traits": { - "smithy.api#documentation": "

Registers a consumer with a Kinesis data stream. When you use this operation, the\n consumer you register can then call SubscribeToShard to receive data\n from the stream using enhanced fan-out, at a rate of up to 2 MiB per second for every\n shard you subscribe to. This rate is unaffected by the total number of consumers that\n read from the same stream.

\n

You can add tags to the registered consumer when making a RegisterStreamConsumer request by setting the Tags parameter. If you pass the Tags parameter, in addition to having the kinesis:RegisterStreamConsumer permission, you must also have the kinesis:TagResource permission for the consumer that will be registered. Tags will take effect from the CREATING status of the consumer.

\n

With On-demand Advantage streams, you can register up to 50 consumers per stream to use Enhanced Fan-out. With On-demand Standard and Provisioned streams, you can register up to 20 consumers per stream to use Enhanced Fan-out. A given consumer can only be \n registered with one stream at a time.

\n

For an example of how to use this operation, see Enhanced Fan-Out\n Using the Kinesis Data Streams API.

\n

The use of this operation has a limit of five transactions per second per account.\n Also, only 5 consumers can be created simultaneously. In other words, you cannot have\n more than 5 consumers in a CREATING status at the same time. Registering a\n 6th consumer while there are 5 in a CREATING status results in a\n LimitExceededException.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" + "INTELLIGENT_TIERING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INTELLIGENT_TIERING" + } + }, + "GLACIER_IR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "GLACIER_IR" } } } }, - "com.amazonaws.kinesis#RegisterStreamConsumerInput": { + "com.amazonaws.kinesis#S3StorageConfiguration": { "type": "structure", "members": { - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "BucketARN": { + "target": "com.amazonaws.kinesis#BucketARN", "traits": { - "smithy.api#documentation": "

The ARN of the Kinesis data stream that you want to register the consumer with. For\n more info, see Amazon Resource Names (ARNs) and Amazon Web Services Service\n Namespaces.

", - "smithy.api#required": {}, - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the destination Amazon S3 bucket.

", + "smithy.api#required": {} } }, - "ConsumerName": { - "target": "com.amazonaws.kinesis#ConsumerName", + "ExpectedBucketOwner": { + "target": "com.amazonaws.kinesis#ExpectedBucketOwner", "traits": { - "smithy.api#documentation": "

For a given Kinesis data stream, each consumer must have a unique name. However,\n consumer names don't have to be unique across data streams.

", + "smithy.api#documentation": "

The Amazon Web Services account ID of the expected owner of the destination bucket. This value helps prevent delivery to an unintended bucket if ownership changes.

", "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "OutputKeyTemplate": { + "target": "com.amazonaws.kinesis#S3OutputKeyTemplate", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The template used to construct the Amazon S3 object key for delivered objects. If not specified, a default template is used.

" } }, - "Tags": { - "target": "com.amazonaws.kinesis#TagMap", + "StorageClass": { + "target": "com.amazonaws.kinesis#S3StorageClass", "traits": { - "smithy.api#documentation": "

A set of up to 50 key-value pairs. A tag consists of a required key and an optional value.

" + "smithy.api#documentation": "

The Amazon S3 storage class for delivered objects. Valid values:

\n " } - } - }, - "traits": { - "smithy.api#input": {} - } - }, - "com.amazonaws.kinesis#RegisterStreamConsumerOutput": { - "type": "structure", - "members": { - "Consumer": { - "target": "com.amazonaws.kinesis#Consumer", + }, + "CompressionType": { + "target": "com.amazonaws.kinesis#S3CompressionType", "traits": { - "smithy.api#documentation": "

An object that represents the details of the consumer you registered. When you\n register a consumer, it gets an ARN that is generated by Kinesis Data Streams.

", + "smithy.api#documentation": "

The compression applied to delivered objects. Valid values:

\n ", "smithy.api#required": {} } } }, "traits": { - "smithy.api#output": {} + "smithy.api#documentation": "

The Amazon S3 storage settings for a general purpose Amazon S3 destination.

" } }, - "com.amazonaws.kinesis#RemoveTagsFromStream": { - "type": "operation", - "input": { - "target": "com.amazonaws.kinesis#RemoveTagsFromStreamInput" - }, - "output": { - "target": "smithy.api#Unit" - }, - "errors": [ - { - "target": "com.amazonaws.kinesis#AccessDeniedException" - }, - { - "target": "com.amazonaws.kinesis#InvalidArgumentException" - }, - { - "target": "com.amazonaws.kinesis#LimitExceededException" + "com.amazonaws.kinesis#S3TablesCompressionType": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } }, - { - "target": "com.amazonaws.kinesis#ResourceInUseException" + "ZSTD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ZSTD" + } }, - { - "target": "com.amazonaws.kinesis#ResourceNotFoundException" - } - ], - "traits": { - "smithy.api#documentation": "

Removes tags from the specified Kinesis data stream. Removed tags are deleted and\n cannot be recovered after this operation successfully completes.

\n \n

When invoking this API, you must use either the StreamARN or the\n StreamName parameter, or both. It is recommended that you use the\n StreamARN input parameter when you invoke this API.

\n
\n

If you specify a tag that does not exist, it is ignored.

\n

\n RemoveTagsFromStream has a limit of five transactions per second per\n account.

", - "smithy.rules#staticContextParams": { - "OperationType": { - "value": "control" + "SNAPPY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SNAPPY" } } } }, - "com.amazonaws.kinesis#RemoveTagsFromStreamInput": { + "com.amazonaws.kinesis#S3TablesConfiguration": { "type": "structure", "members": { - "StreamName": { - "target": "com.amazonaws.kinesis#StreamName", + "TableBucketARN": { + "target": "com.amazonaws.kinesis#TableBucketARN", "traits": { - "smithy.api#documentation": "

The name of the stream.

" + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the Amazon S3 table bucket.

", + "smithy.api#required": {} } }, - "TagKeys": { - "target": "com.amazonaws.kinesis#TagKeyList", + "Namespace": { + "target": "com.amazonaws.kinesis#S3TablesNamespace", "traits": { - "smithy.api#documentation": "

A list of tag keys. Each corresponding tag is removed from the stream.

", + "smithy.api#documentation": "

The namespace (database) of the destination table.

", "smithy.api#required": {} } }, - "StreamARN": { - "target": "com.amazonaws.kinesis#StreamARN", + "TableName": { + "target": "com.amazonaws.kinesis#S3TablesTableName", "traits": { - "smithy.api#documentation": "

The ARN of the stream.

", - "smithy.rules#contextParam": { - "name": "StreamARN" - } + "smithy.api#documentation": "

The name of the destination table. Amazon Kinesis Data Streams creates this table in the specified table bucket.

", + "smithy.api#required": {} } }, - "StreamId": { - "target": "com.amazonaws.kinesis#StreamId", + "CompressionType": { + "target": "com.amazonaws.kinesis#S3TablesCompressionType", "traits": { - "smithy.api#documentation": "

Not Implemented. Reserved for future use.

", - "smithy.rules#contextParam": { - "name": "StreamId" - } + "smithy.api#documentation": "

The compression applied to Parquet data files. Valid values:

\n ", + "smithy.api#required": {} + } + }, + "PartitionSpec": { + "target": "com.amazonaws.kinesis#PartitionSpec", + "traits": { + "smithy.api#documentation": "

The partitioning specification for the destination table.

" } } }, "traits": { - "smithy.api#documentation": "

Represents the input for RemoveTagsFromStream.

", - "smithy.api#input": {} + "smithy.api#documentation": "

Specifies a destination streaming table on Apache Iceberg.

" } }, - "com.amazonaws.kinesis#ResourceARN": { - "type": "string", + "com.amazonaws.kinesis#S3TablesConfigurationList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#S3TablesConfiguration" + }, "traits": { "smithy.api#length": { "min": 1, - "max": 2048 + "max": 10000 + } + } + }, + "com.amazonaws.kinesis#S3TablesDestinationConfiguration": { + "type": "structure", + "members": { + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", + "traits": { + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data. Valid range is 300 to 900 seconds (5 to 15 minutes). The default value is 300 seconds.

" + } + }, + "DeadLetterQueueS3Configuration": { + "target": "com.amazonaws.kinesis#DeadLetterQueueS3Configuration", + "traits": { + "smithy.api#documentation": "

The dead-letter queue configuration for records that cannot be delivered. Required for streaming table destinations.

", + "smithy.api#required": {} + } }, - "smithy.api#pattern": "^arn:aws.*:kinesis:.*:\\d{12}:.*stream/\\S+$" + "S3TablesConfigurationList": { + "target": "com.amazonaws.kinesis#S3TablesConfigurationList", + "traits": { + "smithy.api#documentation": "

The list of streaming table configurations. Currently, one table is supported per channel.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

The configuration for delivery to streaming tables on Apache Iceberg. Used in CreateChannel.

" } }, - "com.amazonaws.kinesis#ResourceInUseException": { + "com.amazonaws.kinesis#S3TablesDestinationDescription": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data.

", + "smithy.api#required": {} + } + }, + "DeadLetterQueueS3Configuration": { + "target": "com.amazonaws.kinesis#DeadLetterQueueS3Configuration", + "traits": { + "smithy.api#documentation": "

The dead-letter queue configuration for records that cannot be delivered.

", + "smithy.api#required": {} + } + }, + "S3TablesConfigurationList": { + "target": "com.amazonaws.kinesis#S3TablesConfigurationList", + "traits": { + "smithy.api#documentation": "

The list of streaming table configurations.

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

The resource is not available for this operation. For successful operation, the\n resource must be in the ACTIVE state.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

The configuration for delivery to streaming tables on Apache Iceberg. Returned in ChannelDescription.

" } }, - "com.amazonaws.kinesis#ResourceNotFoundException": { + "com.amazonaws.kinesis#S3TablesDestinationUpdateInput": { "type": "structure", "members": { - "message": { - "target": "com.amazonaws.kinesis#ErrorMessage", + "DataFreshnessInSeconds": { + "target": "com.amazonaws.kinesis#DataFreshnessInSeconds", "traits": { - "smithy.api#documentation": "

A message that provides information about the error.

" + "smithy.api#documentation": "

The maximum age, in seconds, of undelivered data. Valid range is 300 to 900 seconds (5 to 15 minutes).

", + "smithy.api#required": {} } } }, "traits": { - "smithy.api#documentation": "

The requested resource could not be found. The stream might not be specified\n correctly.

", - "smithy.api#error": "client" + "smithy.api#documentation": "

The updated configuration for a streaming table destination. Used in UpdateChannel. Only DataFreshnessInSeconds can be updated.

" } }, - "com.amazonaws.kinesis#RetentionPeriodHours": { - "type": "integer" + "com.amazonaws.kinesis#S3TablesNamespace": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 255 + }, + "smithy.api#pattern": "^[0-9a-z_]+$" + } + }, + "com.amazonaws.kinesis#S3TablesTableName": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 255 + }, + "smithy.api#pattern": "^[0-9a-z_]+$" + } }, "com.amazonaws.kinesis#ScalingType": { "type": "enum", @@ -11723,7 +15562,7 @@ "com.amazonaws.kinesis#SequenceNumber": { "type": "string", "traits": { - "smithy.api#pattern": "^0|([1-9]\\d{0,128})$" + "smithy.api#pattern": "^(0|([1-9]\\d{0,128}))$" } }, "com.amazonaws.kinesis#SequenceNumberRange": { @@ -12405,12 +16244,51 @@ "traits": { "smithy.api#documentation": "

The maximum record size of a single record in kibibyte (KiB) that you can write to, and read from a stream.

" } + }, + "ChannelCount": { + "target": "com.amazonaws.kinesis#ChannelCountObject", + "traits": { + "smithy.api#documentation": "

The number of channels associated with the stream.

" + } } }, "traits": { "smithy.api#documentation": "

Represents the output for DescribeStreamSummary\n

" } }, + "com.amazonaws.kinesis#StreamFilter": { + "type": "structure", + "members": { + "StreamARN": { + "target": "com.amazonaws.kinesis#StreamARN", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the source stream to filter by.

", + "smithy.api#required": {} + } + }, + "StreamCreationTimestamp": { + "target": "com.amazonaws.kinesis#Timestamp", + "traits": { + "smithy.api#documentation": "

The creation timestamp of the source stream.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Filters ListChannels results by source stream.

" + } + }, + "com.amazonaws.kinesis#StreamFilterList": { + "type": "list", + "member": { + "target": "com.amazonaws.kinesis#StreamFilter" + }, + "traits": { + "smithy.api#length": { + "min": 1, + "max": 10000 + } + } + }, "com.amazonaws.kinesis#StreamId": { "type": "string", "traits": { @@ -12554,6 +16432,9 @@ { "target": "com.amazonaws.kinesis#AccessDeniedException" }, + { + "target": "com.amazonaws.kinesis#DryRunOperationException" + }, { "target": "com.amazonaws.kinesis#InvalidArgumentException" }, @@ -12691,6 +16572,12 @@ "smithy.api#documentation": "

The starting position in the data stream from which to start streaming.

", "smithy.api#required": {} } + }, + "DryRun": { + "target": "com.amazonaws.kinesis#BooleanObject", + "traits": { + "smithy.api#documentation": "

Checks if your request will succeed. DryRun is an optional\n parameter.

" + } } }, "traits": { @@ -12712,6 +16599,16 @@ "smithy.api#output": {} } }, + "com.amazonaws.kinesis#TableBucketARN": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + }, + "smithy.api#pattern": "^arn:aws[-a-z0-9]*:s3tables:[-a-z0-9]+:\\d{12}:bucket/[a-z0-9_-]{3,63}$" + } + }, "com.amazonaws.kinesis#Tag": { "type": "structure", "members": { @@ -12986,6 +16883,157 @@ "smithy.api#output": {} } }, + "com.amazonaws.kinesis#UpdateChannel": { + "type": "operation", + "input": { + "target": "com.amazonaws.kinesis#UpdateChannelInput" + }, + "output": { + "target": "com.amazonaws.kinesis#UpdateChannelOutput" + }, + "errors": [ + { + "target": "com.amazonaws.kinesis#AccessDeniedException" + }, + { + "target": "com.amazonaws.kinesis#InvalidArgumentException" + }, + { + "target": "com.amazonaws.kinesis#LimitExceededException" + }, + { + "target": "com.amazonaws.kinesis#ResourceInUseException" + }, + { + "target": "com.amazonaws.kinesis#ResourceNotFoundException" + }, + { + "target": "com.amazonaws.kinesis#ValidationException" + } + ], + "traits": { + "smithy.api#documentation": "

Updates the data freshness interval or the Amazon CloudWatch Logs configuration of an existing channel. You cannot change the destination, source stream, record format, schema, encryption configuration, or service execution role of an existing channel. To change any other setting, delete the channel and create a new one.

\n

Updating a channel is an asynchronous operation. Upon receiving the request, Amazon Kinesis Data Streams sets the channel to the UPDATING state and returns immediately. After the change is applied, Amazon Kinesis Data Streams sets the channel back to the ACTIVE state.

\n

This operation has a call limit of 5 transactions per second (TPS) for each Amazon Web Services account. Exceeding 5 TPS results in a LimitExceededException.

", + "smithy.api#examples": [ + { + "title": "To update a channel", + "input": { + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "S3DestinationConfiguration": { + "DataFreshnessInSeconds": 600 + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" + } + } + }, + "output": { + "ChannelDescription": { + "ChannelName": "my-channel-name", + "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/my-channel-id", + "ChannelId": "my-channel-id", + "ChannelStatus": "UPDATING", + "ChannelCreationTimestamp": "2024-07-02T00:00:00Z", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/my-channel-role", + "StreamConfigurationList": [ + { + "StreamARN": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream-name", + "StreamCreationTimestamp": "2024-07-01T00:00:00Z", + "RecordConfiguration": { + "RecordFormatType": "JSON" + } + } + ], + "S3DestinationConfiguration": { + "DataFreshnessInSeconds": 600, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::my-channel-dlq-bucket", + "ExpectedBucketOwner": "123456789012", + "ErrorOutputPrefix": "kinesis-channel/errors/my-channel/my-channel-id/" + }, + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::my-channel-bucket", + "ExpectedBucketOwner": "123456789012", + "OutputKeyTemplate": "kinesis-channel/!{channel-name}/!{channel-id}/!{yyyy}/!{MM}/!{dd}/!{HH}/!{channel-name}-!{channel-id}-!{yyyy}-!{MM}-!{dd}-!{HH}-!{mm}!{extension}", + "StorageClass": "STANDARD", + "CompressionType": "ZSTD" + } + }, + "EncryptionConfiguration": { + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab" + }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/my-channel", + "LogStreamName": "my-channel-log-stream" + } + } + } + } + } + ], + "smithy.rules#staticContextParams": { + "OperationType": { + "value": "control" + } + } + } + }, + "com.amazonaws.kinesis#UpdateChannelInput": { + "type": "structure", + "members": { + "ChannelARN": { + "target": "com.amazonaws.kinesis#ChannelARN", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the channel to update.

", + "smithy.api#required": {}, + "smithy.rules#contextParam": { + "name": "ChannelARN" + } + } + }, + "S3DestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3DestinationUpdateInput", + "traits": { + "smithy.api#documentation": "

The updated configuration for a general purpose Amazon S3 destination. Only DataFreshnessInSeconds can be updated.

" + } + }, + "S3TablesDestinationConfiguration": { + "target": "com.amazonaws.kinesis#S3TablesDestinationUpdateInput", + "traits": { + "smithy.api#documentation": "

The updated configuration for a streaming table destination. Only DataFreshnessInSeconds can be updated.

" + } + }, + "LoggingConfiguration": { + "target": "com.amazonaws.kinesis#ChannelLoggingUpdateInput", + "traits": { + "smithy.api#documentation": "

The updated Amazon CloudWatch Logs configuration for the channel.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.kinesis#UpdateChannelOutput": { + "type": "structure", + "members": { + "ChannelDescription": { + "target": "com.amazonaws.kinesis#ChannelDescription", + "traits": { + "smithy.api#documentation": "

The configuration and current status of the updated channel.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, "com.amazonaws.kinesis#UpdateMaxRecordSize": { "type": "operation", "input": { diff --git a/aws-models/lambda.json b/aws-models/lambda.json index 3d933812d..5d3c9b6fb 100644 --- a/aws-models/lambda.json +++ b/aws-models/lambda.json @@ -6816,7 +6816,7 @@ }, "smithy.api#idempotent": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -6830,7 +6830,7 @@ "smithy.api#httpLabel": {}, "smithy.api#required": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -6840,7 +6840,7 @@ "smithy.api#documentation": "

The revision ID that the existing policy must match for the deletion to proceed. If the revision ID doesn't match, the operation fails with a PreconditionFailedException error. To retrieve the current revision ID, use the GetResourcePolicy operation.

", "smithy.api#httpQuery": "RevisionId", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } } @@ -6848,7 +6848,7 @@ "traits": { "smithy.api#input": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -6906,6 +6906,43 @@ ] } }, + "com.amazonaws.lambda#DirectS3Read": { + "type": "enum", + "members": { + "ENABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ENABLED", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + }, + "DISABLED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DISABLED", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + }, + "AUTO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AUTO", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + } + }, + "traits": { + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + }, "com.amazonaws.lambda#DocumentDBEventSourceConfig": { "type": "structure", "members": { @@ -6952,7 +6989,7 @@ "traits": { "smithy.api#documentation": "

The ARN of the Key Management Service (KMS) customer managed key that is used to encrypt your durable execution's payload data, including input, output, and error payloads.

", "smithy.api#tags": [ - "feature:dar-cmkms" + "feature:public" ] } }, @@ -9249,10 +9286,19 @@ "feature:public" ] } + }, + "S3FilesConfig": { + "target": "com.amazonaws.lambda#S3FilesConfig", + "traits": { + "smithy.api#documentation": "

The configuration for how your function accesses data on an Amazon S3 file system. Valid only when the file system access point ARN is an Amazon S3 Files access point. If you specify a different access point type (for example, Amazon Elastic File System), the operation returns an InvalidParameterException.

", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } } }, "traits": { - "smithy.api#documentation": "

Details about the connection between a Lambda function and an Amazon EFS file system or an Amazon S3 Files file system.

", + "smithy.api#documentation": "

Details about the connection between a Lambda function and an Amazon EFS file system or an Amazon S3 file system.

", "smithy.api#tags": [ "feature:public" ] @@ -10033,7 +10079,7 @@ "FileSystemConfigs": { "target": "com.amazonaws.lambda#FileSystemConfigList", "traits": { - "smithy.api#documentation": "

Connection settings for an Amazon EFS file system or an Amazon S3 Files file system.

", + "smithy.api#documentation": "

Connection settings for an Amazon EFS file system or an Amazon S3 file system.

", "smithy.api#tags": [ "feature:public" ] @@ -13532,7 +13578,7 @@ }, "smithy.api#readonly": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -13546,7 +13592,7 @@ "smithy.api#httpLabel": {}, "smithy.api#required": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } } @@ -13554,7 +13600,7 @@ "traits": { "smithy.api#input": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -13566,7 +13612,7 @@ "traits": { "smithy.api#documentation": "

The resource-based policy attached to the Lambda resource you specified.

", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -13575,7 +13621,7 @@ "traits": { "smithy.api#documentation": "

The revision ID of the policy. Pass this value as the RevisionId in a PutResourcePolicy or DeleteResourcePolicy request. Doing so ensures the operation acts on the expected version of the policy.

", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } } @@ -13583,7 +13629,7 @@ "traits": { "smithy.api#output": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -19074,7 +19120,7 @@ "min": 1, "max": 256 }, - "smithy.api#pattern": "^(arn:(aws[a-zA-Z-]*)?:lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:|(((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?))(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$", + "smithy.api#pattern": "^(arn:(aws[a-zA-Z-]*)?:lambda:)?((eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_]+))?$", "smithy.api#tags": [ "feature:public" ] @@ -19914,7 +19960,7 @@ }, "smithy.api#pattern": "^arn:(aws[a-zA-Z-]*)?:lambda:(eusc-)?[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:function:[a-zA-Z0-9-_]+(:(\\$LATEST(\\.PUBLISHED)?|[a-zA-Z0-9-_])+)?$", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21493,7 +21539,7 @@ }, "smithy.api#idempotent": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21507,7 +21553,7 @@ "smithy.api#httpLabel": {}, "smithy.api#required": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21517,7 +21563,7 @@ "smithy.api#documentation": "

The policy document you want to add to your Lambda resource. This is formatted as a JSON string.

For more information, see Working with resource-based policies in Lambda in the Lambda Developer Guide.

", "smithy.api#required": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21526,7 +21572,7 @@ "traits": { "smithy.api#documentation": "

The revision ID that the existing policy must match for the replacement to proceed. If the revision ID doesn't match, the operation fails with a PreconditionFailedException error. To retrieve the current revision ID, use the GetResourcePolicy operation.

", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } } @@ -21534,7 +21580,7 @@ "traits": { "smithy.api#input": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21546,7 +21592,7 @@ "traits": { "smithy.api#documentation": "

The resource-based policy that Lambda adds to the resource.

", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -21555,7 +21601,7 @@ "traits": { "smithy.api#documentation": "

The revision ID of the policy that Lambda adds to your Lambda resource.

", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } } @@ -21563,7 +21609,7 @@ "traits": { "smithy.api#output": {}, "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -22268,7 +22314,7 @@ }, "smithy.api#pattern": "^[\\s\\S]+$", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -22359,7 +22405,7 @@ }, "smithy.api#pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "smithy.api#tags": [ - "feature:rbp" + "feature:public" ] } }, @@ -22816,7 +22862,7 @@ "traits": { "smithy.api#enumValue": "java8.al2023", "smithy.api#tags": [ - "feature:java-al2023" + "feature:public" ] } }, @@ -22825,7 +22871,7 @@ "traits": { "smithy.api#enumValue": "java11.al2023", "smithy.api#tags": [ - "feature:java-al2023" + "feature:public" ] } }, @@ -22834,7 +22880,7 @@ "traits": { "smithy.api#enumValue": "java17.al2023", "smithy.api#tags": [ - "feature:java-al2023" + "feature:public" ] } } @@ -22929,6 +22975,26 @@ ] } }, + "com.amazonaws.lambda#S3FilesConfig": { + "type": "structure", + "members": { + "DirectS3Read": { + "target": "com.amazonaws.lambda#DirectS3Read", + "traits": { + "smithy.api#documentation": "

Specifies if a function reads from the file system for the lowest latency, or through Amazon S3 Files feature \"direct Amazon S3 bucket reads\" for the highest throughput. Valid values:

To use direct reads, you must grant the execution role the s3:GetObject and s3:GetObjectVersion permissions. If a direct read fails, Lambda automatically falls back to reading through the file system.

", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + } + }, + "traits": { + "smithy.api#documentation": "

Setting controls how your function accesses data from an Amazon S3 file system.

", + "smithy.api#tags": [ + "feature:s3files-rbp" + ] + } + }, "com.amazonaws.lambda#S3FilesMountConnectivityException": { "type": "structure", "members": { diff --git a/aws-models/mediaconvert.json b/aws-models/mediaconvert.json index 88af1db5a..a01d8ce33 100644 --- a/aws-models/mediaconvert.json +++ b/aws-models/mediaconvert.json @@ -145,6 +145,26 @@ "smithy.api#documentation": "Choose the loudness measurement mode for your audio content. For music or advertisements: We recommend that you keep the default value, Program. For speech or other content: We recommend that you choose Anchor. When you do, MediaConvert optimizes the loudness of your output for clarify by applying speech gates." } }, + "com.amazonaws.mediaconvert#AacPassthroughControl": { + "type": "enum", + "members": { + "WHEN_POSSIBLE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WHEN_POSSIBLE" + } + }, + "NO_PASSTHROUGH": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NO_PASSTHROUGH" + } + } + }, + "traits": { + "smithy.api#documentation": "When set to WHEN_POSSIBLE, input AAC audio will be passed through if it is present on the input. This detection is dynamic over the life of the transcode. Inputs that alternate between AAC and non-AAC content will have a consistent AAC output as the system alternates between passthrough and encoding." + } + }, "com.amazonaws.mediaconvert#AacRateControlMode": { "type": "enum", "members": { @@ -223,6 +243,13 @@ "smithy.api#jsonName": "loudnessMeasurementMode" } }, + "PassthroughControl": { + "target": "com.amazonaws.mediaconvert#AacPassthroughControl", + "traits": { + "smithy.api#documentation": "When set to WHEN_POSSIBLE, input AAC audio will be passed through if it is present on the input. This detection is dynamic over the life of the transcode. Inputs that alternate between AAC and non-AAC content will have a consistent AAC output as the system alternates between passthrough and encoding.", + "smithy.api#jsonName": "passthroughControl" + } + }, "RapInterval": { "target": "com.amazonaws.mediaconvert#__integerMin2000Max30000", "traits": { @@ -1226,6 +1253,28 @@ "smithy.api#documentation": "The anti-alias filter is automatically applied to all outputs. The service no longer accepts the value DISABLED for AntiAlias. If you specify that in your job, the service will ignore the setting." } }, + "com.amazonaws.mediaconvert#AspectRatio": { + "type": "structure", + "members": { + "Denominator": { + "target": "com.amazonaws.mediaconvert#__integer", + "traits": { + "smithy.api#documentation": "The denominator, or bottom number, in the fractional aspect ratio. For example, for a display aspect ratio of 16 / 9, the denominator would be 9.", + "smithy.api#jsonName": "denominator" + } + }, + "Numerator": { + "target": "com.amazonaws.mediaconvert#__integer", + "traits": { + "smithy.api#documentation": "The numerator, or top number, in the fractional aspect ratio. For example, for a display aspect ratio of 16 / 9, the numerator would be 16.", + "smithy.api#jsonName": "numerator" + } + } + }, + "traits": { + "smithy.api#documentation": "An aspect ratio expressed as a fraction with numerator and denominator values, reduced to lowest terms. Used for the sample (pixel) aspect ratio and the display aspect ratio of a video track. For example, a 720x576 anamorphic track has a sample aspect ratio of 64 / 45 and a display aspect ratio of 16 / 9." + } + }, "com.amazonaws.mediaconvert#AssociateCertificate": { "type": "operation", "input": { @@ -1571,7 +1620,7 @@ } }, "traits": { - "smithy.api#documentation": "Choose the audio codec for this output. Note that the option Dolby Digital passthrough applies only to Dolby Digital and Dolby Digital Plus audio inputs. Make sure that you choose a codec that's supported with your output container: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#reference-codecs-containers-output-audio For audio-only outputs, make sure that both your input audio codec and your output audio codec are supported for audio-only workflows. For more information, see: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers-input.html#reference-codecs-containers-input-audio-only and https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#audio-only-output" + "smithy.api#documentation": "Choose the audio codec for this output. Note that the option passthrough applies only to Dolby Digital, Dolby Digital Plus, AAC LC, AAC HEV1, and AAC HEV2 audio inputs. Make sure that you choose a codec that's supported with your output container: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#reference-codecs-containers-output-audio For audio-only outputs, make sure that both your input audio codec and your output audio codec are supported for audio-only workflows. For more information, see: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers-input.html#reference-codecs-containers-input-audio-only and https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#audio-only-output" } }, "com.amazonaws.mediaconvert#AudioCodecSettings": { @@ -1608,7 +1657,7 @@ "Codec": { "target": "com.amazonaws.mediaconvert#AudioCodec", "traits": { - "smithy.api#documentation": "Choose the audio codec for this output. Note that the option Dolby Digital passthrough applies only to Dolby Digital and Dolby Digital Plus audio inputs. Make sure that you choose a codec that's supported with your output container: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#reference-codecs-containers-output-audio For audio-only outputs, make sure that both your input audio codec and your output audio codec are supported for audio-only workflows. For more information, see: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers-input.html#reference-codecs-containers-input-audio-only and https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#audio-only-output", + "smithy.api#documentation": "Choose the audio codec for this output. Note that the option passthrough applies only to Dolby Digital, Dolby Digital Plus, AAC LC, AAC HEV1, and AAC HEV2 audio inputs. Make sure that you choose a codec that's supported with your output container: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#reference-codecs-containers-output-audio For audio-only outputs, make sure that both your input audio codec and your output audio codec are supported for audio-only workflows. For more information, see: https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers-input.html#reference-codecs-containers-input-audio-only and https://docs.aws.amazon.com/mediaconvert/latest/ug/reference-codecs-containers.html#audio-only-output", "smithy.api#jsonName": "codec" } }, @@ -2024,6 +2073,13 @@ "smithy.api#jsonName": "bitRate" } }, + "ChannelLayout": { + "target": "com.amazonaws.mediaconvert#__string", + "traits": { + "smithy.api#documentation": "The audio channel layout of the track, such as \"mono\", \"stereo\", \"5.1\", or \"7.1\". Object-based or immersive audio is reported as \"5.1.4\" or \"7.1.4\".", + "smithy.api#jsonName": "channelLayout" + } + }, "Channels": { "target": "com.amazonaws.mediaconvert#__integer", "traits": { @@ -5251,10 +5307,16 @@ "traits": { "smithy.api#enumValue": "NONE" } + }, + "MANIFEST_CUES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MANIFEST_CUES" + } } }, "traits": { - "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output." + "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest." } }, "com.amazonaws.mediaconvert#CmfcSettings": { @@ -5340,7 +5402,7 @@ "Scte35Source": { "target": "com.amazonaws.mediaconvert#CmfcScte35Source", "traits": { - "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output.", + "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest.", "smithy.api#jsonName": "scte35Source" } }, @@ -5445,6 +5507,12 @@ "smithy.api#enumValue": "AC3" } }, + "AMR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AMR" + } + }, "EAC3": { "target": "smithy.api#Unit", "traits": { @@ -5487,6 +5555,24 @@ "smithy.api#enumValue": "VORBIS" } }, + "WMA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WMA" + } + }, + "WMA2": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WMA2" + } + }, + "WMAPRO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WMAPRO" + } + }, "AV1": { "target": "smithy.api#Unit", "traits": { @@ -5499,6 +5585,18 @@ "smithy.api#enumValue": "AVC" } }, + "DV": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DV" + } + }, + "H263": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "H263" + } + }, "HEVC": { "target": "smithy.api#Unit", "traits": { @@ -5559,6 +5657,18 @@ "smithy.api#enumValue": "UNCOMPRESSED" } }, + "VC1": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VC1" + } + }, + "VC3": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VC3" + } + }, "VFW": { "target": "smithy.api#Unit", "traits": { @@ -5642,6 +5752,13 @@ "smithy.api#jsonName": "fieldOrder" } }, + "Hdr10PlusPresence": { + "target": "com.amazonaws.mediaconvert#Hdr10PlusPresence", + "traits": { + "smithy.api#documentation": "Indicates that HDR10+ (SMPTE ST 2094-40) dynamic metadata was detected in the HEVC bitstream. Present only when detected.", + "smithy.api#jsonName": "hdr10PlusPresence" + } + }, "Height": { "target": "com.amazonaws.mediaconvert#__integer", "traits": { @@ -6137,7 +6254,7 @@ "Format": { "target": "com.amazonaws.mediaconvert#Format", "traits": { - "smithy.api#documentation": "The format of your media file. For example: MP4, QuickTime (MOV), Matroska (MKV), WebM, MXF, Wave, AVI, MPEG-TS, MPEG-PS, or MP3. Note that this will be blank if your media file has a format that the MediaConvert Probe operation does not recognize.", + "smithy.api#documentation": "The format of your media file. For example: MP4, QuickTime (MOV), Matroska (MKV), WebM, MXF, Wave, AVI, MPEG-TS, MPEG-PS, MP3, FLAC, ASF (Windows Media / WMA), OGG. Note that this will be blank if your media file has a format that the MediaConvert Probe operation does not recognize.", "smithy.api#jsonName": "format" } }, @@ -6992,7 +7109,7 @@ "PlaybackDeviceCompatibility": { "target": "com.amazonaws.mediaconvert#DashIsoPlaybackDeviceCompatibility", "traits": { - "smithy.api#documentation": "This setting can improve the compatibility of your output with video players on obsolete devices. It applies only to DASH H.264 outputs with DRM encryption. Choose Unencrypted SEI only to correct problems with playback on older devices. Otherwise, keep the default setting CENC v1. If you choose Unencrypted SEI, for that output, the service will exclude the access unit delimiter and will leave the SEI NAL units unencrypted.", + "smithy.api#documentation": "This setting can improve the compatibility of your output with video players on obsolete devices. It applies only to DASH outputs with DRM encryption. Choose Unencrypted SEI only to correct problems with playback on older H.264 devices. Choose CENC v1 unencrypted headers to leave NAL unit headers and slice headers unencrypted for H.265 outputs, improving compatibility with strict HEVC decoders. Otherwise, keep the default setting CENC v1.", "smithy.api#jsonName": "playbackDeviceCompatibility" } }, @@ -7435,10 +7552,16 @@ "traits": { "smithy.api#enumValue": "UNENCRYPTED_SEI" } + }, + "CENC_V1_UNENCRYPTED_HEADERS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CENC_V1_UNENCRYPTED_HEADERS" + } } }, "traits": { - "smithy.api#documentation": "This setting can improve the compatibility of your output with video players on obsolete devices. It applies only to DASH H.264 outputs with DRM encryption. Choose Unencrypted SEI only to correct problems with playback on older devices. Otherwise, keep the default setting CENC v1. If you choose Unencrypted SEI, for that output, the service will exclude the access unit delimiter and will leave the SEI NAL units unencrypted." + "smithy.api#documentation": "This setting can improve the compatibility of your output with video players on obsolete devices. It applies only to DASH outputs with DRM encryption. Choose Unencrypted SEI only to correct problems with playback on older H.264 devices. Choose CENC v1 unencrypted headers to leave NAL unit headers and slice headers unencrypted for H.265 outputs, improving compatibility with strict HEVC decoders. Otherwise, keep the default setting CENC v1." } }, "com.amazonaws.mediaconvert#DashIsoPtsOffsetHandlingForBFrames": { @@ -10714,6 +10837,24 @@ "traits": { "smithy.api#enumValue": "mp3" } + }, + "flac": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "flac" + } + }, + "asf": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "asf" + } + }, + "ogg": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ogg" + } } } }, @@ -13840,6 +13981,20 @@ "smithy.api#documentation": "Setting for HDR10+ metadata insertion" } }, + "com.amazonaws.mediaconvert#Hdr10PlusPresence": { + "type": "enum", + "members": { + "PRESENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PRESENT" + } + } + }, + "traits": { + "smithy.api#documentation": "Indicates that HDR10+ (SMPTE ST 2094-40) dynamic metadata was detected in the HEVC bitstream. Present only when detected." + } + }, "com.amazonaws.mediaconvert#HdrMetadata": { "type": "structure", "members": { @@ -16332,7 +16487,7 @@ "FollowSource": { "target": "com.amazonaws.mediaconvert#__integerMin1Max150", "traits": { - "smithy.api#documentation": "Specify the input that MediaConvert references for your default output settings. MediaConvert uses this input's Resolution, Frame rate, and Pixel aspect ratio for all outputs that you don't manually specify different output settings for. Enabling this setting will disable \"Follow source\" for all other inputs. If MediaConvert cannot follow your source, for example if you specify an audio-only input, MediaConvert uses the first followable input instead. In your JSON job specification, enter an integer from 1 to 150 corresponding to the order of your inputs.", + "smithy.api#documentation": "Specify the input that MediaConvert references for your default output settings. MediaConvert uses this input's Resolution, Frame rate, and Pixel aspect ratio for all outputs that you don't manually specify different output settings for. Enabling this setting will disable \"Follow source\" for all other inputs. If MediaConvert cannot follow your source, for example if you specify an audio-only input, MediaConvert uses the first followable input instead. In your JSON job specification, enter an integer from 1 to 150 corresponding to the order of your inputs.", "smithy.api#jsonName": "followSource" } }, @@ -16605,7 +16760,7 @@ "FollowSource": { "target": "com.amazonaws.mediaconvert#__integerMin1Max150", "traits": { - "smithy.api#documentation": "Specify the input that MediaConvert references for your default output settings. MediaConvert uses this input's Resolution, Frame rate, and Pixel aspect ratio for all outputs that you don't manually specify different output settings for. Enabling this setting will disable \"Follow source\" for all other inputs. If MediaConvert cannot follow your source, for example if you specify an audio-only input, MediaConvert uses the first followable input instead. In your JSON job specification, enter an integer from 1 to 150 corresponding to the order of your inputs.", + "smithy.api#documentation": "Specify the input that MediaConvert references for your default output settings. MediaConvert uses this input's Resolution, Frame rate, and Pixel aspect ratio for all outputs that you don't manually specify different output settings for. Enabling this setting will disable \"Follow source\" for all other inputs. If MediaConvert cannot follow your source, for example if you specify an audio-only input, MediaConvert uses the first followable input instead. In your JSON job specification, enter an integer from 1 to 150 corresponding to the order of your inputs.", "smithy.api#jsonName": "followSource" } }, @@ -16676,7 +16831,7 @@ "Key": { "target": "com.amazonaws.mediaconvert#JobsQueryFilterKey", "traits": { - "smithy.api#documentation": "Specify job details to filter for while performing a jobs query. You specify these filters as part of a key-value pair within the JobsQueryFilter array. The following list describes which keys are available and their possible values: * queue - Your Queue's name or ARN. * status - Your job's status. (SUBMITTED | PROGRESSING | COMPLETE | CANCELED | ERROR) * fileInput - Your input file URL, or partial input file name. * jobEngineVersionRequested - The Job engine version that you requested for your job. Valid versions are in a YYYY-MM-DD format. * jobEngineVersionUsed - The Job engine version that your job used. This may differ from the version that you requested. Valid versions are in a YYYY-MM-DD format. * audioCodec - Your output's audio codec. (AAC | MP2 | MP3 | WAV | AIFF | AC3| EAC3 | EAC3_ATMOS | VORBIS | OPUS | PASSTHROUGH | FLAC) * videoCodec - Your output's video codec. (AV1 | AVC_INTRA | FRAME_CAPTURE | H_264 | H_265 | MPEG2 | PASSTHROUGH | PRORES | UNCOMPRESSED | VC3 | VP8 | VP9 | XAVC)", + "smithy.api#documentation": "Specify job details to filter for while performing a jobs query. You specify these filters as part of a key-value pair within the JobsQueryFilter array. The following list describes which keys are available and their possible values: * queue - Your Queue's name or ARN. * status - Your job's status. (SUBMITTED | PROGRESSING | COMPLETE | CANCELED | ERROR) * fileInput - Your input file URL, or partial input file name. * jobEngineVersionRequested - The Job engine version that you requested for your job. Valid versions are in a YYYY-MM-DD format. * jobEngineVersionUsed - The Job engine version that your job used. This may differ from the version that you requested. Valid versions are in a YYYY-MM-DD format. * audioCodec - Your output's audio codec. (AAC | MP2 | MP3 | WAV | AIFF | AC3| EAC3 | EAC3_ATMOS | VORBIS | OPUS | PASSTHROUGH | FLAC) * videoCodec - Your output's video codec. (AV1 | AVC_INTRA | FRAME_CAPTURE | H_264 | H_265 | MPEG2 | PASSTHROUGH | PRORES | UNCOMPRESSED | VC3 | VP8 | VP9 | XAVC) * errorCode - The error code that your job failed with. For example, 1010. For more information, see https://docs.aws.amazon.com/mediaconvert/latest/ug/mediaconvert_error_codes.html", "smithy.api#jsonName": "key" } }, @@ -16736,10 +16891,16 @@ "traits": { "smithy.api#enumValue": "videoCodec" } + }, + "errorCode": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "errorCode" + } } }, "traits": { - "smithy.api#documentation": "Specify job details to filter for while performing a jobs query. You specify these filters as part of a key-value pair within the JobsQueryFilter array. The following list describes which keys are available and their possible values: * queue - Your Queue's name or ARN. * status - Your job's status. (SUBMITTED | PROGRESSING | COMPLETE | CANCELED | ERROR) * fileInput - Your input file URL, or partial input file name. * jobEngineVersionRequested - The Job engine version that you requested for your job. Valid versions are in a YYYY-MM-DD format. * jobEngineVersionUsed - The Job engine version that your job used. This may differ from the version that you requested. Valid versions are in a YYYY-MM-DD format. * audioCodec - Your output's audio codec. (AAC | MP2 | MP3 | WAV | AIFF | AC3| EAC3 | EAC3_ATMOS | VORBIS | OPUS | PASSTHROUGH | FLAC) * videoCodec - Your output's video codec. (AV1 | AVC_INTRA | FRAME_CAPTURE | H_264 | H_265 | MPEG2 | PASSTHROUGH | PRORES | UNCOMPRESSED | VC3 | VP8 | VP9 | XAVC)" + "smithy.api#documentation": "Specify job details to filter for while performing a jobs query. You specify these filters as part of a key-value pair within the JobsQueryFilter array. The following list describes which keys are available and their possible values: * queue - Your Queue's name or ARN. * status - Your job's status. (SUBMITTED | PROGRESSING | COMPLETE | CANCELED | ERROR) * fileInput - Your input file URL, or partial input file name. * jobEngineVersionRequested - The Job engine version that you requested for your job. Valid versions are in a YYYY-MM-DD format. * jobEngineVersionUsed - The Job engine version that your job used. This may differ from the version that you requested. Valid versions are in a YYYY-MM-DD format. * audioCodec - Your output's audio codec. (AAC | MP2 | MP3 | WAV | AIFF | AC3| EAC3 | EAC3_ATMOS | VORBIS | OPUS | PASSTHROUGH | FLAC) * videoCodec - Your output's video codec. (AV1 | AVC_INTRA | FRAME_CAPTURE | H_264 | H_265 | MPEG2 | PASSTHROUGH | PRORES | UNCOMPRESSED | VC3 | VP8 | VP9 | XAVC) * errorCode - The error code that your job failed with. For example, 1010. For more information, see https://docs.aws.amazon.com/mediaconvert/latest/ug/mediaconvert_error_codes.html" } }, "com.amazonaws.mediaconvert#JobsQueryStatus": { @@ -18934,10 +19095,16 @@ "traits": { "smithy.api#enumValue": "NONE" } + }, + "MANIFEST_CUES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MANIFEST_CUES" + } } }, "traits": { - "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None. Also provide the ESAM XML as a string in the setting Signal processing notification XML. Also enable ESAM SCTE-35 (include the property scte35Esam)." + "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest. For SCTE-35 markers from an ESAM XML document-- Choose None. Also provide the ESAM XML as a string in the setting Signal processing notification XML. Also enable ESAM SCTE-35 (include the property scte35Esam)." } }, "com.amazonaws.mediaconvert#M2tsSegmentationMarkers": { @@ -19262,7 +19429,7 @@ "Scte35Source": { "target": "com.amazonaws.mediaconvert#M2tsScte35Source", "traits": { - "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None. Also provide the ESAM XML as a string in the setting Signal processing notification XML. Also enable ESAM SCTE-35 (include the property scte35Esam).", + "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest. For SCTE-35 markers from an ESAM XML document-- Choose None. Also provide the ESAM XML as a string in the setting Signal processing notification XML. Also enable ESAM SCTE-35 (include the property scte35Esam).", "smithy.api#jsonName": "scte35Source" } }, @@ -19407,10 +19574,16 @@ "traits": { "smithy.api#enumValue": "NONE" } + }, + "MANIFEST_CUES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MANIFEST_CUES" + } } }, "traits": { - "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None if you don't want manifest conditioning. Choose Passthrough and choose Ad markers if you do want manifest conditioning. In both cases, also provide the ESAM XML as a string in the setting Signal processing notification XML." + "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None if you don't want manifest conditioning. Choose Passthrough and choose Ad markers if you do want manifest conditioning. In both cases, also provide the ESAM XML as a string in the setting Signal processing notification XML. For SCTE-35 markers from your input HLS manifest-- Choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest." } }, "com.amazonaws.mediaconvert#M3u8Settings": { @@ -19538,7 +19711,7 @@ "Scte35Source": { "target": "com.amazonaws.mediaconvert#M3u8Scte35Source", "traits": { - "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None if you don't want manifest conditioning. Choose Passthrough and choose Ad markers if you do want manifest conditioning. In both cases, also provide the ESAM XML as a string in the setting Signal processing notification XML.", + "smithy.api#documentation": "For SCTE-35 markers from your input-- Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want SCTE-35 markers in this output. For SCTE-35 markers from an ESAM XML document-- Choose None if you don't want manifest conditioning. Choose Passthrough and choose Ad markers if you do want manifest conditioning. In both cases, also provide the ESAM XML as a string in the setting Signal processing notification XML. For SCTE-35 markers from your input HLS manifest-- Choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest.", "smithy.api#jsonName": "scte35Source" } }, @@ -21827,10 +22000,16 @@ "traits": { "smithy.api#enumValue": "NONE" } + }, + "MANIFEST_CUES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MANIFEST_CUES" + } } }, "traits": { - "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output." + "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest." } }, "com.amazonaws.mediaconvert#MpdSettings": { @@ -21895,7 +22074,7 @@ "Scte35Source": { "target": "com.amazonaws.mediaconvert#MpdScte35Source", "traits": { - "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output.", + "smithy.api#documentation": "Ignore this setting unless you have SCTE-35 markers in your input video file. Choose Passthrough if you want SCTE-35 markers that appear in your input to also appear in this output. Choose None if you don't want those SCTE-35 markers in this output. When your input is an HLS manifest, choose Manifest cues to pass through CUE markers in your HLS manifest as segment boundaries and SCTE-35 markers in this output at each EXT-X-CUE-OUT splice point in the input manifest.", "smithy.api#jsonName": "scte35Source" } }, @@ -26814,21 +26993,199 @@ "smithy.api#documentation": "Specify the initial presentation timestamp (PTS) offset for your transport stream output. To let MediaConvert automatically determine the initial PTS offset: Keep the default value, Auto. We recommend that you choose Auto for the widest player compatibility. The initial PTS will be at least two seconds and vary depending on your output's bitrate, HRD buffer size and HRD buffer initial fill percentage. To manually specify an initial PTS offset: Choose Seconds or Milliseconds. Then specify the number of seconds or milliseconds with PTS offset." } }, + "com.amazonaws.mediaconvert#TtmlBackgroundColor": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "BLACK": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLACK" + } + }, + "WHITE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WHITE" + } + }, + "AUTO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AUTO" + } + } + }, + "traits": { + "smithy.api#documentation": "Specify the background color for TTML captions output." + } + }, "com.amazonaws.mediaconvert#TtmlDestinationSettings": { "type": "structure", "members": { + "BackgroundColor": { + "target": "com.amazonaws.mediaconvert#TtmlBackgroundColor", + "traits": { + "smithy.api#documentation": "Specify the color of the rectangle behind the captions. If Style passthrough is set to enabled, leave blank or set to Auto to pass through the background color from your input captions. If Style passthrough is set to disabled, leave blank or set to Auto to use the default black.", + "smithy.api#jsonName": "backgroundColor" + } + }, + "BackgroundOpacity": { + "target": "com.amazonaws.mediaconvert#__integerMin0Max255", + "traits": { + "smithy.api#documentation": "Specify the opacity of the background rectangle. Enter a value from 0 to 255, where 0 is transparent and 255 is opaque. If Style passthrough is set to enabled, leave blank to pass through the background style information in your input captions to your output captions. If Style passthrough is set to disabled and backgroundColor is set, leave blank to use a value of 255 (opaque).", + "smithy.api#jsonName": "backgroundOpacity" + } + }, + "FontColor": { + "target": "com.amazonaws.mediaconvert#TtmlFontColor", + "traits": { + "smithy.api#documentation": "Specify the color of the captions text. If Style passthrough is set to enabled, leave blank or set to Auto to pass through the font color from your input captions. If Style passthrough is set to disabled, leave blank or set to Auto to use the default white.", + "smithy.api#jsonName": "fontColor" + } + }, + "FontOpacity": { + "target": "com.amazonaws.mediaconvert#__integerMin0Max255", + "traits": { + "smithy.api#documentation": "Specify the opacity of the captions. Enter a value from 0 to 255, where 0 is transparent and 255 is opaque. If Style passthrough is set to enabled, leave blank to pass through the font opacity information in your input captions to your output captions. If Style passthrough is set to disabled and fontColor is set, leave blank to use a value of 255 (opaque).", + "smithy.api#jsonName": "fontOpacity" + } + }, + "FontSize": { + "target": "com.amazonaws.mediaconvert#__integerMin0Max96", + "traits": { + "smithy.api#documentation": "Specify the Font size in pixels. Must be a positive integer. Set to 0, or leave blank, for automatic font size.", + "smithy.api#jsonName": "fontSize" + } + }, + "FontStyle": { + "target": "com.amazonaws.mediaconvert#TtmlFontStyle", + "traits": { + "smithy.api#documentation": "Specify the font style of the caption text. If Style passthrough is set to enabled, leave blank to pass through the font style from your input captions. If Style passthrough is set to disabled, leave blank to use the default normal style.", + "smithy.api#jsonName": "fontStyle" + } + }, + "FontWeight": { + "target": "com.amazonaws.mediaconvert#TtmlFontWeight", + "traits": { + "smithy.api#documentation": "Specify the font weight of the caption text. If Style passthrough is set to enabled, leave blank to pass through the font weight from your input captions. If Style passthrough is set to disabled, leave blank to use the default normal weight.", + "smithy.api#jsonName": "fontWeight" + } + }, "StylePassthrough": { "target": "com.amazonaws.mediaconvert#TtmlStylePassthrough", "traits": { "smithy.api#documentation": "Pass through style and position information from a TTML-like input source (TTML, IMSC, SMPTE-TT) to the TTML output.", "smithy.api#jsonName": "stylePassthrough" } + }, + "TextDecoration": { + "target": "com.amazonaws.mediaconvert#TtmlTextDecoration", + "traits": { + "smithy.api#documentation": "Specify the text decoration of the caption text. If Style passthrough is set to enabled, leave blank to pass through the text decoration from your input captions. If Style passthrough is set to disabled, leave blank to use the default of none.", + "smithy.api#jsonName": "textDecoration" + } } }, "traits": { "smithy.api#documentation": "Settings related to TTML captions. TTML is a sidecar format that holds captions in a file that is separate from the video container. Set up sidecar captions in the same output group, but different output from your video. For more information, see https://docs.aws.amazon.com/mediaconvert/latest/ug/ttml-and-webvtt-output-captions.html." } }, + "com.amazonaws.mediaconvert#TtmlFontColor": { + "type": "enum", + "members": { + "WHITE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "WHITE" + } + }, + "BLACK": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLACK" + } + }, + "YELLOW": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "YELLOW" + } + }, + "RED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "RED" + } + }, + "GREEN": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "GREEN" + } + }, + "BLUE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BLUE" + } + }, + "AUTO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AUTO" + } + } + }, + "traits": { + "smithy.api#documentation": "Specify the font color for TTML captions output." + } + }, + "com.amazonaws.mediaconvert#TtmlFontStyle": { + "type": "enum", + "members": { + "NORMAL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NORMAL" + } + }, + "ITALIC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ITALIC" + } + } + }, + "traits": { + "smithy.api#documentation": "Specify the font style for TTML captions output." + } + }, + "com.amazonaws.mediaconvert#TtmlFontWeight": { + "type": "enum", + "members": { + "NORMAL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NORMAL" + } + }, + "BOLD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BOLD" + } + } + }, + "traits": { + "smithy.api#documentation": "Specify the font weight for TTML captions output." + } + }, "com.amazonaws.mediaconvert#TtmlStylePassthrough": { "type": "enum", "members": { @@ -26849,6 +27206,26 @@ "smithy.api#documentation": "Pass through style and position information from a TTML-like input source (TTML, IMSC, SMPTE-TT) to the TTML output." } }, + "com.amazonaws.mediaconvert#TtmlTextDecoration": { + "type": "enum", + "members": { + "NONE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "NONE" + } + }, + "UNDERLINE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "UNDERLINE" + } + } + }, + "traits": { + "smithy.api#documentation": "Specify the text decoration for TTML captions output." + } + }, "com.amazonaws.mediaconvert#Type": { "type": "enum", "members": { @@ -28479,6 +28856,13 @@ "smithy.api#jsonName": "colorPrimaries" } }, + "DisplayAspectRatio": { + "target": "com.amazonaws.mediaconvert#AspectRatio", + "traits": { + "smithy.api#documentation": "An aspect ratio expressed as a fraction with numerator and denominator values, reduced to lowest terms. Used for the sample (pixel) aspect ratio and the display aspect ratio of a video track. For example, a 720x576 anamorphic track has a sample aspect ratio of 64 / 45 and a display aspect ratio of 16 / 9.", + "smithy.api#jsonName": "displayAspectRatio" + } + }, "FrameRate": { "target": "com.amazonaws.mediaconvert#FrameRate", "traits": { @@ -28514,6 +28898,13 @@ "smithy.api#jsonName": "rotation" } }, + "SampleAspectRatio": { + "target": "com.amazonaws.mediaconvert#AspectRatio", + "traits": { + "smithy.api#documentation": "An aspect ratio expressed as a fraction with numerator and denominator values, reduced to lowest terms. Used for the sample (pixel) aspect ratio and the display aspect ratio of a video track. For example, a 720x576 anamorphic track has a sample aspect ratio of 64 / 45 and a display aspect ratio of 16 / 9.", + "smithy.api#jsonName": "sampleAspectRatio" + } + }, "TransferCharacteristics": { "target": "com.amazonaws.mediaconvert#TransferCharacteristics", "traits": { @@ -29775,6 +30166,13 @@ "com.amazonaws.mediaconvert#XavcHdIntraCbgProfileSettings": { "type": "structure", "members": { + "InterlaceMode": { + "target": "com.amazonaws.mediaconvert#XavcInterlaceMode", + "traits": { + "smithy.api#documentation": "Choose the scan line type for the output. Keep the default value, Progressive to create a progressive output, regardless of the scan type of your input. Use Top field first or Bottom field first to create an output that's interlaced with the same field polarity throughout. Use Follow, default top or Follow, default bottom to produce outputs with the same field polarity as the source. For jobs that have multiple inputs, the output field polarity might change over the course of the output. Follow behavior depends on the input scan type. If the source is interlaced, the output will be interlaced with the same polarity as the source. If the source is progressive, the output will be interlaced with top field bottom field first, depending on which of the Follow options you choose.", + "smithy.api#jsonName": "interlaceMode" + } + }, "XavcClass": { "target": "com.amazonaws.mediaconvert#XavcHdIntraCbgProfileClass", "traits": { diff --git a/aws-models/mwaa.json b/aws-models/mwaa.json index 916092e89..9bcf23481 100644 --- a/aws-models/mwaa.json +++ b/aws-models/mwaa.json @@ -2595,7 +2595,7 @@ "type": "string", "traits": { "smithy.api#length": { - "min": 1, + "min": 0, "max": 1024 }, "smithy.api#pattern": ".*" @@ -2720,7 +2720,7 @@ "type": "string", "traits": { "smithy.api#length": { - "min": 1, + "min": 0, "max": 1024 } } diff --git a/aws-models/sagemaker.json b/aws-models/sagemaker.json index f717c7a1c..d837579ca 100644 --- a/aws-models/sagemaker.json +++ b/aws-models/sagemaker.json @@ -64421,7 +64421,7 @@ "StorageType": { "target": "com.amazonaws.sagemaker#StorageType", "traits": { - "smithy.api#documentation": "

Option for different tiers of low latency storage for real-time data retrieval.

" + "smithy.api#documentation": "

Option for different tiers of low latency storage for real-time data retrieval.

" } } }, @@ -64437,6 +64437,12 @@ "traits": { "smithy.api#documentation": "

Time to live duration, where the record is hard deleted after the expiration time is reached; ExpiresAt = EventTime + TtlDuration. For information on HardDelete, see the DeleteRecord API in the Amazon SageMaker API Reference guide.

" } + }, + "StorageType": { + "target": "com.amazonaws.sagemaker#StorageType", + "traits": { + "smithy.api#documentation": "

The online store storage type to migrate the feature group to. Use this parameter to migrate an existing feature group from Standard to Standard_V2 storage format, enabling support for the UpdateRecord operation. Migration is a one-way operation and cannot be reversed.

" + } } }, "traits": { @@ -81129,6 +81135,12 @@ "smithy.api#enumValue": "Standard" } }, + "STANDARD_V2": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "Standard_V2" + } + }, "IN_MEMORY": { "target": "smithy.api#Unit", "traits": { @@ -86102,6 +86114,54 @@ "traits": { "smithy.api#enumValue": "ml.g6.48xlarge" } + }, + "ML_G6E_XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.xlarge" + } + }, + "ML_G6E_2XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.2xlarge" + } + }, + "ML_G6E_4XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.4xlarge" + } + }, + "ML_G6E_8XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.8xlarge" + } + }, + "ML_G6E_12XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.12xlarge" + } + }, + "ML_G6E_16XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.16xlarge" + } + }, + "ML_G6E_24XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.24xlarge" + } + }, + "ML_G6E_48XLARGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ml.g6e.48xlarge" + } } } }, diff --git a/aws-models/sesv2.json b/aws-models/sesv2.json index 52e4f8d0b..8e917732e 100644 --- a/aws-models/sesv2.json +++ b/aws-models/sesv2.json @@ -158,6 +158,74 @@ "smithy.api#documentation": "

Used to associate a configuration set with a MailManager archive.

" } }, + "com.amazonaws.sesv2#AssociateEmailIdentityCertificate": { + "type": "operation", + "input": { + "target": "com.amazonaws.sesv2#AssociateEmailIdentityCertificateRequest" + }, + "output": { + "target": "com.amazonaws.sesv2#AssociateEmailIdentityCertificateResponse" + }, + "errors": [ + { + "target": "com.amazonaws.sesv2#AlreadyExistsException" + }, + { + "target": "com.amazonaws.sesv2#BadRequestException" + }, + { + "target": "com.amazonaws.sesv2#NotFoundException" + }, + { + "target": "com.amazonaws.sesv2#TooManyRequestsException" + } + ], + "traits": { + "smithy.api#documentation": "

Associates an S/MIME certificate with an email identity. After the certificate is\n active, Amazon SES API v2 can add an S/MIME signature to messages that you send from the associated\n address when signing is enabled on the configuration set used to send the message.

\n

The certificate is an X.509 certificate that you manage in Certificate Manager\n (ACM). You identify it by its Amazon Resource Name (ARN).

\n \n

When the association is created, the certificate begins provisioning and its status is\n PROVISIONING. The status changes to ACTIVE when the certificate\n is ready to use for signing. Each email address can have only one certificate\n association. If an association already exists for the address, this operation returns an\n error, unless the existing association is in the DEPROVISIONING state.

", + "smithy.api#http": { + "method": "POST", + "uri": "/v2/email/identity/certificates", + "code": 200 + } + } + }, + "com.amazonaws.sesv2#AssociateEmailIdentityCertificateRequest": { + "type": "structure", + "members": { + "EmailIdentity": { + "target": "com.amazonaws.sesv2#Identity", + "traits": { + "smithy.api#documentation": "

The email identity, either an email address or a domain, to associate the certificate\n with.

", + "smithy.api#required": {} + } + }, + "FromAddress": { + "target": "com.amazonaws.sesv2#EmailAddress", + "traits": { + "smithy.api#documentation": "

The email address that the certificate applies to. This value is required when the\n email identity is a domain, and the address must belong to that domain or one of its\n subdomains. When the email identity is an email address, this value is optional. If you\n specify it, it must exactly match the email identity.

" + } + }, + "CertificateArn": { + "target": "com.amazonaws.sesv2#CertificateArn", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the Certificate Manager (ACM) certificate to\n associate with the email identity.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A request to associate an S/MIME certificate with an email identity.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.sesv2#AssociateEmailIdentityCertificateResponse": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

An HTTP 200 response if the request succeeds, or an error message if the request\n fails.

", + "smithy.api#output": {} + } + }, "com.amazonaws.sesv2#Attachment": { "type": "structure", "members": { @@ -833,6 +901,16 @@ "com.amazonaws.sesv2#CaseId": { "type": "string" }, + "com.amazonaws.sesv2#CertificateArn": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 20, + "max": 2048 + }, + "smithy.api#pattern": "^arn:[\\w+=/,.@-]+:[\\w+=/,.@-]+:[\\w+=/,.@-]*:[0-9]+:certificate/[\\w+=,.@-]+$" + } + }, "com.amazonaws.sesv2#Charset": { "type": "string" }, @@ -1276,6 +1354,12 @@ "traits": { "smithy.api#documentation": "

An object that defines the MailManager archiving options for emails that you send\n using the configuration set.

" } + }, + "MessageSecurityOptions": { + "target": "com.amazonaws.sesv2#MessageSecurityOptions", + "traits": { + "smithy.api#documentation": "

The message security options to apply to the configuration set, such as the signing\n scheme used for messages that you send with the configuration set.

" + } } }, "traits": { @@ -2546,6 +2630,13 @@ "smithy.api#documentation": "

The default value of the dimension that is published to Amazon CloudWatch if you don't provide the\n value of the dimension when you send an email. This value has to meet the following\n criteria:

\n " } }, + "com.amazonaws.sesv2#DefaultSigningScheme": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

Specifies the default signing scheme, in which Amazon SES API v2 doesn't apply S/MIME signing to\n messages sent with the configuration set.

" + } + }, "com.amazonaws.sesv2#DeleteConfigurationSet": { "type": "operation", "input": { @@ -3555,6 +3646,64 @@ } } }, + "com.amazonaws.sesv2#DisassociateEmailIdentityCertificate": { + "type": "operation", + "input": { + "target": "com.amazonaws.sesv2#DisassociateEmailIdentityCertificateRequest" + }, + "output": { + "target": "com.amazonaws.sesv2#DisassociateEmailIdentityCertificateResponse" + }, + "errors": [ + { + "target": "com.amazonaws.sesv2#BadRequestException" + }, + { + "target": "com.amazonaws.sesv2#NotFoundException" + }, + { + "target": "com.amazonaws.sesv2#TooManyRequestsException" + } + ], + "traits": { + "smithy.api#documentation": "

Removes the association between an S/MIME certificate and an email identity. After the\n association is removed, Amazon SES API v2 stops adding an S/MIME signature to messages sent from\n that address.

\n

If the email identity is a domain, specify the FromAddress whose\n certificate association you want to remove.

\n

This operation is idempotent. If the specified email identity exists but there's no\n matching certificate association, the operation succeeds without making any changes.\n Amazon SES API v2 returns a NotFoundException only when the specified email identity\n doesn't exist.

", + "smithy.api#http": { + "method": "POST", + "uri": "/v2/email/identity/certificates/delete", + "code": 200 + } + } + }, + "com.amazonaws.sesv2#DisassociateEmailIdentityCertificateRequest": { + "type": "structure", + "members": { + "EmailIdentity": { + "target": "com.amazonaws.sesv2#Identity", + "traits": { + "smithy.api#documentation": "

The email identity whose certificate association you want to remove.

", + "smithy.api#required": {} + } + }, + "FromAddress": { + "target": "com.amazonaws.sesv2#EmailAddress", + "traits": { + "smithy.api#documentation": "

The email address whose certificate association you want to remove. This value is\n required when the email identity is a domain. When the email identity is an email\n address, this value is optional.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A request to remove the association between an S/MIME certificate and an email\n identity.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.sesv2#DisassociateEmailIdentityCertificateResponse": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

An HTTP 200 response if the request succeeds, or an error message if the request\n fails.

", + "smithy.api#output": {} + } + }, "com.amazonaws.sesv2#DisplayName": { "type": "string" }, @@ -5205,6 +5354,12 @@ "traits": { "smithy.api#documentation": "

An object that defines the MailManager archive where sent emails are archived that you send\n using the configuration set.

" } + }, + "MessageSecurityOptions": { + "target": "com.amazonaws.sesv2#MessageSecurityOptions", + "traits": { + "smithy.api#documentation": "

The message security options that are applied to the configuration set, such as the\n signing scheme used for messages that you send with the configuration set.

" + } } }, "traits": { @@ -7010,6 +7165,82 @@ } } }, + "com.amazonaws.sesv2#IdentityCertificate": { + "type": "structure", + "members": { + "FromAddress": { + "target": "com.amazonaws.sesv2#EmailAddress", + "traits": { + "smithy.api#documentation": "

The email address that the certificate applies to.

" + } + }, + "Status": { + "target": "com.amazonaws.sesv2#IdentityCertificateStatus", + "traits": { + "smithy.api#documentation": "

The status of the certificate association. A status of ACTIVE indicates\n that the certificate is ready to use for signing.

" + } + }, + "CertificateArn": { + "target": "com.amazonaws.sesv2#CertificateArn", + "traits": { + "smithy.api#documentation": "

The Amazon Resource Name (ARN) of the Certificate Manager (ACM) certificate that's\n associated with the email identity.

" + } + }, + "CertificateExpiryTime": { + "target": "com.amazonaws.sesv2#Timestamp", + "traits": { + "smithy.api#documentation": "

The timestamp after which the certificate is no longer valid.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

An object that contains information about an S/MIME certificate that's associated with\n an email identity.

" + } + }, + "com.amazonaws.sesv2#IdentityCertificateList": { + "type": "list", + "member": { + "target": "com.amazonaws.sesv2#IdentityCertificate" + } + }, + "com.amazonaws.sesv2#IdentityCertificateStatus": { + "type": "enum", + "members": { + "PROVISIONING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PROVISIONING" + } + }, + "INACTIVE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INACTIVE" + } + }, + "DEPROVISIONING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DEPROVISIONING" + } + }, + "ACTIVE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "ACTIVE" + } + }, + "FAILED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FAILED" + } + } + }, + "traits": { + "smithy.api#documentation": "

The status of an S/MIME certificate that's associated with an email identity. The\n status can be one of the following values:

\n " + } + }, "com.amazonaws.sesv2#IdentityInfo": { "type": "structure", "members": { @@ -8102,6 +8333,89 @@ "smithy.api#output": {} } }, + "com.amazonaws.sesv2#ListEmailIdentityCertificates": { + "type": "operation", + "input": { + "target": "com.amazonaws.sesv2#ListEmailIdentityCertificatesRequest" + }, + "output": { + "target": "com.amazonaws.sesv2#ListEmailIdentityCertificatesResponse" + }, + "errors": [ + { + "target": "com.amazonaws.sesv2#BadRequestException" + }, + { + "target": "com.amazonaws.sesv2#NotFoundException" + }, + { + "target": "com.amazonaws.sesv2#TooManyRequestsException" + } + ], + "traits": { + "smithy.api#documentation": "

Lists the S/MIME certificates that are associated with the specified email identity.\n The results include certificates in all states, such as PROVISIONING,\n ACTIVE, INACTIVE, DEPROVISIONING, and\n FAILED.

\n

If a certificate has passed its expiration time, it's returned with a status of\n FAILED.

\n

We recommend using pagination to ensure that the operation returns quickly and\n successfully. When there are more results than fit in a single response, the response\n includes a NextToken value that you use in a subsequent call to retrieve\n the next set of results.

", + "smithy.api#http": { + "method": "POST", + "uri": "/v2/email/identity/certificates/list", + "code": 200 + }, + "smithy.api#paginated": { + "inputToken": "NextToken", + "outputToken": "NextToken", + "items": "Certificates", + "pageSize": "PageSize" + } + } + }, + "com.amazonaws.sesv2#ListEmailIdentityCertificatesRequest": { + "type": "structure", + "members": { + "EmailIdentity": { + "target": "com.amazonaws.sesv2#Identity", + "traits": { + "smithy.api#documentation": "

The email identity whose certificate associations you want to list.

", + "smithy.api#required": {} + } + }, + "NextToken": { + "target": "com.amazonaws.sesv2#NextToken", + "traits": { + "smithy.api#documentation": "

A token returned from a previous call to ListEmailIdentityCertificates to\n indicate the position in the list of certificates.

" + } + }, + "PageSize": { + "target": "com.amazonaws.sesv2#MaxItems", + "traits": { + "smithy.api#documentation": "

The number of results to show in a single call to\n ListEmailIdentityCertificates. If the number of results is larger than the\n number you specified in this parameter, then the response includes a\n NextToken element, which you can use to obtain additional results.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A request to list the S/MIME certificates that are associated with an email\n identity.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.sesv2#ListEmailIdentityCertificatesResponse": { + "type": "structure", + "members": { + "Certificates": { + "target": "com.amazonaws.sesv2#IdentityCertificateList", + "traits": { + "smithy.api#documentation": "

An array that contains the certificate associations for the email identity. Each entry\n includes the from address, the certificate's status, its Amazon Resource Name (ARN),\n and its expiry time.

" + } + }, + "NextToken": { + "target": "com.amazonaws.sesv2#NextToken", + "traits": { + "smithy.api#documentation": "

A token that indicates that there are additional certificates to list. To view\n additional certificates, issue another request to\n ListEmailIdentityCertificates, and pass this token in the\n NextToken parameter.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Information about the S/MIME certificates that are associated with an email\n identity.

", + "smithy.api#output": {} + } + }, "com.amazonaws.sesv2#ListEmailTemplates": { "type": "operation", "input": { @@ -9393,7 +9707,7 @@ "MaxResults": { "target": "com.amazonaws.sesv2#MessageInsightsExportMaxResults", "traits": { - "smithy.api#documentation": "

The maximum number of results.

" + "smithy.api#documentation": "

The maximum number of results.

\n \n

If you don't specify MaxResults, the export returns a maximum of\n 1,000 results.

\n
" } } }, @@ -9467,6 +9781,20 @@ "smithy.api#httpError": 400 } }, + "com.amazonaws.sesv2#MessageSecurityOptions": { + "type": "structure", + "members": { + "SigningScheme": { + "target": "com.amazonaws.sesv2#SigningScheme", + "traits": { + "smithy.api#documentation": "

The signing scheme that Amazon SES API v2 applies to messages sent with the configuration\n set.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

An object that defines the message-level security options that apply to messages that\n you send using the configuration set. Currently, these options determine whether Amazon SES API v2\n adds an S/MIME signature to your messages and, if so, the format of that\n signature.

" + } + }, "com.amazonaws.sesv2#MessageTag": { "type": "structure", "members": { @@ -12666,10 +12994,47 @@ "smithy.api#default": 0 } }, + "com.amazonaws.sesv2#SignatureFormat": { + "type": "enum", + "members": { + "DETACHED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DETACHED" + } + } + }, + "traits": { + "smithy.api#documentation": "

The format of the S/MIME signature that's applied to a message. The following value is\n supported:

\n " + } + }, + "com.amazonaws.sesv2#SigningScheme": { + "type": "union", + "members": { + "DefaultScheme": { + "target": "com.amazonaws.sesv2#DefaultSigningScheme", + "traits": { + "smithy.api#documentation": "

Use the default signing behavior. When you select this option, Amazon SES API v2 doesn't add an\n S/MIME signature to messages sent with the configuration set.

" + } + }, + "SmimeScheme": { + "target": "com.amazonaws.sesv2#SmimeSigningScheme", + "traits": { + "smithy.api#documentation": "

Sign messages sent with the configuration set using S/MIME. For signing to apply, the\n email identity used to send a message must have an active S/MIME certificate\n association.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies the signing scheme to apply to messages sent with a configuration set. This\n is a union type, so you specify exactly one of its members.

" + } + }, "com.amazonaws.sesv2#SimpleEmailService_v2": { "type": "service", "version": "2019-09-27", "operations": [ + { + "target": "com.amazonaws.sesv2#AssociateEmailIdentityCertificate" + }, { "target": "com.amazonaws.sesv2#BatchGetMetricData" }, @@ -12760,6 +13125,9 @@ { "target": "com.amazonaws.sesv2#DeleteTenantResourceAssociation" }, + { + "target": "com.amazonaws.sesv2#DisassociateEmailIdentityCertificate" + }, { "target": "com.amazonaws.sesv2#GetAccount" }, @@ -12859,6 +13227,9 @@ { "target": "com.amazonaws.sesv2#ListEmailIdentities" }, + { + "target": "com.amazonaws.sesv2#ListEmailIdentityCertificates" + }, { "target": "com.amazonaws.sesv2#ListEmailTemplates" }, @@ -12982,6 +13353,9 @@ { "target": "com.amazonaws.sesv2#UntagResource" }, + { + "target": "com.amazonaws.sesv2#UpdateConfigurationSet" + }, { "target": "com.amazonaws.sesv2#UpdateConfigurationSetEventDestination" }, @@ -14927,6 +15301,20 @@ } } }, + "com.amazonaws.sesv2#SmimeSigningScheme": { + "type": "structure", + "members": { + "SignatureFormat": { + "target": "com.amazonaws.sesv2#SignatureFormat", + "traits": { + "smithy.api#documentation": "

The format of the S/MIME signature that Amazon SES API v2 applies to messages.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Specifies that Amazon SES API v2 signs messages sent with the configuration set using\n S/MIME.

" + } + }, "com.amazonaws.sesv2#SnsDestination": { "type": "structure", "members": { @@ -15959,6 +16347,34 @@ "smithy.api#output": {} } }, + "com.amazonaws.sesv2#UpdateConfigurationSet": { + "type": "operation", + "input": { + "target": "com.amazonaws.sesv2#UpdateConfigurationSetRequest" + }, + "output": { + "target": "com.amazonaws.sesv2#UpdateConfigurationSetResponse" + }, + "errors": [ + { + "target": "com.amazonaws.sesv2#BadRequestException" + }, + { + "target": "com.amazonaws.sesv2#NotFoundException" + }, + { + "target": "com.amazonaws.sesv2#TooManyRequestsException" + } + ], + "traits": { + "smithy.api#documentation": "

Updates an existing configuration set.

\n

This operation performs a partial update. Only the attributes that you include in the\n request are updated; any omitted attribute is left unchanged.

", + "smithy.api#http": { + "method": "POST", + "uri": "/v2/email/update-configuration-sets", + "code": 200 + } + } + }, "com.amazonaws.sesv2#UpdateConfigurationSetEventDestination": { "type": "operation", "input": { @@ -16027,6 +16443,36 @@ "smithy.api#output": {} } }, + "com.amazonaws.sesv2#UpdateConfigurationSetRequest": { + "type": "structure", + "members": { + "ConfigurationSetName": { + "target": "com.amazonaws.sesv2#ConfigurationSetName", + "traits": { + "smithy.api#documentation": "

The name of the configuration set to update.

", + "smithy.api#required": {} + } + }, + "MessageSecurityOptions": { + "target": "com.amazonaws.sesv2#MessageSecurityOptions", + "traits": { + "smithy.api#documentation": "

The security options that apply to the MIME message itself for messages sent with the\n configuration set.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

A request to update the configuration of an existing configuration set.

", + "smithy.api#input": {} + } + }, + "com.amazonaws.sesv2#UpdateConfigurationSetResponse": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#documentation": "

An HTTP 200 response if the request succeeds, or an error message if the request\n fails.

", + "smithy.api#output": {} + } + }, "com.amazonaws.sesv2#UpdateContact": { "type": "operation", "input": { diff --git a/aws-models/sfn.json b/aws-models/sfn.json index df2def9da..1a30c7df2 100644 --- a/aws-models/sfn.json +++ b/aws-models/sfn.json @@ -1546,7 +1546,7 @@ "name": { "target": "com.amazonaws.sfn#Name", "traits": { - "smithy.api#documentation": "

The name of the activity to create. This name must be unique for your Amazon Web Services account and region for 90 days. For more information,\n see \n Limits Related to State Machine Executions in the Step Functions Developer Guide.

\n

A name must not contain:

\n \n

To enable logging with CloudWatch Logs, the name should only contain 0-9, A-Z, a-z, - and _.

", + "smithy.api#documentation": "

The name of the activity to create. This name must be unique for your Amazon Web Services account and region.

\n

A name must not contain:

\n \n

To enable logging with CloudWatch Logs, the name should only contain 0-9, A-Z, a-z, - and _.

", "smithy.api#required": {} } }, @@ -3243,7 +3243,7 @@ } ], "traits": { - "smithy.api#documentation": "

Used by workers to retrieve a task (with the specified activity ARN) which has been\n scheduled for execution by a running state machine. This initiates a long poll, where the\n service holds the HTTP connection open and responds as soon as a task becomes available (i.e.\n an execution of a task of this type is needed.) The maximum time the service holds on to the\n request before responding is 60 seconds. If no task is available within 60 seconds, the poll\n returns a taskToken with a null string.

\n \n

This API action isn't logged in CloudTrail.

\n
\n \n

Workers should set their client side socket timeout to at least 65 seconds (5 seconds\n higher than the maximum time the service may hold the poll request).

\n

Polling with GetActivityTask can cause latency in some implementations. See\n Avoid\n Latency When Polling for Activity Tasks in the Step Functions Developer Guide.

\n
" + "smithy.api#documentation": "

Used by workers to retrieve a task (with the specified activity ARN) which has been\n scheduled for execution by a running state machine. This initiates a long poll, where the\n service holds the HTTP connection open and responds as soon as a task becomes available (i.e.\n an execution of a task of this type is needed.) The maximum time the service holds on to the\n request before responding is 60 seconds. If no task is available within 60 seconds, the poll\n returns a taskToken with a null string.

\n \n

Workers should set their client side socket timeout to at least 65 seconds (5 seconds\n higher than the maximum time the service may hold the poll request).

\n

Polling with GetActivityTask can cause latency in some implementations. See\n Avoid\n Latency When Polling for Activity Tasks in the Step Functions Developer Guide.

\n
" } }, "com.amazonaws.sfn#GetActivityTaskInput": { @@ -4772,7 +4772,7 @@ } ], "traits": { - "smithy.api#documentation": "

Lists all executions of a state machine or a Map Run. You can list all executions related to a state machine by specifying a state machine Amazon Resource Name (ARN), or those related to a Map Run by specifying a Map Run ARN. Using this API action, you can also list all redriven executions.

\n

You can also provide a state machine alias ARN or version ARN to list the executions associated with a specific alias or version.

\n

Results are\n sorted by time, with the most recent execution first.

\n

If nextToken is returned, there are more results available. The value of nextToken is a unique pagination token for each page.\n Make the call again using the returned token to retrieve the next page. Keep all other arguments unchanged. Each pagination token expires after 24 hours. Using an expired pagination token will return an HTTP 400 InvalidToken error.

\n \n

This operation is eventually consistent. The results are best effort and may not reflect very recent updates and changes.

\n
\n

This API action is not supported by EXPRESS state machines.

", + "smithy.api#documentation": "

Lists all executions of a state machine or a Map Run. You can list all executions related to a state machine by specifying a state machine Amazon Resource Name (ARN), or those related to a Map Run by specifying a Map Run ARN. Using this API action, you can also list all redriven executions.

\n

You can also provide a state machine alias ARN or version ARN to list the executions associated with a specific alias or version.

\n

Results are sorted by time, with the most recent execution first. Running executions are sorted by their startDate or redriveDate, and other executions are sorted by their stopDate.

\n

If nextToken is returned, there are more results available. The value of nextToken is a unique pagination token for each page.\n Make the call again using the returned token to retrieve the next page. Keep all other arguments unchanged. Each pagination token expires after 24 hours. Using an expired pagination token will return an HTTP 400 InvalidToken error.

\n \n

This operation is eventually consistent. The results are best effort and may not reflect very recent updates and changes.

\n
\n

This API action is not supported by EXPRESS state machines. However, you may list EXPRESS children started by a map run using the mapRunArn parameter.

", "smithy.api#paginated": { "inputToken": "nextToken", "outputToken": "nextToken", @@ -6262,7 +6262,7 @@ } ], "traits": { - "smithy.api#documentation": "

Starts a state machine execution.

\n

A qualified state machine ARN can either refer to a Distributed Map state defined within a state machine, a version ARN, or an alias ARN.

\n

The following are some examples of qualified and unqualified state machine ARNs:

\n \n

If you start an execution with an unqualified state machine ARN, Step Functions uses the latest revision of the state machine for the execution.

\n

To start executions of a state machine version, call\n StartExecution and provide the version ARN or the ARN of an alias that points to the version.

\n \n

\n StartExecution is idempotent for STANDARD workflows. For a\n STANDARD workflow, if you call StartExecution with the same name\n and input as a running execution, the call succeeds and return the same response as the\n original request. If the execution is closed or if the input is different, it returns a\n 400 ExecutionAlreadyExists error. You can reuse names after 90 days.

\n

\n StartExecution isn't idempotent for EXPRESS workflows.

\n
", + "smithy.api#documentation": "

Starts a state machine execution.

\n

A qualified state machine ARN can either refer to a Distributed Map state defined within a state machine, a version ARN, or an alias ARN.

\n

The following are some examples of qualified and unqualified state machine ARNs:

\n \n

If you start an execution with an unqualified state machine ARN, Step Functions uses the latest revision of the state machine for the execution.

\n

To start executions of a state machine version, call\n StartExecution and provide the version ARN or the ARN of an alias that points to the version.

\n \n

\n StartExecution is idempotent for STANDARD workflows. For a\n STANDARD workflow, if you call StartExecution with the same name\n and input as a running execution, the call succeeds and return the same response as the\n original request. If the execution is closed or if the input is different, it returns a\n 400 ExecutionAlreadyExists error. You can reuse the name 90 days after it closes.

\n

\n StartExecution isn't idempotent for EXPRESS workflows.

\n
", "smithy.api#idempotent": {} } }, @@ -6279,7 +6279,7 @@ "name": { "target": "com.amazonaws.sfn#Name", "traits": { - "smithy.api#documentation": "

Optional name of the execution. This name must be unique for your Amazon Web Services account, Region, and state machine for 90 days. For more information,\n see \n Limits Related to State Machine Executions in the Step Functions Developer Guide.

\n

If you don't provide a name for the execution, Step Functions automatically generates a universally unique identifier (UUID) as the execution name.

\n

A name must not contain:

\n \n

To enable logging with CloudWatch Logs, the name should only contain 0-9, A-Z, a-z, - and _.

" + "smithy.api#documentation": "

Optional name of the execution. For STANDARD workflows, this name must be unique for your Amazon Web Services account, region, and state machine.\n If a previous execution with the same name exists, you can reuse the name 90 days after it closes. For EXPRESS workflows, execution names\n can be reused immediately.\n For more information, see \n Limits Related to State Machine Executions in the Step Functions Developer Guide.

\n

If you don't provide a name for the execution, Step Functions automatically generates a universally unique identifier (UUID) as the execution name.

\n

A name must not contain:

\n \n

To enable logging with CloudWatch Logs, the name should only contain 0-9, A-Z, a-z, - and _.

" } }, "input": { @@ -6359,7 +6359,7 @@ } ], "traits": { - "smithy.api#documentation": "

Starts a Synchronous Express state machine execution. StartSyncExecution \n\t\t\t is not available for STANDARD workflows.

\n \n

\n StartSyncExecution will return a 200 OK response, even if your\n execution fails, because the status code in the API response doesn't reflect function\n errors. Error codes are reserved for errors that prevent your execution from running, such\n as permissions errors, limit errors, or issues with your state machine code and\n configuration.

\n
\n \n

This API action isn't logged in CloudTrail.

\n
", + "smithy.api#documentation": "

Starts a Synchronous Express state machine execution. StartSyncExecution \n\t\t\t is not available for STANDARD workflows.

\n \n

\n StartSyncExecution will return a 200 OK response, even if your\n execution fails, because the status code in the API response doesn't reflect function\n errors. Error codes are reserved for errors that prevent your execution from running, such\n as permissions errors, limit errors, or issues with your state machine code and\n configuration.

\n
", "smithy.api#endpoint": { "hostPrefix": "sync-" } diff --git a/aws-models/support.json b/aws-models/support.json index 8b7210d21..d9b0acb8a 100644 --- a/aws-models/support.json +++ b/aws-models/support.json @@ -39,12 +39,18 @@ { "target": "com.amazonaws.support#AddCommunicationToCase" }, + { + "target": "com.amazonaws.support#CompleteAttachmentUpload" + }, { "target": "com.amazonaws.support#CreateCase" }, { "target": "com.amazonaws.support#DescribeAttachment" }, + { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatus" + }, { "target": "com.amazonaws.support#DescribeCases" }, @@ -75,6 +81,12 @@ { "target": "com.amazonaws.support#DescribeTrustedAdvisorCheckSummaries" }, + { + "target": "com.amazonaws.support#GetAttachmentDownloadLink" + }, + { + "target": "com.amazonaws.support#GetAttachmentUploadLinks" + }, { "target": "com.amazonaws.support#RefreshTrustedAdvisorCheck" }, @@ -94,7 +106,7 @@ "name": "support" }, "aws.protocols#awsJson1_1": {}, - "smithy.api#documentation": "Amazon Web Services Support\n

The Amazon Web Services Support API Reference is intended for programmers who need detailed\n information about the Amazon Web Services Support operations and data types. You can use the API to manage\n your support cases programmatically. The Amazon Web Services Support API uses HTTP methods that return\n results in JSON format.

\n \n \n \n

You can also use the Amazon Web Services Support API to access features for Trusted Advisor. You can return a list of\n checks and their descriptions, get check results, specify checks to refresh, and get the\n refresh status of checks.

\n

You can manage your support cases with the following Amazon Web Services Support API operations:

\n \n

You can also use the Amazon Web Services Support API to call the Trusted Advisor operations. For more\n information, see Trusted Advisor in the\n Amazon Web Services Support User Guide.

\n

For authentication of requests, Amazon Web Services Support uses Signature Version 4 Signing\n Process.

\n

For more information about this service and the endpoints to use, see About the\n Amazon Web Services Support API in the Amazon Web Services Support User Guide.

", + "smithy.api#documentation": "Amazon Web Services Support\n

The Amazon Web Services Support API Reference is intended for programmers who need detailed\n information about the Amazon Web Services Support operations and data types. You can use the API to manage\n your support cases programmatically. The Amazon Web Services Support API uses HTTP methods that return\n results in JSON format.

\n \n \n \n

You can also use the Amazon Web Services Support API to access features for Trusted Advisor. You can return a list of\n checks and their descriptions, get check results, specify checks to refresh, and get the\n refresh status of checks.

\n

You can manage your support cases with the following Amazon Web Services Support API operations:

\n \n

You can also use the Amazon Web Services Support API to call the Trusted Advisor operations. For more\n information, see Trusted Advisor in the\n Amazon Web Services Support User Guide.

\n

For authentication of requests, Amazon Web Services Support uses Signature Version 4 Signing\n Process.

\n

For more information about this service and the endpoints to use, see About the\n Amazon Web Services Support API in the Amazon Web Services Support User Guide.

", "smithy.api#title": "AWS Support", "smithy.api#xmlNamespace": { "uri": "http://support.amazonaws.com/doc/2013-04-15/" @@ -1546,12 +1558,15 @@ { "target": "com.amazonaws.support#AttachmentSetSizeLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Adds one or more attachments to an attachment set.

\n

An attachment set is a temporary container for attachments that you add to a case or\n case communication. The set is available for 1 hour after it's created. The\n expiryTime returned in the response is when the set expires.

\n \n \n " + "smithy.api#documentation": "

Adds one or more attachments to an attachment set.

\n

An attachment set is a temporary container for attachments that you add to a case or\n case communication. The set is available for 1 hour after it's created. The\n expiryTime returned in the response is when the set expires.

\n \n \n " } }, "com.amazonaws.support#AddAttachmentsToSetRequest": { @@ -1569,6 +1584,12 @@ "smithy.api#documentation": "

One or more attachments to add to the set. You can add up to three attachments per\n set. The size limit is 5 MB per attachment.

\n

In the Attachment object, use the data parameter to specify\n the contents of the attachment file. In the previous request syntax, the value for\n data appear as blob, which is represented as a\n base64-encoded string. The value for fileName is the name of the\n attachment, such as troubleshoot-screenshot.png.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually adding the attachments. When set\n to true, the request is validated but no attachments are stored, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } } }, "traits": { @@ -1614,12 +1635,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Adds additional customer communication to an Amazon Web Services Support case. Use the caseId\n parameter to identify the case to which to add communication. You can list a set of\n email addresses to copy on the communication by using the ccEmailAddresses\n parameter. The communicationBody value contains the text of the\n communication.

\n \n \n " + "smithy.api#documentation": "

Adds additional customer communication to a Amazon Web Services Support case. Use the caseId\n parameter to identify the case to which to add communication. To list a set of\n email addresses to copy on the communication, use the ccEmailAddresses\n parameter. The communicationBody value contains the text of the\n communication.

\n

To attach files larger than 5 MB to the communication, use the uploadIds parameter.

\n \n

Amazon Web Services Support automatically redacts sensitive information from support cases to protect your data. The following information is replaced with [REDACTED_BY_Amazon Web Services] and is not stored:

\n \n

This sensitive information is never required by Amazon Web Services Support.

\n
\n \n \n " } }, "com.amazonaws.support#AddCommunicationToCaseRequest": { @@ -1628,7 +1652,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "communicationBody": { @@ -1647,7 +1671,19 @@ "attachmentSetId": { "target": "com.amazonaws.support#AttachmentSetId", "traits": { - "smithy.api#documentation": "

The ID of a set of one or more attachments for the communication to add to the case.\n Create the set by calling AddAttachmentsToSet\n

" + "smithy.api#documentation": "

The ID of a set of one or more attachments for the communication to add to the case.\n Create the set by calling AddAttachmentsToSet. Each attachment in the\n set must be 5 MB or smaller. To attach files larger than 5 MB, use uploadIds.

" + } + }, + "uploadIds": { + "target": "com.amazonaws.support#UploadIds", + "traits": { + "smithy.api#documentation": "

A list of upload IDs that identify attachments to add to the case. Each\n uploadId is returned by the GetAttachmentUploadLinks\n operation. The upload must reach the attachment-ready state by calling CompleteAttachmentUpload before it can be passed here.\n Use\n uploadIds to attach files of any supported size, including files larger than\n 5 MB.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually adding the communication to the\n case. When set to true, the request is validated but the communication isn't\n added, and the operation returns a DryRunOperationException. When omitted or set\n to false, the request runs normally.

" } } }, @@ -1840,7 +1876,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "displayId": { @@ -1906,12 +1942,12 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } } }, "traits": { - "smithy.api#documentation": "

A JSON-formatted object that contains the metadata for a support case. It is contained\n in the response from a DescribeCases request. CaseDetails contains the following fields:

\n " + "smithy.api#documentation": "

A JSON-formatted object that contains the metadata for a support case. It is contained\n in the response from a DescribeCases request. CaseDetails contains the following fields:

\n " } }, "com.amazonaws.support#CaseId": { @@ -2009,7 +2045,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "body": { @@ -2030,10 +2066,16 @@ "smithy.api#documentation": "

The time the communication was created.

" } }, + "attachments": { + "target": "com.amazonaws.support#AttachmentSet", + "traits": { + "smithy.api#documentation": "

Information about all attachments on the case communication. This includes attachments added through AddAttachmentsToSet and attachments uploaded through GetAttachmentUploadLinks.

\n

Use this field to enumerate every attachment on the communication. To download an attachment listed in this field, use GetAttachmentDownloadLink. GetAttachmentDownloadLink returns a presigned URL that works for attachments of any size.

" + } + }, "attachmentSet": { "target": "com.amazonaws.support#AttachmentSet", "traits": { - "smithy.api#documentation": "

Information about the attachments to the case communication.

" + "smithy.api#documentation": "

Information about the attachments to the case communication that are 5 MB or smaller.\n This field doesn't include attachments larger than 5 MB. To enumerate every attachment on\n the communication, including attachments larger than 5 MB, use the\n attachments field instead.

" } } }, @@ -2094,6 +2136,106 @@ } } }, + "com.amazonaws.support#CompleteAttachmentUpload": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#CompleteAttachmentUploadRequest" + }, + "output": { + "target": "com.amazonaws.support#CompleteAttachmentUploadResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Completes an attachment upload that was started with GetAttachmentUploadLinks. After you upload a part of the file to its\n presigned Amazon S3 URL, call CompleteAttachmentUpload with the\n partIndex and eTag of that part. You can include one part per\n call, or multiple parts in a single call. After CompleteAttachmentUpload has\n been called for every part of the file, the service processes the upload asynchronously. The\n attachment-ready status might not be reflected immediately. Use DescribeAttachmentUploadStatus to poll for the uploadStatus to\n become attachment-ready before passing the uploadId to CreateCase or AddCommunicationToCase.

" + } + }, + "com.amazonaws.support#CompleteAttachmentUploadRequest": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The identifier associated with the upload to complete.

", + "smithy.api#required": {} + } + }, + "completedUploads": { + "target": "com.amazonaws.support#CompletedUploadList", + "traits": { + "smithy.api#documentation": "

The list of parts being reported as completed in this call. Each entry must contain the partIndex of an uploaded part and the ETag returned by Amazon S3 when that part was uploaded.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually completing the upload. When set\n to true, the request is validated but the upload isn't finalized, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#CompleteAttachmentUploadResponse": { + "type": "structure", + "members": { + "uploadStatus": { + "target": "com.amazonaws.support#UploadStatus", + "traits": { + "smithy.api#documentation": "

The status of the multipart upload after the operation finalizes the\n attachment. Valid values: attachment-ready, attachment-not-ready,\n and failed.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#CompletedUpload": { + "type": "structure", + "members": { + "partIndex": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The index of the uploaded part. This is the same partIndex value returned for the corresponding entry in the uploadUrls field of the GetAttachmentUploadLinks response.

", + "smithy.api#required": {} + } + }, + "eTag": { + "target": "com.amazonaws.support#ETag", + "traits": { + "smithy.api#documentation": "

The ETag returned in the response headers when the part was uploaded to Amazon S3. The ETag value identifies the part contents.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Identifies a single uploaded part of a multipart attachment upload. Pass a list of\n CompletedUpload objects to CompleteAttachmentUpload to\n finalize the upload.

" + } + }, + "com.amazonaws.support#CompletedUploadList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#CompletedUpload" + } + }, + "com.amazonaws.support#CoralAvailabilityThrottledResource": { + "type": "string" + }, + "com.amazonaws.support#CoralAvailabilityThrottlingReason": { + "type": "string" + }, "com.amazonaws.support#CreateCase": { "type": "operation", "input": { @@ -2112,12 +2254,15 @@ { "target": "com.amazonaws.support#CaseCreationLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Creates a case in the Amazon Web Services Support Center. This operation is similar to how you create a case\n in the Amazon Web Services Support Center Create\n Case page.

\n

The Amazon Web Services Support API doesn't support requesting service limit increases. You can submit a\n service limit increase in the following ways:

\n \n

A successful CreateCase request returns an Amazon Web Services Support case number. You can use\n the DescribeCases operation and specify the case number to get\n existing Amazon Web Services Support cases. After you create a case, use the AddCommunicationToCase operation to add additional communication or\n attachments to an existing case.

\n

The caseId is separate from the displayId that appears in\n the Amazon Web Services Support Center. Use the DescribeCases operation to get the displayId.

\n \n \n " + "smithy.api#documentation": "

Creates a case in the Amazon Web Services Support Center. This operation is similar to how you create a case\n in the Amazon Web Services Support Center Create\n Case page.

\n

The Amazon Web Services Support API doesn't support requesting service limit increases. You can submit a\n service limit increase in the following ways:

\n \n \n

Amazon Web Services Support automatically redacts sensitive information from support cases to protect your data. The following information is replaced with [REDACTED_BY_Amazon Web Services] and is not stored:

\n \n

This sensitive information is never required by Amazon Web Services Support.

\n
\n

A successful CreateCase request returns a Amazon Web Services Support case number. You can use\n the DescribeCases operation and specify the case number to get\n existing Amazon Web Services Support cases. After you create a case, use the AddCommunicationToCase operation to add additional communication or\n attachments to an existing case.

\n

The caseId is separate from the displayId that appears in\n the Amazon Web Services Support Center. Use the DescribeCases operation to get the displayId.

\n \n \n " } }, "com.amazonaws.support#CreateCaseRequest": { @@ -2164,7 +2309,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } }, "issueType": { @@ -2176,7 +2321,19 @@ "attachmentSetId": { "target": "com.amazonaws.support#AttachmentSetId", "traits": { - "smithy.api#documentation": "

The ID of a set of one or more attachments for the case. Create the set by using the\n AddAttachmentsToSet operation.

" + "smithy.api#documentation": "

The ID of a set of one or more attachments for the case. Create the set by using the\n AddAttachmentsToSet operation. Each attachment in the set must be 5\n MB or smaller. To attach files larger than 5 MB, use uploadIds.

" + } + }, + "uploadIds": { + "target": "com.amazonaws.support#UploadIds", + "traits": { + "smithy.api#documentation": "

A list of upload IDs that identify attachments to add to the case. Each\n uploadId is returned by the GetAttachmentUploadLinks\n operation. The upload must reach the attachment-ready state by calling CompleteAttachmentUpload before it can be passed here.\n Use\n uploadIds to attach files of any supported size, including files larger than\n 5 MB.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually creating the case. When set to\n true, the request is validated but no case is created, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" } } }, @@ -2190,7 +2347,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string in the following format:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string in the following format:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } } }, @@ -2243,12 +2400,15 @@ { "target": "com.amazonaws.support#DescribeAttachmentLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the attachment that has the specified ID. Attachments can include screenshots,\n error logs, or other files that describe your issue. Attachment IDs are generated by the\n case management system when you add an attachment to a case or case communication.\n Attachment IDs are returned in the AttachmentDetails objects that are\n returned by the DescribeCommunications operation.

\n \n \n " + "smithy.api#documentation": "

Returns the attachment that has the specified ID. Attachments can include screenshots,\n error logs, or other files that describe your issue. Attachment IDs are generated by the\n case management system when you add an attachment to a case or case communication.\n Attachment IDs are returned in the AttachmentDetails objects that are\n returned by the DescribeCommunications operation.

\n \n \n \n \n

\n DescribeAttachment can't return attachments larger than 5 MB. If the\n specified attachmentId refers to an attachment larger than 5 MB, the\n request fails with InvalidParameterValueException.

\n

To download an attachment of any size, including attachments larger than 5 MB, use\n GetAttachmentDownloadLink.\n GetAttachmentDownloadLink returns an Amazon S3 presigned URL that you can\n use to download the attachment directly.

\n
" } }, "com.amazonaws.support#DescribeAttachmentLimitExceeded": { @@ -2272,9 +2432,15 @@ "attachmentId": { "target": "com.amazonaws.support#AttachmentId", "traits": { - "smithy.api#documentation": "

The ID of the attachment to return. Attachment IDs are returned by the DescribeCommunications operation.

", + "smithy.api#documentation": "

The ID of the attachment to return. Attachment IDs are returned by the DescribeCommunications operation.

\n

If the specified attachment is larger than 5 MB, this operation returns\n InvalidParameterValueException. To download attachments larger than 5\n MB, use GetAttachmentDownloadLink.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually retrieving the attachment. When\n set to true, the request is validated but no attachment content is returned, and\n the operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2296,6 +2462,78 @@ "smithy.api#output": {} } }, + "com.amazonaws.support#DescribeAttachmentUploadStatus": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatusRequest" + }, + "output": { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatusResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Returns the current status, file name, and progress of a multipart attachment upload that\n was started with GetAttachmentUploadLinks. Use this operation to track\n where an upload is in the workflow. While parts are still being uploaded and reported through\n CompleteAttachmentUpload, the uploadStatus is\n attachment-not-ready and uploadProgress reports the total number\n of parts and how many have been completed so far. After every part has been reported and the\n service finishes processing the upload asynchronously, the uploadStatus becomes\n attachment-ready and the uploadId can be attached to a case\n through CreateCase or AddCommunicationToCase.

\n \n \n " + } + }, + "com.amazonaws.support#DescribeAttachmentUploadStatusRequest": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier for the upload. The uploadId is returned by\n GetAttachmentUploadLinks when you initiate the upload.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning upload status. When\n set to true, the request is validated but no status is returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#DescribeAttachmentUploadStatusResponse": { + "type": "structure", + "members": { + "uploadStatus": { + "target": "com.amazonaws.support#UploadStatus", + "traits": { + "smithy.api#documentation": "

The current status of the multipart upload. Valid values: attachment-ready,\n attachment-not-ready, and failed.

", + "smithy.api#required": {} + } + }, + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the file being uploaded, including the file extension.

", + "smithy.api#required": {} + } + }, + "uploadProgress": { + "target": "com.amazonaws.support#UploadProgress", + "traits": { + "smithy.api#documentation": "

The progress of the multipart upload, including the total number of parts and the number\n of parts that have been successfully uploaded.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, "com.amazonaws.support#DescribeCases": { "type": "operation", "input": { @@ -2308,12 +2546,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns a list of cases that you specify by passing one or more case IDs. You can use\n the afterTime and beforeTime parameters to filter the cases by\n date. You can set values for the includeResolvedCases and\n includeCommunications parameters to specify how much information to\n return.

\n

The response returns the following in JSON format:

\n \n

Case data is available for 12 months after creation. If a case was created more than\n 12 months ago, a request might return an error.

\n \n \n ", + "smithy.api#documentation": "

Returns a list of cases that you specify by passing one or more case IDs. You can use\n the afterTime and beforeTime parameters to filter the cases by\n date. You can set values for the includeResolvedCases and\n includeCommunications parameters to specify how much information to\n return.

\n

The response returns the following in JSON format:

\n \n

Case data is available for 24 months after creation. If a case was created more than\n 24 months ago, a request might return an error.

\n \n \n \n \n

Each Communication returned by this operation includes\n attachment information in two fields:

\n \n

Amazon Web Services recommends that you use the attachments field and download each\n attachment with GetAttachmentDownloadLink, which supports\n attachments of any size. The attachmentSet field and DescribeAttachment return only attachments that are 5 MB or\n smaller.

\n
", "smithy.api#paginated": { "inputToken": "nextToken", "outputToken": "nextToken", @@ -2340,13 +2581,13 @@ "afterTime": { "target": "com.amazonaws.support#AfterTime", "traits": { - "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "beforeTime": { "target": "com.amazonaws.support#BeforeTime", "traits": { - "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "includeResolvedCases": { @@ -2371,7 +2612,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } }, "includeCommunications": { @@ -2379,6 +2620,12 @@ "traits": { "smithy.api#documentation": "

Specifies whether to include communications in the DescribeCases\n response. By default, communications are included.

" } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning case data. When set\n to true, the request is validated but no cases are returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } } }, "traits": { @@ -2418,12 +2665,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns communications and attachments for one or more support cases. Use the\n afterTime and beforeTime parameters to filter by date. You\n can use the caseId parameter to restrict the results to a specific\n case.

\n

Case data is available for 12 months after creation. If a case was created more than\n 12 months ago, a request for data might cause an error.

\n

You can use the maxResults and nextToken parameters to\n control the pagination of the results. Set maxResults to the number of\n cases that you want to display on each page, and use nextToken to specify\n the resumption of pagination.

\n \n \n ", + "smithy.api#documentation": "

Returns communications and attachments for one or more support cases. Use the\n afterTime and beforeTime parameters to filter by date. You\n can use the caseId parameter to restrict the results to a specific\n case.

\n

Case data is available for 24 months after creation. If a case was created more than\n 24 months ago, a request for data might cause an error.

\n

You can use the maxResults and nextToken parameters to\n control the pagination of the results. Set maxResults to the number of\n cases that you want to display on each page, and use nextToken to specify\n the resumption of pagination.

\n \n \n \n \n

Each Communication returned by this operation includes\n attachment information in two fields:

\n \n

Amazon Web Services recommends that you use the attachments field and download each\n attachment with GetAttachmentDownloadLink, which supports\n attachments of any size. The attachmentSet field and DescribeAttachment return only attachments that are 5 MB or\n smaller.

\n
", "smithy.api#paginated": { "inputToken": "nextToken", "outputToken": "nextToken", @@ -2438,20 +2688,20 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

", + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

", "smithy.api#required": {} } }, "beforeTime": { "target": "com.amazonaws.support#BeforeTime", "traits": { - "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "afterTime": { "target": "com.amazonaws.support#AfterTime", "traits": { - "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "nextToken": { @@ -2465,6 +2715,12 @@ "traits": { "smithy.api#documentation": "

The maximum number of results to return before paginating.

" } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning communications. When\n set to true, the request is validated but no communications are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2501,6 +2757,9 @@ "target": "com.amazonaws.support#DescribeCreateCaseOptionsResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" }, @@ -2509,7 +2768,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns a list of CreateCaseOption types along with the \n corresponding supported hours and language availability. You can specify the language\n categoryCode, \n issueType and serviceCode used to retrieve the CreateCaseOptions.

\n \n \n " + "smithy.api#documentation": "

Returns a list of CreateCaseOption types along with the \n corresponding supported hours and language availability. You can specify the language\n categoryCode, \n issueType and serviceCode used to retrieve the CreateCaseOptions.

\n \n \n " } }, "com.amazonaws.support#DescribeCreateCaseOptionsRequest": { @@ -2532,7 +2791,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

", + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

", "smithy.api#required": {} } }, @@ -2542,6 +2801,12 @@ "smithy.api#documentation": "

The category of problem for the support case. You also use the DescribeServices operation to get the category code for a service. Each\n Amazon Web Services service defines its own set of category codes.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning case option data.\n When set to true, the request is validated but no options are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2577,12 +2842,15 @@ "target": "com.amazonaws.support#DescribeServicesResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the current list of Amazon Web Services services and a list of service categories for each\n service. You then use service names and categories in your CreateCase\n requests. Each Amazon Web Services service has its own set of categories.

\n

The service codes and category codes correspond to the values that appear in the\n Service and Category lists on the Amazon Web Services Support Center Create Case page. The values in those fields\n don't necessarily match the service codes and categories returned by the\n DescribeServices operation. Always use the service codes and categories\n that the DescribeServices operation returns, so that you have the most\n recent set of service and category codes.

\n \n \n " + "smithy.api#documentation": "

Returns the current list of Amazon Web Services services and a list of service categories for each\n service. You then use service names and categories in your CreateCase\n requests. Each Amazon Web Services service has its own set of categories.

\n

The service codes and category codes correspond to the values that appear in the\n Service and Category lists on the Amazon Web Services Support Center Create Case page. The values in those fields\n don't necessarily match the service codes and categories returned by the\n DescribeServices operation. Always use the service codes and categories\n that the DescribeServices operation returns, so that you have the most\n recent set of service and category codes.

\n \n \n " } }, "com.amazonaws.support#DescribeServicesRequest": { @@ -2597,7 +2865,13 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning the list of services.\n When set to true, the request is validated but no services are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" } } }, @@ -2629,12 +2903,15 @@ "target": "com.amazonaws.support#DescribeSeverityLevelsResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the list of severity levels that you can assign to a support case. The\n severity level for a case is also a field in the CaseDetails data type\n that you include for a CreateCase request.

\n \n \n " + "smithy.api#documentation": "

Returns the list of severity levels that you can assign to a support case. The\n severity level for a case is also a field in the CaseDetails data type\n that you include for a CreateCase request.

\n \n \n " } }, "com.amazonaws.support#DescribeSeverityLevelsRequest": { @@ -2643,7 +2920,13 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning severity levels. When\n set to true, the request is validated but no severity levels are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" } } }, @@ -2675,6 +2958,9 @@ "target": "com.amazonaws.support#DescribeSupportedLanguagesResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" }, @@ -2683,7 +2969,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns a list of supported languages for a specified categoryCode, \n issueType and serviceCode. The returned supported languages will \n include a ISO 639-1 code for the language, and the language display name.

\n \n \n " + "smithy.api#documentation": "

Returns a list of supported languages for a specified categoryCode, \n issueType and serviceCode. The returned supported languages will \n include a ISO 639-1 code for the language, and the language display name.

\n \n \n " } }, "com.amazonaws.support#DescribeSupportedLanguagesRequest": { @@ -2709,6 +2995,12 @@ "smithy.api#documentation": "

The category of problem for the support case. You also use the DescribeServices operation to get the category code for a service. Each\n Amazon Web Services service defines its own set of category codes.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning supported languages.\n When set to true, the request is validated but no languages are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2746,7 +3038,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the refresh status of the Trusted Advisor checks that have the specified check\n IDs. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

Some checks are refreshed automatically, and you can't return their refresh statuses\n by using the DescribeTrustedAdvisorCheckRefreshStatuses operation. If you\n call this operation for these checks, you might see an\n InvalidParameterValue error.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the refresh status of the Trusted Advisor checks that have the specified check\n IDs. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

Some checks are refreshed automatically, and you can't return their refresh statuses\n by using the DescribeTrustedAdvisorCheckRefreshStatuses operation. If you\n call this operation for these checks, you might see an\n InvalidParameterValue error.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckRefreshStatusesRequest": { @@ -2797,7 +3089,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the results of the Trusted Advisor check that has the specified check ID. You\n can get the check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

The response contains a TrustedAdvisorCheckResult object, which\n contains these three objects:

\n \n

In addition, the response contains these fields:

\n \n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the results of the Trusted Advisor check that has the specified check ID. You\n can get the check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

The response contains a TrustedAdvisorCheckResult object, which\n contains these three objects:

\n \n

In addition, the response contains these fields:

\n \n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckResultRequest": { @@ -2854,7 +3146,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the results for the Trusted Advisor check summaries for the check IDs that you\n specified. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

The response contains an array of TrustedAdvisorCheckSummary\n objects.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the results for the Trusted Advisor check summaries for the check IDs that you\n specified. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

The response contains an array of TrustedAdvisorCheckSummary\n objects.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

\n

\n Understanding the Trusted Advisor Resources processed value\n

\n

The Resources processed value, resourcesProcessed, usually shows both flagged resources (those with warnings or errors) and resources in good standing (ok status resources). However, some checks report flagged resources only. To understand what a specific check reports, review the detailed check information in the Trusted Advisor check reference. If you see a Green criterion listed in the Alert criteria, then the check reports all resources. If there's no Green criterion listed in the Alert criteria, then the check reports only flagged resources. For example, the Amazon EC2 Reserved Instance optimization check (cX3c2R1chu) doesn't list a Green criterion in the Alert criteria. So, this check only reports flagged resources.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckSummariesRequest": { @@ -2905,7 +3197,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns information about all available Trusted Advisor checks, including the name, ID,\n category, description, and metadata. You must specify a language code.

\n

The response contains a TrustedAdvisorCheckDescription object for\n each check. You must set the Amazon Web Services Region to us-east-1.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns information about all available Trusted Advisor checks, including the name, ID,\n category, description, and metadata. You must specify a language code.

\n

The response contains a TrustedAdvisorCheckDescription object for\n each check. You must set the Amazon Web Services Region to us-east-1.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorChecksRequest": { @@ -2951,6 +3243,57 @@ "smithy.api#default": 0 } }, + "com.amazonaws.support#DownloadUrl": { + "type": "structure", + "members": { + "url": { + "target": "com.amazonaws.support#HttpsUrl", + "traits": { + "smithy.api#documentation": "

The presigned HTTPS URL that you can use to download the attachment. Download URLs are\n served from downloadv1.attachments.support.{region}.amazonaws.com. The\n downloadv1 prefix is subject to change.

", + "smithy.api#required": {} + } + }, + "expiryDate": { + "target": "com.amazonaws.support#ValidatedDateTime", + "traits": { + "smithy.api#documentation": "

The date and time, in ISO-8601 format, when the presigned URL expires. Download the\n attachment before this time.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A presigned URL for downloading an attachment, along with the date and time the URL\n expires. Returned by GetAttachmentDownloadLink.

" + } + }, + "com.amazonaws.support#DryRunOperationException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.support#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The request was valid, but the operation wasn't performed because dryRun was\n set to true.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.support#ETag": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + } + } + }, + "com.amazonaws.support#EndIndex": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 2 + } + } + }, "com.amazonaws.support#EndTime": { "type": "string" }, @@ -2960,9 +3303,202 @@ "com.amazonaws.support#ExpiryTime": { "type": "string" }, + "com.amazonaws.support#FieldIntegerValue": { + "type": "integer" + }, "com.amazonaws.support#FileName": { "type": "string" }, + "com.amazonaws.support#FileSize": { + "type": "long", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 157286400 + } + } + }, + "com.amazonaws.support#GetAttachmentDownloadLink": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#GetAttachmentDownloadLinkRequest" + }, + "output": { + "target": "com.amazonaws.support#GetAttachmentDownloadLinkResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#AttachmentIdNotFound" + }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + } + ], + "traits": { + "smithy.api#documentation": "

Returns a presigned download URL for an attachment that is associated with a case\n communication. The download link works for an attachment of any size, including attachments\n added through AddAttachmentsToSet and attachments uploaded through GetAttachmentUploadLinks. The download URL is time-limited and expires at the\n date and time indicated in the downloadUrl response field. Download the\n attachment from the URL before it expires.

\n \n \n " + } + }, + "com.amazonaws.support#GetAttachmentDownloadLinkRequest": { + "type": "structure", + "members": { + "attachmentId": { + "target": "com.amazonaws.support#AttachmentId", + "traits": { + "smithy.api#documentation": "

The unique identifier of the attachment for which to retrieve a download link. Attachment\n IDs are returned in the AttachmentDetails objects in the attachments\n field of a Communication returned by DescribeCommunications\n or DescribeCases.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning a download link. When\n set to true, the request is validated but no URL is returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#GetAttachmentDownloadLinkResponse": { + "type": "structure", + "members": { + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the attachment file, including the file extension.

", + "smithy.api#required": {} + } + }, + "downloadUrl": { + "target": "com.amazonaws.support#DownloadUrl", + "traits": { + "smithy.api#documentation": "

The presigned download URL and the date and time the URL expires.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#GetAttachmentUploadLinks": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#GetAttachmentUploadLinksRequest" + }, + "output": { + "target": "com.amazonaws.support#GetAttachmentUploadLinksResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Returns one or more presigned upload URLs for uploading a large file attachment to a\n support case by using a multipart upload workflow. The maximum file size that you can upload\n with this workflow is 150 MB, and parts can be up to 100 MB each. Initiate a new upload by\n providing fileName and fileSizeBytes; the response returns a unique\n uploadId, the part size, the total number of parts, and a list of presigned\n upload URLs for the requested range of parts. A maximum of 10 upload URLs are returned per\n call. To retrieve more upload URLs for an upload\n that's already in progress, call GetAttachmentUploadLinks again with the existing\n uploadId and a new uploadRange.

\n

Upload each part to its presigned URL by using HTTP PUT and capture the ETag\n from the response. After you upload all parts, call CompleteAttachmentUpload\n with the uploadId and the list of part indexes and ETags to finalize the upload.\n You can then attach the upload to a case by passing the uploadId in the\n uploadIds parameter of CreateCase or AddCommunicationToCase. To monitor progress before completion, call DescribeAttachmentUploadStatus.

\n \n \n " + } + }, + "com.amazonaws.support#GetAttachmentUploadLinksRequest": { + "type": "structure", + "members": { + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the file to upload, including the file extension. This value is required when\n you initiate a new upload.

", + "smithy.api#required": {} + } + }, + "fileSizeBytes": { + "target": "com.amazonaws.support#FileSize", + "traits": { + "smithy.api#documentation": "

The total size of the file in bytes. The service uses this value to calculate the total\n number of parts and the size of each part. Required when you initiate a new upload (when\n uploadId isn't provided). Valid range: 1 to 157,286,400 bytes (approximately\n 150 MB).

" + } + }, + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier of an in-progress multipart upload, returned by a previous call to\n GetAttachmentUploadLinks. Specify uploadId to retrieve additional\n presigned upload URLs for an upload that has already been initiated. Required when\n fileSizeBytes isn't provided. Length: 1 to 2,048 characters.

" + } + }, + "uploadRange": { + "target": "com.amazonaws.support#UploadRange", + "traits": { + "smithy.api#documentation": "

The range of part indexes for which to return presigned upload URLs. Use this parameter\n to page through the upload URLs for a large file across multiple calls. If you omit this\n parameter, the service determines the range to return.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually generating upload URLs. When\n set to true, the request is validated but no URLs are returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#GetAttachmentUploadLinksResponse": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier for the multipart upload. Use this value in subsequent calls to\n GetAttachmentUploadLinks, DescribeAttachmentUploadStatus,\n and CompleteAttachmentUpload, and to attach the upload to a case through the\n uploadIds parameter on CreateCase or AddCommunicationToCase.

", + "smithy.api#required": {} + } + }, + "partSizeBytes": { + "target": "com.amazonaws.support#PartSizeBytes", + "traits": { + "smithy.api#documentation": "

The size, in bytes, of each part. Split the file into parts of this size before you upload\n them to the presigned URLs. For an upload with n total parts, parts 1 through\n n - 1 are exactly this size; the last part may be smaller. Maximum:\n 104,857,600 bytes (approximately 100 MB).

", + "smithy.api#required": {} + } + }, + "totalParts": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The total number of parts that the file is split into. Upload one part to each presigned\n URL.

", + "smithy.api#required": {} + } + }, + "nextIndex": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The next part index to request presigned URLs for. If all upload URLs for the file have\n been returned, this field is null. Use this value as the startIndex in\n uploadRange on a subsequent call to GetAttachmentUploadLinks to\n retrieve the next batch of upload URLs.

" + } + }, + "uploadUrls": { + "target": "com.amazonaws.support#UploadUrlList", + "traits": { + "smithy.api#documentation": "

The list of presigned upload URLs for the requested range of parts. The list contains at\n most 10 URLs per call. Upload each part to its corresponding URL by using HTTP\n PUT before the URL expires.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#HttpsUrl": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 8 + }, + "smithy.api#pattern": "^https://[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]\\.[a-zA-Z]{2,}(/.*)?$" + } + }, "com.amazonaws.support#IncludeCommunications": { "type": "boolean" }, @@ -2972,6 +3508,12 @@ "smithy.api#default": false } }, + "com.amazonaws.support#Integer": { + "type": "integer", + "traits": { + "smithy.api#default": 0 + } + }, "com.amazonaws.support#InternalServerError": { "type": "structure", "members": { @@ -3011,6 +3553,18 @@ "com.amazonaws.support#NextToken": { "type": "string" }, + "com.amazonaws.support#NullableBooleanType": { + "type": "boolean" + }, + "com.amazonaws.support#PartSizeBytes": { + "type": "long", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 104857600 + } + } + }, "com.amazonaws.support#RecentCaseCommunications": { "type": "structure", "members": { @@ -3045,7 +3599,7 @@ } ], "traits": { - "smithy.api#documentation": "

Refreshes the Trusted Advisor check that you specify using the check ID. You can get the\n check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

Some checks are refreshed automatically. If you call the\n RefreshTrustedAdvisorCheck operation to refresh them, you might see\n the InvalidParameterValue error.

\n

The response contains a TrustedAdvisorCheckRefreshStatus\n object.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Refreshes the Trusted Advisor check that you specify using the check ID. You can get the\n check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

Some checks are refreshed automatically. If you call the\n RefreshTrustedAdvisorCheck operation to refresh them, you might see\n the InvalidParameterValue error.

\n

The response contains a TrustedAdvisorCheckRefreshStatus\n object.

\n \n \n \n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#RefreshTrustedAdvisorCheckRequest": { @@ -3092,12 +3646,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Resolves a support case. This operation takes a caseId and returns the\n initial and final state of the case.

\n \n \n " + "smithy.api#documentation": "

Resolves a support case. This operation takes a caseId and returns the\n initial and final state of the case.

\n \n \n " } }, "com.amazonaws.support#ResolveCaseRequest": { @@ -3106,7 +3663,13 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually resolving the case. When set\n to true, the request is validated but the case isn't resolved, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" } } }, @@ -3232,6 +3795,14 @@ "target": "com.amazonaws.support#SeverityLevel" } }, + "com.amazonaws.support#StartIndex": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1 + } + } + }, "com.amazonaws.support#StartTime": { "type": "string" }, @@ -3325,6 +3896,12 @@ "members": { "message": { "target": "com.amazonaws.support#AvailabilityErrorMessage" + }, + "throttlingReasons": { + "target": "com.amazonaws.support#ThrottlingReasonList", + "traits": { + "smithy.api#documentation": "

A list of one or more reasons that the request was throttled.

" + } } }, "traits": { @@ -3337,6 +3914,32 @@ "smithy.api#httpError": 400 } }, + "com.amazonaws.support#ThrottlingReason": { + "type": "structure", + "members": { + "reason": { + "target": "com.amazonaws.support#CoralAvailabilityThrottlingReason", + "traits": { + "smithy.api#documentation": "

The reason that the request was throttled.

" + } + }, + "resource": { + "target": "com.amazonaws.support#CoralAvailabilityThrottledResource", + "traits": { + "smithy.api#documentation": "

The resource that caused the request to be throttled.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Information about why a request was throttled.

" + } + }, + "com.amazonaws.support#ThrottlingReasonList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#ThrottlingReason" + } + }, "com.amazonaws.support#TimeCreated": { "type": "string" }, @@ -3658,6 +4261,139 @@ "com.amazonaws.support#Type": { "type": "string" }, + "com.amazonaws.support#UploadId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + } + } + }, + "com.amazonaws.support#UploadIdNotFound": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.support#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The specified uploadId couldn't be located.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.support#UploadIds": { + "type": "list", + "member": { + "target": "com.amazonaws.support#UploadId" + }, + "traits": { + "smithy.api#length": { + "min": 0, + "max": 10 + } + } + }, + "com.amazonaws.support#UploadProgress": { + "type": "structure", + "members": { + "totalParts": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The total number of parts that the file is split into.

" + } + }, + "completedPartsCount": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The number of parts that have been successfully uploaded.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The progress of a multipart attachment upload, returned by DescribeAttachmentUploadStatus.

" + } + }, + "com.amazonaws.support#UploadRange": { + "type": "structure", + "members": { + "startIndex": { + "target": "com.amazonaws.support#StartIndex", + "traits": { + "smithy.api#documentation": "

The starting part index of the range, inclusive. Part indexes start at 1.

", + "smithy.api#required": {} + } + }, + "endIndex": { + "target": "com.amazonaws.support#EndIndex", + "traits": { + "smithy.api#documentation": "

The ending part index of the range, exclusive. The range is half-open:\n startIndex is inclusive and endIndex is exclusive. For example,\n a range with startIndex of 1 and endIndex of 4 requests URLs for\n parts 1, 2, and 3. The range size (endIndex - startIndex)\n must not exceed 10. If you omit endIndex, the service defaults to\n startIndex + 10, capped by the total number of parts.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The range of part indexes for which to return presigned upload URLs from GetAttachmentUploadLinks.

" + } + }, + "com.amazonaws.support#UploadStatus": { + "type": "enum", + "members": { + "ATTACHMENT_READY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "attachment-ready" + } + }, + "ATTACHMENT_NOT_READY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "attachment-not-ready" + } + }, + "FAILED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "failed" + } + } + } + }, + "com.amazonaws.support#UploadUrl": { + "type": "structure", + "members": { + "url": { + "target": "com.amazonaws.support#HttpsUrl", + "traits": { + "smithy.api#documentation": "

The presigned HTTPS URL that you use to upload a single part with HTTP\n PUT. Upload URLs are served from\n uploadv1.attachments.support.{region}.amazonaws.com. The\n uploadv1 prefix is subject to change.

", + "smithy.api#required": {} + } + }, + "partIndex": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The index of the part that this URL uploads.

", + "smithy.api#required": {} + } + }, + "expiryDate": { + "target": "com.amazonaws.support#ValidatedDateTime", + "traits": { + "smithy.api#documentation": "

The date and time, in ISO-8601 format, when the presigned URL expires. Upload the part\n before this time.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A presigned URL for uploading a single part of a multipart attachment upload, along with\n the part index and the date and time the URL expires. Returned by GetAttachmentUploadLinks.

" + } + }, + "com.amazonaws.support#UploadUrlList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#UploadUrl" + } + }, "com.amazonaws.support#ValidatedCategoryCode": { "type": "string", "traits": { diff --git a/aws-models/transcribe.json b/aws-models/transcribe.json index e26c4d7d8..6956d3cb0 100644 --- a/aws-models/transcribe.json +++ b/aws-models/transcribe.json @@ -5323,6 +5323,114 @@ "smithy.api#enumValue": "SSN" } }, + "DATE_TIME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DATE_TIME" + } + }, + "PASSPORT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PASSPORT_NUMBER" + } + }, + "DRIVER_ID": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DRIVER_ID" + } + }, + "URL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "URL" + } + }, + "AGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AGE" + } + }, + "USERNAME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "USERNAME" + } + }, + "PASSWORD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PASSWORD" + } + }, + "AWS_ACCESS_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_ACCESS_KEY" + } + }, + "AWS_SECRET_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_SECRET_KEY" + } + }, + "IP_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "IP_ADDRESS" + } + }, + "MAC_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MAC_ADDRESS" + } + }, + "LICENSE_PLATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "LICENSE_PLATE" + } + }, + "VEHICLE_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VEHICLE_IDENTIFICATION_NUMBER" + } + }, + "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER" + } + }, + "CA_HEALTH_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_HEALTH_NUMBER" + } + }, + "CA_SOCIAL_INSURANCE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_SOCIAL_INSURANCE_NUMBER" + } + }, + "INTERNATIONAL_BANK_ACCOUNT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INTERNATIONAL_BANK_ACCOUNT_NUMBER" + } + }, + "SWIFT_CODE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SWIFT_CODE" + } + }, "ALL": { "target": "smithy.api#Unit", "traits": { @@ -5339,7 +5447,7 @@ "traits": { "smithy.api#length": { "min": 0, - "max": 11 + "max": 29 } } }, diff --git a/aws-models/transfer.json b/aws-models/transfer.json index 3bfbf488a..51e136cc6 100644 --- a/aws-models/transfer.json +++ b/aws-models/transfer.json @@ -7964,6 +7964,28 @@ } } }, + "com.amazonaws.transfer#SecretVersionStage": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + } + } + }, + "com.amazonaws.transfer#SecretVersionStageList": { + "type": "list", + "member": { + "target": "com.amazonaws.transfer#SecretVersionStage" + }, + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2 + }, + "smithy.api#uniqueItems": {} + } + }, "com.amazonaws.transfer#SecurityGroupId": { "type": "string", "traits": { @@ -8306,6 +8328,12 @@ "smithy.api#default": 1, "smithy.api#documentation": "

Specify the number of concurrent connections that your connector creates to the remote server. The default value is 1. The maximum values is 5.

If you are using the Amazon Web Services Management Console, the default value is 5.

This parameter specifies the number of active connections that your connector can establish with the remote server at the same time. Increasing this value can enhance connector performance when transferring large file batches by enabling parallel operations.

" } + }, + "OrderedUserSecretVersionStages": { + "target": "com.amazonaws.transfer#SecretVersionStageList", + "traits": { + "smithy.api#documentation": "

An ordered list of Amazon Web Services Secrets Manager version stages (staging labels, such as AWSCURRENT and AWSPREVIOUS) for the secret identified by UserSecretId. When establishing a connection, the connector attempts to retrieve the SFTP user's credentials from each version stage in the order listed, and uses the first version it can successfully retrieve. This lets you rotate the user secret without interrupting connector operations.

" + } } }, "traits": { @@ -9472,7 +9500,7 @@ "name": "transfer" }, "aws.protocols#awsJson1_1": {}, - "smithy.api#documentation": "

Transfer Family is a fully managed service that enables the transfer of files over the File Transfer Protocol (FTP), File Transfer Protocol over SSL (FTPS), or Secure Shell (SSH) File Transfer Protocol (SFTP) directly into and out of Amazon Simple Storage Service (Amazon S3) or Amazon EFS. Additionally, you can use Applicability Statement 2 (AS2) to transfer files into and out of Amazon S3. Amazon Web Services helps you seamlessly migrate your file transfer workflows to Transfer Family by integrating with existing authentication systems, and providing DNS routing with Amazon Route 53 so nothing changes for your customers and partners, or their applications. With your data in Amazon S3, you can use it with Amazon Web Services services for processing, analytics, machine learning, and archiving. Getting started with Transfer Family is easy since there is no infrastructure to buy and set up.

", + "smithy.api#documentation": "

Transfer Family offers fully managed support for the transfer of files over SFTP, AS2, FTPS, FTP, and web browser-based transfers directly into and out of Amazon Web Services storage services.

File transfer protocols are used in data exchange workflows across different industries such as financial services, healthcare, advertising, and retail, among others. Transfer Family simplifies the migration of file transfer workflows to Amazon Web Services.

To use the Transfer Family service, you instantiate a server in the Amazon Web Services Region of your choice. You can create the server, list available servers, and update and delete servers. The server is the entity that requests file operations from Transfer Family. Servers have a number of important properties. The server is a named instance as identified by a system assigned ServerId identifier. You can optionally assign a hostname, or even a custom hostname to a server. The service bills for any instantiated servers (even ones OFFLINE), and for the amount of data transferred.

Users must be known to the server that requests file operations. A user as identified by their username is assigned to a server. Usernames are used to authenticate requests. A server can have only one authentication method: AWS_DIRECTORY_SERVICE, SERVICE_MANAGED, AWS_LAMBDA, or API_GATEWAY.

Transfer Family also supports web applications that provide browser-based file transfer capabilities. Web applications can be configured with VPC endpoints to enable secure, private connectivity within your Virtual Private Cloud (VPC). This allows you to control network access and route traffic through your VPC infrastructure while maintaining the managed benefits of Transfer Family.

This API interface reference for Transfer Family contains documentation for a programming interface that you can use to manage Transfer Family. The reference structure is as follows:

Rather than actually running a command, you can use the --generate-cli-skeleton parameter with any API call to generate and display a parameter template. You can then use the generated template to customize and use as input on a later command. For details, see Generate and use a parameter skeleton file.

", "smithy.api#title": "AWS Transfer Family", "smithy.rules#endpointBdd": { "version": "1.1", From 1f151474baa9b9f91536bfd4795dc68d6ebd3134 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sun, 13 Sep 2026 14:22:59 -0300 Subject: [PATCH 2/3] feat(models): implement the operations the weekly aws-models refresh added The refresh drifted 75 operation checksums across bedrock, ec2, ecs, kinesis, lambda, ses, and transfer, and added 14 brand-new operations to already implemented services. Fix the drift and implement every new operation for real. Kinesis delivery channels (CreateChannel, DescribeChannel, ListChannels, UpdateChannel, DeleteChannel): channels fan records from one or more source streams into an S3 bucket or into Iceberg tables on S3 Tables. Destination, freshness, encryption, logging, and tags are validated against the model, persisted, and round-tripped; the ARN carries the channel id the way AWS's own model examples show; a stream cannot be deleted while a channel draws from it. SES v2 identity certificates (AssociateEmailIdentityCertificate, DisassociateEmailIdentityCertificate, ListEmailIdentityCertificates) plus UpdateConfigurationSet: an ACM certificate ARN is associated with a verified identity (per from-address for domain identities), stored with its provisioning status, listed back with pagination, and dropped with the identity. AWS Support attachment uploads (GetAttachmentUploadLinks, CompleteAttachmentUpload, DescribeAttachmentUploadStatus, GetAttachmentDownloadLink) with the presigned data plane behind them: the links point back at the server and really transfer bytes, parts are ETag-checked before they are concatenated, and uploadIds attach to cases. The vendored support and transcribe model copies are re-synced with aws-models. EC2 ValidateSecurityGroupQuotasForInterface: resolves every referenced group out of state and answers from the real rule counts against the per-interface and per-group quotas. Docs, the operations index, and every evergreen count surface move with the implementation: 7,491 -> 7,505 operations (EC2 802, SES 116, Kinesis 44, Support 20). --- AGENTS.md | 2 +- Cargo.lock | 3 + README.md | 2 +- .../src/resource_provisioner/ses.rs | 4 + crates/fakecloud-conformance/tests/bedrock.rs | 4 +- crates/fakecloud-conformance/tests/ec2.rs | 145 ++- crates/fakecloud-conformance/tests/ecs.rs | 10 +- crates/fakecloud-conformance/tests/kinesis.rs | 180 +++- crates/fakecloud-conformance/tests/lambda.rs | 62 +- crates/fakecloud-conformance/tests/ses.rs | 210 +++- crates/fakecloud-conformance/tests/support.rs | 264 +++++ .../fakecloud-conformance/tests/transfer.rs | 6 +- crates/fakecloud-ec2/src/service/mod.rs | 7 +- crates/fakecloud-ec2/src/service/sg.rs | 286 ++++- crates/fakecloud-kinesis/Cargo.toml | 1 + crates/fakecloud-kinesis/src/service.rs | 247 ++++- .../fakecloud-kinesis/src/service_helpers.rs | 637 ++++++++++- crates/fakecloud-kinesis/src/service_tests.rs | 652 ++++++++++++ crates/fakecloud-kinesis/src/state.rs | 279 +++++ crates/fakecloud-server/src/main.rs | 14 + .../src/support_attachments.rs | 229 ++++ crates/fakecloud-ses/src/fanout.rs | 2 + .../src/service/configuration_sets.rs | 81 +- crates/fakecloud-ses/src/service/helpers.rs | 61 ++ .../fakecloud-ses/src/service/identities.rs | 313 ++++++ crates/fakecloud-ses/src/service/mod.rs | 18 + crates/fakecloud-ses/src/service/tests.rs | 471 +++++++++ crates/fakecloud-ses/src/state.rs | 54 + crates/fakecloud-ses/src/v1_helpers.rs | 1 + crates/fakecloud-ses/src/v1_tests.rs | 1 + crates/fakecloud-support/Cargo.toml | 2 + crates/fakecloud-support/model.json | 812 ++++++++++++++- crates/fakecloud-support/src/dataplane.rs | 326 ++++++ crates/fakecloud-support/src/lib.rs | 20 +- crates/fakecloud-support/src/persistence.rs | 59 +- crates/fakecloud-support/src/service.rs | 985 +++++++++++++++++- crates/fakecloud-support/src/shared.rs | 137 ++- crates/fakecloud-support/src/state.rs | 217 +++- crates/fakecloud-support/src/validate.rs | 36 +- crates/fakecloud-transcribe/model.json | 110 +- website/content/docs/about/conformance.md | 2 +- .../content/docs/migration-from-localstack.md | 2 +- website/content/docs/operations/_index.md | 14 + website/content/docs/parity.md | 10 +- website/content/docs/services/_index.md | 2 +- website/content/docs/services/ec2.md | 6 +- website/content/docs/services/kinesis.md | 3 +- website/content/docs/services/ses.md | 5 +- website/content/docs/services/support.md | 53 +- website/content/fake-aws-server.md | 2 +- website/content/faq.md | 4 +- website/content/localstack-alternative.md | 4 +- website/content/supported-services.md | 8 +- website/static/llms-full.txt | 2 +- website/static/llms.txt | 8 +- website/templates/index.html | 4 +- 56 files changed, 6890 insertions(+), 189 deletions(-) create mode 100644 crates/fakecloud-conformance/tests/support.rs create mode 100644 crates/fakecloud-server/src/support_attachments.rs create mode 100644 crates/fakecloud-support/src/dataplane.rs diff --git a/AGENTS.md b/AGENTS.md index 8e77c7a10..0744f56c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Local AWS cloud emulator. Part of the faisca project family. ## Product Context - FakeCloud is a local AWS emulator focused on high-fidelity behavior and AWS-compatible responses. -- Current project state: 105 AWS services, 7,491 operations, 248,557/248,557 Smithy conformance variants pass — true 100% across every implemented service, no flake margin. See [the parity matrix](website/content/docs/parity.md) for the full service-by-service breakdown of control-plane vs data-plane coverage and known limitations. +- Current project state: 105 AWS services, 7,505 operations, 248,557/248,557 Smithy conformance variants pass — true 100% across every implemented service, no flake margin. See [the parity matrix](website/content/docs/parity.md) for the full service-by-service breakdown of control-plane vs data-plane coverage and known limitations. - The broader roadmap prioritizes services that LocalStack keeps behind paid tiers, especially ECS, ELB/ALB, CloudFront, CloudWatch Metrics, and EC2. - Introspection SDKs (Rust, Python, TypeScript, Go, PHP, Java) are already built and maintained for the `/_fakecloud/*` endpoints. diff --git a/Cargo.lock b/Cargo.lock index 3f5bbd2ce..7c713629b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6013,6 +6013,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "uuid", ] [[package]] @@ -6896,11 +6897,13 @@ name = "fakecloud-support" version = "0.44.10" dependencies = [ "async-trait", + "base64 0.22.1", "bytes", "chrono", "fakecloud-core", "fakecloud-persistence", "http 1.4.0", + "md-5 0.10.6", "parking_lot", "regex", "serde", diff --git a/README.md b/README.md index fa509a3a1..d67498ee3 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Works as a drop-in for LocalStack in CI, with Terraform (`endpoints` block), CDK ## Supported services -105 services, 7,491 operations, and true 100% conformance across every implemented service. +105 services, 7,505 operations, and true 100% conformance across every implemented service. Highlights: S3, DynamoDB, SQS, SNS, EventBridge, Lambda, IAM, STS, KMS, Secrets Manager, CloudFormation, SES, Cognito, Kinesis, RDS (6 real engines), ElastiCache, ECS/ECR, EC2, Step Functions, API Gateway v1/v2, Bedrock, and 80+ more. diff --git a/crates/fakecloud-cloudformation/src/resource_provisioner/ses.rs b/crates/fakecloud-cloudformation/src/resource_provisioner/ses.rs index 036a69394..832e24882 100644 --- a/crates/fakecloud-cloudformation/src/resource_provisioner/ses.rs +++ b/crates/fakecloud-cloudformation/src/resource_provisioner/ses.rs @@ -162,6 +162,10 @@ impl ResourceProvisioner { .and_then(|v| v.get("ArchiveArn")) .and_then(|v| v.as_str()) .map(String::from), + message_security_options: props + .get("MessageSecurityOptions") + .filter(|v| v.is_object()) + .cloned(), archiving_options_present: props.get("ArchivingOptions").is_some_and(|v| v.is_object()), }; let mut accounts = self.ses_state.write(); diff --git a/crates/fakecloud-conformance/tests/bedrock.rs b/crates/fakecloud-conformance/tests/bedrock.rs index bd8e6c7c2..997cef35f 100644 --- a/crates/fakecloud-conformance/tests/bedrock.rs +++ b/crates/fakecloud-conformance/tests/bedrock.rs @@ -2266,8 +2266,8 @@ async fn bedrock_converse_stream() { // next SDK refresh. // --------------------------------------------------------------------------- -#[test_action("bedrock", "GetAccountDataRetention", checksum = "9cbe11ec")] -#[test_action("bedrock", "PutAccountDataRetention", checksum = "ff88323b")] +#[test_action("bedrock", "GetAccountDataRetention", checksum = "89658316")] +#[test_action("bedrock", "PutAccountDataRetention", checksum = "98f4d1c0")] #[tokio::test] async fn bedrock_account_data_retention() { let server = TestServer::start().await; diff --git a/crates/fakecloud-conformance/tests/ec2.rs b/crates/fakecloud-conformance/tests/ec2.rs index d9338ce48..2d0f88e1a 100644 --- a/crates/fakecloud-conformance/tests/ec2.rs +++ b/crates/fakecloud-conformance/tests/ec2.rs @@ -2688,7 +2688,7 @@ async fn ec2_describe_instance_attribute() { assert_eq!(r.instance_id(), Some(id.as_str())); } -#[test_action("ec2", "ModifyInstanceAttribute", checksum = "fe95a738")] +#[test_action("ec2", "ModifyInstanceAttribute", checksum = "8d3e7eae")] #[tokio::test] async fn ec2_modify_instance_attribute() { let s = TestServer::start().await; @@ -5219,7 +5219,7 @@ async fn make_cr(c: &aws_sdk_ec2::Client) -> String { .to_string() } -#[test_action("ec2", "CreateCapacityReservation", checksum = "f1bdd159")] +#[test_action("ec2", "CreateCapacityReservation", checksum = "a8679b7b")] #[tokio::test] async fn ec2_create_capacity_reservation() { let s = TestServer::start().await; @@ -5240,7 +5240,7 @@ async fn ec2_create_capacity_reservation() { .starts_with("cr-")); } -#[test_action("ec2", "DescribeCapacityReservations", checksum = "86846084")] +#[test_action("ec2", "DescribeCapacityReservations", checksum = "b4031fa3")] #[tokio::test] async fn ec2_describe_capacity_reservations() { let s = TestServer::start().await; @@ -5286,7 +5286,7 @@ async fn ec2_modify_capacity_reservation() { assert_eq!(r.r#return(), Some(true)); } -#[test_action("ec2", "GetCapacityReservationUsage", checksum = "58a97958")] +#[test_action("ec2", "GetCapacityReservationUsage", checksum = "eb553936")] #[tokio::test] async fn ec2_get_capacity_reservation_usage() { let s = TestServer::start().await; @@ -5412,7 +5412,7 @@ async fn ec2_modify_instance_capacity_reservation_attributes() { assert_eq!(r.r#return(), Some(true)); } -#[test_action("ec2", "CreateCapacityReservationBySplitting", checksum = "6b626c96")] +#[test_action("ec2", "CreateCapacityReservationBySplitting", checksum = "5343c12c")] #[tokio::test] async fn ec2_create_capacity_reservation_by_splitting() { let s = TestServer::start().await; @@ -5428,7 +5428,7 @@ async fn ec2_create_capacity_reservation_by_splitting() { assert!(r.destination_capacity_reservation().is_some()); } -#[test_action("ec2", "MoveCapacityReservationInstances", checksum = "d3c04631")] +#[test_action("ec2", "MoveCapacityReservationInstances", checksum = "816db8da")] #[tokio::test] async fn ec2_move_capacity_reservation_instances() { let s = TestServer::start().await; @@ -5559,7 +5559,7 @@ async fn ec2_describe_capacity_blocks() { assert!(r.capacity_blocks().is_empty()); } -#[test_action("ec2", "PurchaseCapacityBlock", checksum = "c194354d")] +#[test_action("ec2", "PurchaseCapacityBlock", checksum = "9f5f4fd5")] #[tokio::test] async fn ec2_purchase_capacity_block() { let s = TestServer::start().await; @@ -5644,7 +5644,7 @@ async fn ec2_describe_capacity_reservation_topology() { #[test_action( "ec2", "CreateInterruptibleCapacityReservationAllocation", - checksum = "84e54291" + checksum = "58f291b4" )] #[tokio::test] async fn ec2_create_interruptible_capacity_reservation_allocation() { @@ -5661,7 +5661,7 @@ async fn ec2_create_interruptible_capacity_reservation_allocation() { #[test_action( "ec2", "UpdateInterruptibleCapacityReservationAllocation", - checksum = "645c8a4d" + checksum = "6a191018" )] #[tokio::test] async fn ec2_update_interruptible_capacity_reservation_allocation() { @@ -10734,7 +10734,7 @@ async fn ec2_attach_classic_link_vpc() { .unwrap(); } -#[test_action("ec2", "BundleInstance", checksum = "769d9852")] +#[test_action("ec2", "BundleInstance", checksum = "b11a5f30")] #[tokio::test] async fn ec2_bundle_instance() { let s = TestServer::start().await; @@ -10742,7 +10742,7 @@ async fn ec2_bundle_instance() { c.bundle_instance().instance_id("x").send().await.unwrap(); } -#[test_action("ec2", "CancelBundleTask", checksum = "17e14890")] +#[test_action("ec2", "CancelBundleTask", checksum = "97f6e998")] #[tokio::test] async fn ec2_cancel_bundle_task() { let s = TestServer::start().await; @@ -11315,7 +11315,7 @@ async fn ec2_describe_aws_network_performance_metric_subscriptions() { .unwrap(); } -#[test_action("ec2", "DescribeBundleTasks", checksum = "7fe1331b")] +#[test_action("ec2", "DescribeBundleTasks", checksum = "6850d013")] #[tokio::test] async fn ec2_describe_bundle_tasks() { let s = TestServer::start().await; @@ -13056,14 +13056,14 @@ async fn make_ir_association(c: &aws_sdk_ec2::Client, q: &Ec2Query) -> String { xml_value(&body, "ipamInternetRegistryAssociationId") } -#[test_action("ec2", "CreateIpamInternetRegistryAssociation", checksum = "5f34bb6b")] +#[test_action("ec2", "CreateIpamInternetRegistryAssociation", checksum = "a4a63a5d")] #[test_action( "ec2", "DescribeIpamInternetRegistryAssociations", - checksum = "3e5b69be" + checksum = "e794060d" )] -#[test_action("ec2", "EnableIpamInternetRegistryAssociation", checksum = "5b7cab96")] -#[test_action("ec2", "DeleteIpamInternetRegistryAssociation", checksum = "aa2e586a")] +#[test_action("ec2", "EnableIpamInternetRegistryAssociation", checksum = "bd31da14")] +#[test_action("ec2", "DeleteIpamInternetRegistryAssociation", checksum = "4b0b5e55")] #[tokio::test] async fn ec2_ipam_internet_registry_association_lifecycle() { let s = TestServer::start().await; @@ -13443,3 +13443,116 @@ async fn ec2_ipam_route_discovery_and_protection_findings() { .await; assert_eq!(status, 400); } + +// ---- security-group quota validation ---- + +#[test_action( + "ec2", + "ValidateSecurityGroupQuotasForInterface", + checksum = "7bb2ccf7" +)] +#[tokio::test] +async fn ec2_validate_security_group_quotas_for_interface() { + let s = TestServer::start().await; + let c = s.ec2_client().await; + let q = Ec2Query::new(&s); + + let a = make_sg(&c).await; + let b = make_sg(&c).await; + // Rules the validation has to count, so the answer comes from real state. + c.authorize_security_group_ingress() + .group_id(&a) + .ip_permissions( + aws_sdk_ec2::types::IpPermission::builder() + .ip_protocol("tcp") + .from_port(22) + .to_port(22) + .ip_ranges( + aws_sdk_ec2::types::IpRange::builder() + .cidr_ip("10.0.0.0/8") + .build(), + ) + .build(), + ) + .send() + .await + .unwrap(); + + let body = q + .call( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", &a), ("SecurityGroupId.2", &b)], + ) + .await; + // ec2Query envelope: no wrapper, a lowercase , and the + // output member under its Smithy xmlName. + assert!( + body.contains(""), "{body}"); + assert!(body.contains("true"), "{body}"); + + // An id that does not exist is a not-found, not a false answer. + let (status, err) = q + .send( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", &a), ("SecurityGroupId.2", "sg-ghost")], + ) + .await; + assert_eq!(status, 400, "{err}"); + assert!(body_has_code(&err, "InvalidGroup.NotFound"), "{err}"); + + // The same id twice is rejected rather than deduplicated. + let (status, _) = q + .send( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", &a), ("SecurityGroupId.2", &a)], + ) + .await; + assert_eq!(status, 400); + + // More groups than one interface may carry. + let mut over = Vec::new(); + for _ in 0..6 { + over.push(make_sg(&c).await); + } + let params: Vec<(String, String)> = over + .iter() + .enumerate() + .map(|(i, id)| (format!("SecurityGroupId.{}", i + 1), id.clone())) + .collect(); + let pairs: Vec<(&str, &str)> = params + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + let (status, err) = q + .send("ValidateSecurityGroupQuotasForInterface", &pairs) + .await; + assert_eq!(status, 400, "{err}"); + assert!( + body_has_code(&err, "SecurityGroupsPerInterfaceLimitExceeded"), + "{err}" + ); + + // A dry run validates the request and answers nothing. + let body = q + .call( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", &a), ("DryRun", "true")], + ) + .await; + assert!(!body.contains(""), "{body}"); + let (status, _) = q + .send( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", "sg-ghost"), ("DryRun", "true")], + ) + .await; + assert_eq!(status, 400); +} + +/// True when an `ec2Query` error body carries `code` in its `` element. +fn body_has_code(body: &str, code: &str) -> bool { + body.contains(&format!("{code}")) +} diff --git a/crates/fakecloud-conformance/tests/ecs.rs b/crates/fakecloud-conformance/tests/ecs.rs index 77bddb0e7..fda3444c2 100644 --- a/crates/fakecloud-conformance/tests/ecs.rs +++ b/crates/fakecloud-conformance/tests/ecs.rs @@ -1851,7 +1851,7 @@ async fn create_daemon_with_arn(client: &aws_sdk_ecs::Client, name: &str) -> Str resp.daemon_arn().unwrap().to_string() } -#[test_action("ecs", "CreateDaemon", checksum = "8b96e9bf")] +#[test_action("ecs", "CreateDaemon", checksum = "2c40f5eb")] #[tokio::test] async fn ecs_create_daemon() { let server = TestServer::start().await; @@ -1861,7 +1861,7 @@ async fn ecs_create_daemon() { assert!(arn.contains(":daemon/")); } -#[test_action("ecs", "DescribeDaemon", checksum = "af1141c6")] +#[test_action("ecs", "DescribeDaemon", checksum = "25f93304")] #[tokio::test] async fn ecs_describe_daemon() { let server = TestServer::start().await; @@ -1877,7 +1877,7 @@ async fn ecs_describe_daemon() { assert!(resp.daemon().is_some()); } -#[test_action("ecs", "UpdateDaemon", checksum = "87f1f95b")] +#[test_action("ecs", "UpdateDaemon", checksum = "2b41a668")] #[tokio::test] async fn ecs_update_daemon() { let server = TestServer::start().await; @@ -1937,7 +1937,7 @@ async fn create_daemon_get_deployment( ) } -#[test_action("ecs", "DescribeDaemonDeployments", checksum = "d0f1127a")] +#[test_action("ecs", "DescribeDaemonDeployments", checksum = "09589af9")] #[tokio::test] async fn ecs_describe_daemon_deployments() { let server = TestServer::start().await; @@ -1969,7 +1969,7 @@ async fn ecs_list_daemon_deployments() { assert!(!resp.daemon_deployments().is_empty()); } -#[test_action("ecs", "DescribeDaemonRevisions", checksum = "4eb9e0f0")] +#[test_action("ecs", "DescribeDaemonRevisions", checksum = "f3f01047")] #[tokio::test] async fn ecs_describe_daemon_revisions() { let server = TestServer::start().await; diff --git a/crates/fakecloud-conformance/tests/kinesis.rs b/crates/fakecloud-conformance/tests/kinesis.rs index d8abb33f3..42df3bdd1 100644 --- a/crates/fakecloud-conformance/tests/kinesis.rs +++ b/crates/fakecloud-conformance/tests/kinesis.rs @@ -7,10 +7,11 @@ use aws_sdk_kinesis::types::{ }; use fakecloud_conformance_macros::test_action; use helpers::TestServer; +use serde_json::{json, Value}; #[test_action("kinesis", "CreateStream", checksum = "d2d1a234")] -#[test_action("kinesis", "DescribeStream", checksum = "eca54e4c")] -#[test_action("kinesis", "DescribeStreamSummary", checksum = "50667cc4")] +#[test_action("kinesis", "DescribeStream", checksum = "833e726c")] +#[test_action("kinesis", "DescribeStreamSummary", checksum = "4963083e")] #[test_action("kinesis", "ListStreams", checksum = "ca5dcdd7")] #[test_action("kinesis", "DeleteStream", checksum = "51c62afa")] #[tokio::test] @@ -149,7 +150,7 @@ async fn kinesis_tags_and_retention() { assert!(tags.tags().is_empty()); } -#[test_action("kinesis", "PutRecord", checksum = "ebd87879")] +#[test_action("kinesis", "PutRecord", checksum = "55718b65")] #[tokio::test] async fn kinesis_put_record() { let server = TestServer::start().await; @@ -184,7 +185,7 @@ async fn kinesis_put_record() { assert!(first.sequence_number() < second.sequence_number()); } -#[test_action("kinesis", "PutRecords", checksum = "27e5bb6b")] +#[test_action("kinesis", "PutRecords", checksum = "a5b28725")] #[tokio::test] async fn kinesis_put_records() { let server = TestServer::start().await; @@ -226,8 +227,8 @@ async fn kinesis_put_records() { ); } -#[test_action("kinesis", "GetShardIterator", checksum = "8d745e01")] -#[test_action("kinesis", "GetRecords", checksum = "4f940d65")] +#[test_action("kinesis", "GetShardIterator", checksum = "02801846")] +#[test_action("kinesis", "GetRecords", checksum = "a4dec291")] #[tokio::test] async fn kinesis_get_records() { let server = TestServer::start().await; @@ -284,9 +285,9 @@ async fn stream_arn(client: &aws_sdk_kinesis::Client, stream_name: &str) -> Stri .to_string() } -#[test_action("kinesis", "TagResource", checksum = "b9e8db1d")] -#[test_action("kinesis", "ListTagsForResource", checksum = "f215bdf3")] -#[test_action("kinesis", "UntagResource", checksum = "829a3def")] +#[test_action("kinesis", "TagResource", checksum = "58941b22")] +#[test_action("kinesis", "ListTagsForResource", checksum = "0eb8b1e3")] +#[test_action("kinesis", "UntagResource", checksum = "3c1bbd62")] #[tokio::test] async fn kinesis_tag_resource_lifecycle() { let server = TestServer::start().await; @@ -335,9 +336,9 @@ async fn kinesis_tag_resource_lifecycle() { assert!(after.tags().iter().all(|t| t.key() != "env")); } -#[test_action("kinesis", "PutResourcePolicy", checksum = "17a4f058")] -#[test_action("kinesis", "GetResourcePolicy", checksum = "3ed90038")] -#[test_action("kinesis", "DeleteResourcePolicy", checksum = "ba248d13")] +#[test_action("kinesis", "PutResourcePolicy", checksum = "c58d3c4b")] +#[test_action("kinesis", "GetResourcePolicy", checksum = "6ef2f6c0")] +#[test_action("kinesis", "DeleteResourcePolicy", checksum = "991bbf6c")] #[tokio::test] async fn kinesis_resource_policy_lifecycle() { let server = TestServer::start().await; @@ -488,7 +489,7 @@ async fn kinesis_account_settings_lifecycle() { ); } -#[test_action("kinesis", "DescribeLimits", checksum = "c2be62c7")] +#[test_action("kinesis", "DescribeLimits", checksum = "55f0e704")] #[tokio::test] async fn kinesis_describe_limits() { let server = TestServer::start().await; @@ -633,7 +634,7 @@ async fn kinesis_stream_consumer_lifecycle() { .unwrap(); } -#[test_action("kinesis", "ListShards", checksum = "6033797d")] +#[test_action("kinesis", "ListShards", checksum = "a453c893")] #[tokio::test] async fn kinesis_list_shards() { let server = TestServer::start().await; @@ -689,7 +690,7 @@ async fn kinesis_merge_shards() { .unwrap(); } -#[test_action("kinesis", "SplitShard", checksum = "46cf42c5")] +#[test_action("kinesis", "SplitShard", checksum = "3888ccfb")] #[tokio::test] async fn kinesis_split_shard() { let server = TestServer::start().await; @@ -750,7 +751,7 @@ async fn kinesis_update_shard_count() { assert_eq!(response.target_shard_count(), Some(2)); } -#[test_action("kinesis", "SubscribeToShard", checksum = "b6f963e3")] +#[test_action("kinesis", "SubscribeToShard", checksum = "0c29998f")] #[tokio::test] async fn kinesis_subscribe_to_shard_requires_registered_consumer() { let server = TestServer::start().await; @@ -791,3 +792,150 @@ async fn kinesis_subscribe_to_shard_requires_registered_consumer() { .await; assert!(err.is_err()); } + +// ── Channels ── +// +// The pinned `aws-sdk-kinesis` in this workspace predates the channel +// operations, so there is no typed client for them; they are driven over raw +// awsJson1_1 HTTP with the `X-Amz-Target` header, the same way the acm-pca +// suite drives a service with no SDK at all. + +const CHANNEL_AUTH: &str = + "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/kinesis/aws4_request, SignedHeaders=host, Signature=0"; + +/// POST an awsJson1_1 Kinesis action, returning `(status, parsed_body)`. +async fn channel_op(server: &TestServer, op: &str, body: Value) -> (u16, Value) { + let resp = reqwest::Client::new() + .post(format!("{}/", server.endpoint())) + .header("content-type", "application/x-amz-json-1.1") + .header("x-amz-target", format!("Kinesis_20131202.{op}")) + .header("authorization", CHANNEL_AUTH) + .body(body.to_string()) + .send() + .await + .unwrap(); + let status = resp.status().as_u16(); + let text = resp.text().await.unwrap(); + let parsed = serde_json::from_str(&text).unwrap_or(Value::Null); + (status, parsed) +} + +#[test_action("kinesis", "CreateChannel", checksum = "367d85db")] +#[test_action("kinesis", "DescribeChannel", checksum = "c98b8d2a")] +#[test_action("kinesis", "ListChannels", checksum = "1fbca5f2")] +#[test_action("kinesis", "UpdateChannel", checksum = "d7b1f070")] +#[test_action("kinesis", "DeleteChannel", checksum = "f9b9bf41")] +#[tokio::test] +async fn kinesis_channel_lifecycle() { + let server = TestServer::start().await; + let client = server.kinesis_client().await; + + client + .create_stream() + .stream_name("channel-stream") + .shard_count(1) + .send() + .await + .unwrap(); + let source_arn = stream_arn(&client, "channel-stream").await; + + let (status, created) = channel_op( + &server, + "CreateChannel", + json!({ + "ChannelName": "conf-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::000000000000:role/conf-channel", + "StreamConfigurationList": [{ + "StreamARN": source_arn, + "RecordConfiguration": { "RecordFormatType": "JSON" }, + }], + "S3DestinationConfiguration": { + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::conf-channel-bucket", + "ExpectedBucketOwner": "000000000000", + "CompressionType": "ZSTD", + } + }, + "Tags": { "suite": "conformance" }, + }), + ) + .await; + assert_eq!(status, 200, "create channel: {created}"); + let description = &created["ChannelDescription"]; + assert_eq!(description["ChannelName"], "conf-channel"); + assert_eq!(description["ChannelStatus"], "ACTIVE"); + assert_eq!( + description["S3DestinationConfiguration"]["DataFreshnessInSeconds"], + 300 + ); + let channel_arn = description["ChannelARN"].as_str().unwrap().to_string(); + let channel_id = description["ChannelId"].as_str().unwrap(); + // AWS keys the channel ARN off the channel id, not the channel name. + assert!( + channel_arn.ends_with(&format!(":channel/{channel_id}")), + "{channel_arn}" + ); + + let (status, described) = channel_op( + &server, + "DescribeChannel", + json!({ "ChannelARN": channel_arn }), + ) + .await; + assert_eq!(status, 200, "describe channel: {described}"); + assert_eq!( + described["ChannelDescription"]["ChannelId"], + description["ChannelId"] + ); + + let (status, listed) = channel_op( + &server, + "ListChannels", + json!({ "StreamFilter": [{ "StreamARN": source_arn }], "MaxResults": 10 }), + ) + .await; + assert_eq!(status, 200, "list channels: {listed}"); + let summaries = listed["ChannelSummaries"].as_array().unwrap(); + assert_eq!(summaries.len(), 1, "{listed}"); + assert_eq!(summaries[0]["ChannelName"], "conf-channel"); + assert_eq!(summaries[0]["ChannelDestinationType"], "S3"); + + let (status, updated) = channel_op( + &server, + "UpdateChannel", + json!({ + "ChannelARN": channel_arn, + "S3DestinationConfiguration": { "DataFreshnessInSeconds": 900 }, + "LoggingConfiguration": { + "CloudWatchLogs": { "Enabled": true, "LogGroupName": "/aws/kinesis/conf" } + }, + }), + ) + .await; + assert_eq!(status, 200, "update channel: {updated}"); + assert_eq!( + updated["ChannelDescription"]["S3DestinationConfiguration"]["DataFreshnessInSeconds"], + 900 + ); + assert_eq!( + updated["ChannelDescription"]["LoggingConfiguration"]["CloudWatchLogs"]["Enabled"], + true + ); + + let (status, deleted) = channel_op( + &server, + "DeleteChannel", + json!({ "ChannelARN": channel_arn }), + ) + .await; + assert_eq!(status, 200, "delete channel: {deleted}"); + + let (status, missing) = channel_op( + &server, + "DescribeChannel", + json!({ "ChannelARN": channel_arn }), + ) + .await; + assert_eq!(status, 400, "describe deleted channel: {missing}"); + assert_eq!(missing["__type"], "ResourceNotFoundException", "{missing}"); +} diff --git a/crates/fakecloud-conformance/tests/lambda.rs b/crates/fakecloud-conformance/tests/lambda.rs index 4acd9536b..e8b2435e1 100644 --- a/crates/fakecloud-conformance/tests/lambda.rs +++ b/crates/fakecloud-conformance/tests/lambda.rs @@ -20,9 +20,9 @@ fn make_python_zip() -> Vec { // Function lifecycle // --------------------------------------------------------------------------- -#[test_action("lambda", "CreateFunction", checksum = "5f765b54")] -#[test_action("lambda", "GetFunction", checksum = "99d0a95e")] -#[test_action("lambda", "DeleteFunction", checksum = "d6c8676a")] +#[test_action("lambda", "CreateFunction", checksum = "1fca8a44")] +#[test_action("lambda", "GetFunction", checksum = "7bc6c006")] +#[test_action("lambda", "DeleteFunction", checksum = "22b50c89")] #[tokio::test] async fn lambda_create_get_delete_function() { let server = TestServer::start().await; @@ -72,7 +72,7 @@ async fn lambda_create_get_delete_function() { assert!(result.is_err()); } -#[test_action("lambda", "ListFunctions", checksum = "73439b22")] +#[test_action("lambda", "ListFunctions", checksum = "888e068d")] #[tokio::test] async fn lambda_list_functions() { let server = TestServer::start().await; @@ -103,7 +103,7 @@ async fn lambda_list_functions() { // Invoke // --------------------------------------------------------------------------- -#[test_action("lambda", "Invoke", checksum = "05f6e166")] +#[test_action("lambda", "Invoke", checksum = "f941254d")] #[tokio::test] async fn lambda_invoke() { let server = TestServer::start().await; @@ -148,7 +148,7 @@ async fn lambda_invoke() { // PublishVersion // --------------------------------------------------------------------------- -#[test_action("lambda", "PublishVersion", checksum = "63700287")] +#[test_action("lambda", "PublishVersion", checksum = "c8ab2228")] #[tokio::test] async fn lambda_publish_version() { let server = TestServer::start().await; @@ -184,7 +184,7 @@ async fn lambda_publish_version() { // Event source mappings // --------------------------------------------------------------------------- -#[test_action("lambda", "CreateEventSourceMapping", checksum = "0cac5a16")] +#[test_action("lambda", "CreateEventSourceMapping", checksum = "6c00be25")] #[test_action("lambda", "GetEventSourceMapping", checksum = "4821f650")] #[test_action("lambda", "DeleteEventSourceMapping", checksum = "8c9643a0")] #[tokio::test] @@ -234,7 +234,7 @@ async fn lambda_create_get_delete_event_source_mapping() { .unwrap(); } -#[test_action("lambda", "ListEventSourceMappings", checksum = "9e739260")] +#[test_action("lambda", "ListEventSourceMappings", checksum = "8f52c766")] #[tokio::test] async fn lambda_list_event_source_mappings() { let server = TestServer::start().await; @@ -271,9 +271,9 @@ async fn lambda_list_event_source_mappings() { // Resource-based policies // --------------------------------------------------------------------------- -#[test_action("lambda", "AddPermission", checksum = "b78cca63")] -#[test_action("lambda", "GetPolicy", checksum = "80d6c55b")] -#[test_action("lambda", "RemovePermission", checksum = "ea6d40f6")] +#[test_action("lambda", "AddPermission", checksum = "08162d94")] +#[test_action("lambda", "GetPolicy", checksum = "95bf09af")] +#[test_action("lambda", "RemovePermission", checksum = "ddbad384")] #[tokio::test] async fn lambda_resource_policy_roundtrip() { let server = TestServer::start().await; @@ -406,7 +406,7 @@ async fn lambda_alias_lifecycle() { .unwrap(); } -#[test_action("lambda", "ListVersionsByFunction", checksum = "56aa9ad0")] +#[test_action("lambda", "ListVersionsByFunction", checksum = "0cbc5f1a")] #[tokio::test] async fn lambda_list_versions_by_function() { let server = TestServer::start().await; @@ -420,9 +420,9 @@ async fn lambda_list_versions_by_function() { .unwrap(); } -#[test_action("lambda", "GetFunctionConfiguration", checksum = "00d485f6")] -#[test_action("lambda", "UpdateFunctionConfiguration", checksum = "8eb65bb3")] -#[test_action("lambda", "UpdateFunctionCode", checksum = "c6677048")] +#[test_action("lambda", "GetFunctionConfiguration", checksum = "fca9b21b")] +#[test_action("lambda", "UpdateFunctionConfiguration", checksum = "09a98ff8")] +#[test_action("lambda", "UpdateFunctionCode", checksum = "cdf5efe3")] #[tokio::test] async fn lambda_function_configuration_extras() { let server = TestServer::start().await; @@ -458,8 +458,8 @@ async fn lambda_get_account_settings() { client.get_account_settings().send().await.unwrap(); } -#[test_action("lambda", "InvokeAsync", checksum = "46b98ab0")] -#[test_action("lambda", "InvokeWithResponseStream", checksum = "78e42557")] +#[test_action("lambda", "InvokeAsync", checksum = "350f942d")] +#[test_action("lambda", "InvokeWithResponseStream", checksum = "0189ebbc")] #[tokio::test] async fn lambda_invoke_async_and_stream() { let server = TestServer::start().await; @@ -682,9 +682,9 @@ async fn lambda_concurrency_lifecycle() { #[test_action("lambda", "UpdateCodeSigningConfig", checksum = "babf3cfd")] #[test_action("lambda", "DeleteCodeSigningConfig", checksum = "2b03107b")] #[test_action("lambda", "ListCodeSigningConfigs", checksum = "d0a0f166")] -#[test_action("lambda", "PutFunctionCodeSigningConfig", checksum = "dffa379d")] -#[test_action("lambda", "GetFunctionCodeSigningConfig", checksum = "453777f6")] -#[test_action("lambda", "DeleteFunctionCodeSigningConfig", checksum = "f95c89b0")] +#[test_action("lambda", "PutFunctionCodeSigningConfig", checksum = "2d0c93ed")] +#[test_action("lambda", "GetFunctionCodeSigningConfig", checksum = "eb62995a")] +#[test_action("lambda", "DeleteFunctionCodeSigningConfig", checksum = "9e53fbf3")] #[test_action("lambda", "ListFunctionsByCodeSigningConfig", checksum = "fcee00dc")] #[tokio::test] async fn lambda_code_signing_lifecycle() { @@ -757,11 +757,11 @@ async fn lambda_code_signing_lifecycle() { .unwrap(); } -#[test_action("lambda", "PutFunctionEventInvokeConfig", checksum = "d2081393")] -#[test_action("lambda", "GetFunctionEventInvokeConfig", checksum = "958b3c63")] -#[test_action("lambda", "UpdateFunctionEventInvokeConfig", checksum = "cea9fb91")] -#[test_action("lambda", "DeleteFunctionEventInvokeConfig", checksum = "1466085b")] -#[test_action("lambda", "ListFunctionEventInvokeConfigs", checksum = "a7851e11")] +#[test_action("lambda", "PutFunctionEventInvokeConfig", checksum = "a05d2cbd")] +#[test_action("lambda", "GetFunctionEventInvokeConfig", checksum = "b8cc0e93")] +#[test_action("lambda", "UpdateFunctionEventInvokeConfig", checksum = "e3522646")] +#[test_action("lambda", "DeleteFunctionEventInvokeConfig", checksum = "bba8194f")] +#[test_action("lambda", "ListFunctionEventInvokeConfigs", checksum = "c2505c63")] #[tokio::test] async fn lambda_event_invoke_lifecycle() { let server = TestServer::start().await; @@ -802,8 +802,8 @@ async fn lambda_event_invoke_lifecycle() { .unwrap(); } -#[test_action("lambda", "PutRuntimeManagementConfig", checksum = "6f41881f")] -#[test_action("lambda", "GetRuntimeManagementConfig", checksum = "df8416ff")] +#[test_action("lambda", "PutRuntimeManagementConfig", checksum = "29a12cb3")] +#[test_action("lambda", "GetRuntimeManagementConfig", checksum = "a21c79cb")] #[tokio::test] async fn lambda_runtime_management() { let server = TestServer::start().await; @@ -976,11 +976,11 @@ async fn lambda_capacity_provider_lifecycle() { } #[test_action("lambda", "GetDurableExecution", checksum = "39fddd6c")] -#[test_action("lambda", "GetDurableExecutionHistory", checksum = "92185d93")] +#[test_action("lambda", "GetDurableExecutionHistory", checksum = "79352114")] #[test_action("lambda", "GetDurableExecutionState", checksum = "38c14d0e")] -#[test_action("lambda", "CheckpointDurableExecution", checksum = "90a79ff0")] +#[test_action("lambda", "CheckpointDurableExecution", checksum = "a9768cf1")] #[test_action("lambda", "StopDurableExecution", checksum = "c1dacd9e")] -#[test_action("lambda", "ListDurableExecutionsByFunction", checksum = "463e7a91")] +#[test_action("lambda", "ListDurableExecutionsByFunction", checksum = "d4b6f4ca")] #[test_action("lambda", "SendDurableExecutionCallbackSuccess", checksum = "16e7c48e")] #[test_action("lambda", "SendDurableExecutionCallbackFailure", checksum = "9321bea7")] #[test_action( @@ -1050,7 +1050,7 @@ async fn lambda_durable_execution_lifecycle() { .unwrap(); } -#[test_action("lambda", "UpdateEventSourceMapping", checksum = "eb040ee7")] +#[test_action("lambda", "UpdateEventSourceMapping", checksum = "b2c589c7")] #[tokio::test] async fn lambda_update_event_source_mapping() { let server = TestServer::start().await; diff --git a/crates/fakecloud-conformance/tests/ses.rs b/crates/fakecloud-conformance/tests/ses.rs index 1f460f08b..ed3b0be3b 100644 --- a/crates/fakecloud-conformance/tests/ses.rs +++ b/crates/fakecloud-conformance/tests/ses.rs @@ -80,8 +80,8 @@ async fn ses_identity_lifecycle() { // -- Configuration Set CRUD -- -#[test_action("ses", "CreateConfigurationSet", checksum = "85fe8f9d")] -#[test_action("ses", "GetConfigurationSet", checksum = "c0be65b9")] +#[test_action("ses", "CreateConfigurationSet", checksum = "e952eb8e")] +#[test_action("ses", "GetConfigurationSet", checksum = "0ac4f609")] #[test_action("ses", "ListConfigurationSets", checksum = "31486196")] #[test_action("ses", "DeleteConfigurationSet", checksum = "3c50e07a")] #[tokio::test] @@ -2154,3 +2154,209 @@ async fn ses_list_recommendations() { // Smithy ListRecommendations returns Recommendations + NextToken. resp.recommendations(); } + +// -- S/MIME Certificate Associations -- + +/// The repo's aws-sdk-sesv2 predates the identity-certificate operations +/// and UpdateConfigurationSet, so they are driven over raw HTTP against +/// the REST-JSON endpoint with the URIs from the Smithy model. +const FAKE_AUTH: &str = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260411/us-east-1/ses/aws4_request, SignedHeaders=host, Signature=fake"; + +#[test_action("ses", "AssociateEmailIdentityCertificate", checksum = "fa9865a2")] +#[test_action("ses", "ListEmailIdentityCertificates", checksum = "34d56287")] +#[test_action("ses", "DisassociateEmailIdentityCertificate", checksum = "eebd443a")] +#[tokio::test] +async fn ses_email_identity_certificate_lifecycle() { + let server = TestServer::start().await; + let client = server.sesv2_client().await; + let http = reqwest::Client::new(); + + client + .create_email_identity() + .email_identity("smime@example.com") + .send() + .await + .unwrap(); + + let certificate_arn = + "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"; + + // Associate + let resp = http + .post(format!( + "{}/v2/email/identity/certificates", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body( + serde_json::json!({ + "EmailIdentity": "smime@example.com", + "CertificateArn": certificate_arn, + }) + .to_string(), + ) + .send() + .await + .unwrap(); + assert!(resp.status().is_success()); + + // List reports the stored association against the identity. + let resp = http + .post(format!( + "{}/v2/email/identity/certificates/list", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body(serde_json::json!({"EmailIdentity": "smime@example.com"}).to_string()) + .send() + .await + .unwrap(); + assert!(resp.status().is_success()); + let body: serde_json::Value = resp.json().await.unwrap(); + let certificates = body["Certificates"].as_array().unwrap(); + assert_eq!(certificates.len(), 1); + assert_eq!( + certificates[0]["FromAddress"].as_str().unwrap(), + "smime@example.com" + ); + assert_eq!( + certificates[0]["CertificateArn"].as_str().unwrap(), + certificate_arn + ); + assert_eq!(certificates[0]["Status"].as_str().unwrap(), "ACTIVE"); + + // Disassociate + let resp = http + .post(format!( + "{}/v2/email/identity/certificates/delete", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body(serde_json::json!({"EmailIdentity": "smime@example.com"}).to_string()) + .send() + .await + .unwrap(); + assert!(resp.status().is_success()); + + let resp = http + .post(format!( + "{}/v2/email/identity/certificates/list", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body(serde_json::json!({"EmailIdentity": "smime@example.com"}).to_string()) + .send() + .await + .unwrap(); + let body: serde_json::Value = resp.json().await.unwrap(); + assert!(body["Certificates"].as_array().unwrap().is_empty()); +} + +#[tokio::test] +async fn ses_associate_email_identity_certificate_unknown_identity() { + let server = TestServer::start().await; + let http = reqwest::Client::new(); + + let resp = http + .post(format!( + "{}/v2/email/identity/certificates", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body( + serde_json::json!({ + "EmailIdentity": "ghost@example.com", + "CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/abc", + }) + .to_string(), + ) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 404); + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["__type"].as_str().unwrap(), "NotFoundException"); +} + +#[test_action("ses", "UpdateConfigurationSet", checksum = "b6b0c0d1")] +#[tokio::test] +async fn ses_update_configuration_set() { + let server = TestServer::start().await; + let client = server.sesv2_client().await; + let http = reqwest::Client::new(); + + client + .create_configuration_set() + .configuration_set_name("cs-security") + .send() + .await + .unwrap(); + + let resp = http + .post(format!( + "{}/v2/email/update-configuration-sets", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body( + serde_json::json!({ + "ConfigurationSetName": "cs-security", + "MessageSecurityOptions": { + "SigningScheme": {"SmimeScheme": {"SignatureFormat": "DETACHED"}} + }, + }) + .to_string(), + ) + .send() + .await + .unwrap(); + assert!(resp.status().is_success()); + + // GetConfigurationSet echoes the stored MessageSecurityOptions, and the + // partial update left the sending options untouched. MessageSecurityOptions + // is newer than aws-sdk-sesv2 1.x, so read the raw response. + let get = http + .get(format!( + "{}/v2/email/configuration-sets/cs-security", + server.endpoint() + )) + .header("authorization", FAKE_AUTH) + .send() + .await + .unwrap(); + let body: serde_json::Value = get.json().await.unwrap(); + assert_eq!( + body["MessageSecurityOptions"]["SigningScheme"]["SmimeScheme"]["SignatureFormat"] + .as_str() + .unwrap(), + "DETACHED" + ); + assert!(body["SendingOptions"]["SendingEnabled"].as_bool().unwrap()); +} + +#[tokio::test] +async fn ses_update_configuration_set_unknown_set() { + let server = TestServer::start().await; + let http = reqwest::Client::new(); + + let resp = http + .post(format!( + "{}/v2/email/update-configuration-sets", + server.endpoint() + )) + .header("content-type", "application/json") + .header("authorization", FAKE_AUTH) + .body(serde_json::json!({"ConfigurationSetName": "no-such-set"}).to_string()) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 404); + let body: serde_json::Value = resp.json().await.unwrap(); + assert_eq!(body["__type"].as_str().unwrap(), "NotFoundException"); +} diff --git a/crates/fakecloud-conformance/tests/support.rs b/crates/fakecloud-conformance/tests/support.rs new file mode 100644 index 000000000..676812c4b --- /dev/null +++ b/crates/fakecloud-conformance/tests/support.rs @@ -0,0 +1,264 @@ +//! Conformance coverage for the AWS Support presigned attachment-upload +//! operations (`GetAttachmentUploadLinks` / `CompleteAttachmentUpload` / +//! `DescribeAttachmentUploadStatus` / `GetAttachmentDownloadLink`). +//! +//! These operations are newer than any `aws-sdk-support` this workspace +//! depends on, so they are driven over raw awsJson1_1 HTTP: +//! `POST /` with `x-amz-target: AWSSupport_20130415.`. +//! +//! The upload and download links fakecloud hands out point back at fakecloud +//! itself, so the test follows them for real: it `PUT`s the part bytes to the +//! issued link, completes the upload with the `ETag` that `PUT` returned, and +//! `GET`s the download link back, asserting the bytes survive the round trip. + +mod helpers; + +use fakecloud_conformance_macros::test_action; +use helpers::TestServer; +use serde_json::{json, Value}; + +const AUTH: &str = "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/support/aws4_request, SignedHeaders=host, Signature=0"; + +/// POST an awsJson1_1 AWS Support action, returning `(status, parsed_body)`. +async fn support(server: &TestServer, op: &str, body: Value) -> (u16, Value) { + let resp = reqwest::Client::new() + .post(format!("{}/", server.endpoint())) + .header("content-type", "application/x-amz-json-1.1") + .header("x-amz-target", format!("AWSSupport_20130415.{op}")) + .header("Authorization", AUTH) + .body(body.to_string()) + .send() + .await + .unwrap(); + let status = resp.status().as_u16(); + let text = resp.text().await.unwrap(); + let parsed = serde_json::from_str(&text).unwrap_or(Value::Null); + (status, parsed) +} + +#[test_action("support", "GetAttachmentUploadLinks", checksum = "81100c3d")] +#[test_action("support", "CompleteAttachmentUpload", checksum = "b1ea9dc1")] +#[test_action("support", "DescribeAttachmentUploadStatus", checksum = "18064527")] +#[test_action("support", "GetAttachmentDownloadLink", checksum = "3768e4dd")] +#[tokio::test] +async fn support_attachment_upload_round_trip() { + let server = TestServer::start().await; + let client = reqwest::Client::new(); + let contents = b"fakecloud support attachment".to_vec(); + + // 1. Ask for upload links for a small single-part file. + let (status, links) = support( + &server, + "GetAttachmentUploadLinks", + json!({ "fileName": "diagnostics.log", "fileSizeBytes": contents.len() }), + ) + .await; + assert_eq!(status, 200, "{links}"); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + assert!(!upload_id.is_empty()); + assert_eq!(links["totalParts"], 1, "{links}"); + // Nothing has been uploaded yet, so part 1 is still outstanding. + assert_eq!(links["nextIndex"], 1, "{links}"); + assert!(links["partSizeBytes"].as_i64().unwrap() > 0, "{links}"); + let part = &links["uploadUrls"][0]; + assert_eq!(part["partIndex"], 1, "{links}"); + assert!(part["expiryDate"].is_string(), "{links}"); + let upload_url = part["url"].as_str().unwrap().to_string(); + assert!(upload_url.contains("X-Amz-Signature="), "{upload_url}"); + assert!( + upload_url.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256"), + "{upload_url}" + ); + + // 2. The link is real: PUT the bytes and keep the ETag it returns. + let resp = client + .put(&upload_url) + .body(contents.clone()) + .send() + .await + .unwrap(); + assert!( + resp.status().is_success(), + "part upload failed: {}", + resp.status() + ); + let etag = resp + .headers() + .get("etag") + .expect("the part upload returns an ETag") + .to_str() + .unwrap() + .to_string(); + + // 3. The recorded progress is visible before the upload is completed. + let (status, progress) = support( + &server, + "DescribeAttachmentUploadStatus", + json!({ "uploadId": upload_id }), + ) + .await; + assert_eq!(status, 200, "{progress}"); + assert_eq!( + progress["uploadStatus"], "attachment-not-ready", + "{progress}" + ); + assert_eq!(progress["fileName"], "diagnostics.log", "{progress}"); + assert_eq!(progress["uploadProgress"]["totalParts"], 1, "{progress}"); + assert_eq!( + progress["uploadProgress"]["completedPartsCount"], 1, + "{progress}" + ); + + // 4. Complete the upload with the ETag the data plane issued. + let (status, completed) = support( + &server, + "CompleteAttachmentUpload", + json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": etag }], + }), + ) + .await; + assert_eq!(status, 200, "{completed}"); + assert_eq!(completed["uploadStatus"], "attachment-ready", "{completed}"); + + let (_, progress) = support( + &server, + "DescribeAttachmentUploadStatus", + json!({ "uploadId": upload_id }), + ) + .await; + assert_eq!(progress["uploadStatus"], "attachment-ready", "{progress}"); + + // 5. Attach the completed upload to a case to learn its attachment id. + let (status, created) = support( + &server, + "CreateCase", + json!({ + "subject": "conformance attachment upload", + "communicationBody": "diagnostics attached", + "uploadIds": [upload_id], + }), + ) + .await; + assert_eq!(status, 200, "{created}"); + let case_id = created["caseId"].as_str().unwrap().to_string(); + + let (status, comms) = support( + &server, + "DescribeCommunications", + json!({ "caseId": case_id }), + ) + .await; + assert_eq!(status, 200, "{comms}"); + let attachment = &comms["communications"][0]["attachments"][0]; + assert_eq!(attachment["fileName"], "diagnostics.log", "{comms}"); + let attachment_id = attachment["attachmentId"].as_str().unwrap().to_string(); + + // 6. A download link for it, followed for real. + let (status, link) = support( + &server, + "GetAttachmentDownloadLink", + json!({ "attachmentId": attachment_id }), + ) + .await; + assert_eq!(status, 200, "{link}"); + assert_eq!(link["fileName"], "diagnostics.log", "{link}"); + assert!(link["downloadUrl"]["expiryDate"].is_string(), "{link}"); + let download_url = link["downloadUrl"]["url"].as_str().unwrap().to_string(); + assert!(download_url.contains("X-Amz-Signature="), "{download_url}"); + + let resp = client.get(&download_url).send().await.unwrap(); + assert!( + resp.status().is_success(), + "attachment download failed: {}", + resp.status() + ); + assert_eq!(resp.bytes().await.unwrap().to_vec(), contents); + + // A completed upload cannot be completed again. + let (status, err) = support( + &server, + "CompleteAttachmentUpload", + json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": "\"whatever\"" }], + }), + ) + .await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "UploadIdNotFound", "{err}"); +} + +#[tokio::test] +async fn support_attachment_upload_errors() { + let server = TestServer::start().await; + + // Unknown upload ids are UploadIdNotFound on every upload operation. + for (op, body) in [ + ( + "GetAttachmentUploadLinks", + json!({ "fileName": "x.log", "uploadId": "upload-missing" }), + ), + ( + "CompleteAttachmentUpload", + json!({ "uploadId": "upload-missing", "completedUploads": [] }), + ), + ( + "DescribeAttachmentUploadStatus", + json!({ "uploadId": "upload-missing" }), + ), + ] { + let (status, err) = support(&server, op, body).await; + assert_eq!(status, 400, "{op}: {err}"); + assert_eq!(err["__type"], "UploadIdNotFound", "{op}: {err}"); + } + + // An unknown attachment has no download link. + let (status, err) = support( + &server, + "GetAttachmentDownloadLink", + json!({ "attachmentId": "attachment-missing" }), + ) + .await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "AttachmentIdNotFound", "{err}"); + + // An upload that was never completed cannot be attached to a case. + let (status, links) = support( + &server, + "GetAttachmentUploadLinks", + json!({ "fileName": "pending.log", "fileSizeBytes": 4 }), + ) + .await; + assert_eq!(status, 200, "{links}"); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let (status, err) = support( + &server, + "CreateCase", + json!({ + "subject": "premature", + "communicationBody": "not uploaded yet", + "uploadIds": [upload_id], + }), + ) + .await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "ValidationException", "{err}"); + + // Required members are still enforced ahead of any of this. + let (status, err) = support(&server, "GetAttachmentUploadLinks", json!({})).await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "ValidationException", "{err}"); + + // A dry run is refused with the modelled exception instead of taking + // effect. + let (status, err) = support( + &server, + "GetAttachmentUploadLinks", + json!({ "fileName": "dry.log", "fileSizeBytes": 4, "dryRun": true }), + ) + .await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "DryRunOperationException", "{err}"); +} diff --git a/crates/fakecloud-conformance/tests/transfer.rs b/crates/fakecloud-conformance/tests/transfer.rs index 91e741232..f27bace39 100644 --- a/crates/fakecloud-conformance/tests/transfer.rs +++ b/crates/fakecloud-conformance/tests/transfer.rs @@ -12,7 +12,7 @@ use helpers::TestServer; #[test_action("transfer", "CreateAccess", checksum = "482198aa")] #[test_action("transfer", "CreateAgreement", checksum = "5ff5b981")] -#[test_action("transfer", "CreateConnector", checksum = "578da32e")] +#[test_action("transfer", "CreateConnector", checksum = "f8fb4315")] #[test_action("transfer", "CreateProfile", checksum = "0e1e3546")] #[test_action("transfer", "CreateServer", checksum = "ea5dbf8a")] #[test_action("transfer", "CreateUser", checksum = "d6286e25")] @@ -33,7 +33,7 @@ use helpers::TestServer; #[test_action("transfer", "DescribeAccess", checksum = "610847ce")] #[test_action("transfer", "DescribeAgreement", checksum = "21085849")] #[test_action("transfer", "DescribeCertificate", checksum = "2754344d")] -#[test_action("transfer", "DescribeConnector", checksum = "bbeaac81")] +#[test_action("transfer", "DescribeConnector", checksum = "2b361e21")] #[test_action("transfer", "DescribeExecution", checksum = "348aaf31")] #[test_action("transfer", "DescribeHostKey", checksum = "8c8a69e5")] #[test_action("transfer", "DescribeProfile", checksum = "d9d8da1a")] @@ -74,7 +74,7 @@ use helpers::TestServer; #[test_action("transfer", "UpdateAccess", checksum = "018643a1")] #[test_action("transfer", "UpdateAgreement", checksum = "bde6c56f")] #[test_action("transfer", "UpdateCertificate", checksum = "07521e17")] -#[test_action("transfer", "UpdateConnector", checksum = "a7ff2a02")] +#[test_action("transfer", "UpdateConnector", checksum = "05318d94")] #[test_action("transfer", "UpdateHostKey", checksum = "d6bce871")] #[test_action("transfer", "UpdateProfile", checksum = "b2283ba5")] #[test_action("transfer", "UpdateServer", checksum = "56339d3e")] diff --git a/crates/fakecloud-ec2/src/service/mod.rs b/crates/fakecloud-ec2/src/service/mod.rs index 6c26f4f54..4a2b45e56 100644 --- a/crates/fakecloud-ec2/src/service/mod.rs +++ b/crates/fakecloud-ec2/src/service/mod.rs @@ -57,7 +57,8 @@ fn is_mutating_action(action: &str) -> bool { !(action.starts_with("Describe") || action.starts_with("Get") || action.starts_with("Search") - || action.starts_with("List")) + || action.starts_with("List") + || action.starts_with("Validate")) } /// Every EC2 action this build implements. The conformance audit cross-checks @@ -120,6 +121,7 @@ pub const SUPPORTED_ACTIONS: &[&str] = &[ "GetSecurityGroupsForVpc", "DescribeStaleSecurityGroups", "DescribeSecurityGroupReferences", + "ValidateSecurityGroupQuotasForInterface", // Route tables "CreateRouteTable", "DeleteRouteTable", @@ -1398,6 +1400,9 @@ impl AwsService for Ec2Service { "DescribeSecurityGroupReferences" => { sg::describe_security_group_references(self, &request) } + "ValidateSecurityGroupQuotasForInterface" => { + sg::validate_security_group_quotas_for_interface(self, &request) + } "CreateRouteTable" => routing::create_route_table(self, &request), "DeleteRouteTable" => routing::delete_route_table(self, &request), "DescribeRouteTables" => routing::describe_route_tables(self, &request), diff --git a/crates/fakecloud-ec2/src/service/sg.rs b/crates/fakecloud-ec2/src/service/sg.rs index cba3ab466..2e0b02bcb 100644 --- a/crates/fakecloud-ec2/src/service/sg.rs +++ b/crates/fakecloud-ec2/src/service/sg.rs @@ -3,13 +3,13 @@ use std::collections::HashMap; -use fakecloud_aws::ec2query::{ec2_elem, ec2_list, ec2_return}; +use fakecloud_aws::ec2query::{ec2_bool, ec2_elem, ec2_list, ec2_return}; use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError}; use crate::service::Ec2Service; use crate::service_helpers::{ - filter_value_matches, gen_id, indexed_list, parse_filters, require, validate_max_results, - Filter, + filter_value_matches, gen_id, indexed_list, invalid_parameter_value, missing_parameter, + parse_filters, require, validate_max_results, Filter, }; use crate::state::{Ec2State, SecurityGroup, SecurityGroupRule, SecurityGroupVpcAssociation, Tag}; @@ -1057,6 +1057,126 @@ pub(crate) fn describe_security_group_references( )) } +// ---- quota validation ---- + +/// Security groups that may be associated with one network interface. The +/// published Amazon VPC default; AWS allows it to be raised to 16. +const SECURITY_GROUPS_PER_INTERFACE: usize = 5; + +/// Inbound (or outbound) rules per security group. Each direction gets its own +/// allowance, so a group at the limit in both directions is still valid. +/// +/// AWS additionally caps the *product* of the two quotas above at 1000. That +/// binds only once a quota increase is granted; at the published defaults the +/// product is 5 x 60 = 300, so it can never be the reason a request is +/// rejected here and is not modeled as a third check. +const RULES_PER_SECURITY_GROUP: usize = 60; + +/// `SecurityGroupsPerInterfaceLimitExceeded` -- more groups than one network +/// interface may carry. +fn groups_per_interface_exceeded(requested: usize) -> AwsServiceError { + AwsServiceError::aws_error( + http::StatusCode::BAD_REQUEST, + "SecurityGroupsPerInterfaceLimitExceeded", + format!( + "You have exceeded the number of security groups that can be associated with a \ + network interface: requested {requested}, limit {SECURITY_GROUPS_PER_INTERFACE}" + ), + ) +} + +/// `RulesPerSecurityGroupLimitExceeded` -- the rules the requested groups carry +/// exceed a per-group or per-interface rule allowance. +fn rules_limit_exceeded(message: String) -> AwsServiceError { + AwsServiceError::aws_error( + http::StatusCode::BAD_REQUEST, + "RulesPerSecurityGroupLimitExceeded", + message, + ) +} + +/// `ValidateSecurityGroupQuotasForInterface`: answer whether the requested set +/// of security groups could be attached to a single network interface without +/// breaching a VPC quota. +/// +/// The answer is derived from the stored groups, not assumed: every id is +/// resolved (an unknown one is `InvalidGroup.NotFound`, exactly as the rest of +/// the security-group surface reports it), and the rule counts that feed the +/// quota arithmetic are the groups' real ingress/egress rules. AWS returns +/// `valid=true` or an error -- there is no "false" answer -- so each quota it +/// documents gets its own error code and message here. +pub(crate) fn validate_security_group_quotas_for_interface( + svc: &Ec2Service, + req: &AwsRequest, +) -> Result { + let group_ids = indexed_list(&req.query_params, "SecurityGroupId"); + // Unlike the describe ops, an empty list here is not "match everything": + // there is nothing to validate, and AWS requires at least one id. + if group_ids.is_empty() { + return Err(missing_parameter("SecurityGroupId")); + } + // "each ID must be unique" -- a repeat is rejected rather than silently + // deduplicated, which would validate a smaller set than the caller sent. + for (i, id) in group_ids.iter().enumerate() { + if group_ids[..i].contains(id) { + return Err(invalid_parameter_value(format!( + "The security group ID '{id}' may only be specified once" + ))); + } + } + + // Resolve every id against stored state and take its real rule counts. A + // missing group is an error, so no quota arithmetic ever runs over a group + // that was assumed to be empty. + let mut counts: Vec<(String, usize, usize)> = Vec::with_capacity(group_ids.len()); + { + let accounts = svc.state.read(); + let empty = Ec2State::new(&req.account_id, &req.region); + let state = accounts.get(&req.account_id).unwrap_or(&empty); + for id in &group_ids { + let sg = state + .security_groups + .get(id) + .ok_or_else(|| sg_not_found(id))?; + let egress = sg.rules.iter().filter(|r| r.is_egress).count(); + counts.push((id.clone(), sg.rules.len() - egress, egress)); + } + } + + if group_ids.len() > SECURITY_GROUPS_PER_INTERFACE { + return Err(groups_per_interface_exceeded(group_ids.len())); + } + for (id, ingress, egress) in &counts { + // Each direction has its own allowance, so the busier one decides. + let worst = (*ingress).max(*egress); + if worst > RULES_PER_SECURITY_GROUP { + return Err(rules_limit_exceeded(format!( + "The security group '{id}' has {worst} rules in one direction, exceeding the \ + limit of {RULES_PER_SECURITY_GROUP} rules per security group" + ))); + } + } + // A DryRun runs the same validation -- including the quota arithmetic, so a + // set that would be rejected is still rejected -- and reports nothing back, + // matching how the rest of EC2 treats one. + if req + .query_params + .get("DryRun") + .is_some_and(|v| v.eq_ignore_ascii_case("true")) + { + return Ok(Ec2Service::respond( + "ValidateSecurityGroupQuotasForInterface", + &req.request_id, + "", + )); + } + Ok(Ec2Service::respond( + "ValidateSecurityGroupQuotasForInterface", + &req.request_id, + &ec2_bool("valid", true), + )) +} + #[cfg(test)] mod modify_tests { use super::*; @@ -1746,4 +1866,164 @@ mod modify_tests { ) .contains("sg-1")); } + + // ---- ValidateSecurityGroupQuotasForInterface ---- + + /// Store a group carrying `ingress` inbound and `egress` outbound rules, so + /// the quota arithmetic runs over real stored rules. + fn seed_sized_group(svc: &Ec2Service, group_id: &str, ingress: usize, egress: usize) { + let mut rules = Vec::new(); + for i in 0..ingress + egress { + let mut r = ingress_rule(&format!("sgr-{group_id}-{i}"), 22, "10.0.0.0/8"); + r.group_id = group_id.to_string(); + r.is_egress = i >= ingress; + rules.push(r); + } + let mut accounts = svc.state.write(); + let state = accounts.get_or_create("000000000000"); + state.security_groups.insert( + group_id.to_string(), + SecurityGroup { + group_id: group_id.into(), + group_name: group_id.into(), + description: "d".into(), + vpc_id: "vpc-1".into(), + rules, + }, + ); + } + + fn validate_quotas(svc: &Ec2Service, query: &[(&str, &str)]) -> String { + let resp = validate_security_group_quotas_for_interface( + svc, + &req("ValidateSecurityGroupQuotasForInterface", query), + ) + .unwrap(); + String::from_utf8(resp.body.expect_bytes().to_vec()).unwrap() + } + + #[test] + fn validate_quotas_accepts_groups_within_the_quotas() { + let svc = Ec2Service::new(); + seed_sized_group( + &svc, + "sg-a", + RULES_PER_SECURITY_GROUP, + RULES_PER_SECURITY_GROUP, + ); + seed_sized_group(&svc, "sg-b", 1, 1); + let body = validate_quotas( + &svc, + &[("SecurityGroupId.1", "sg-a"), ("SecurityGroupId.2", "sg-b")], + ); + assert!(body.contains("true"), "{body}"); + assert!( + body.contains("rid"), "{body}"); + } + + #[test] + fn validate_quotas_unknown_group_errors() { + let svc = Ec2Service::new(); + seed_sized_group(&svc, "sg-a", 1, 1); + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req( + "ValidateSecurityGroupQuotasForInterface", + &[ + ("SecurityGroupId.1", "sg-a"), + ("SecurityGroupId.2", "sg-gone"), + ], + ), + )); + assert_eq!(err.code(), "InvalidGroup.NotFound"); + } + + #[test] + fn validate_quotas_requires_at_least_one_group() { + let svc = Ec2Service::new(); + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req("ValidateSecurityGroupQuotasForInterface", &[]), + )); + assert_eq!(err.code(), "MissingParameter"); + } + + #[test] + fn validate_quotas_rejects_a_repeated_group() { + let svc = Ec2Service::new(); + seed_sized_group(&svc, "sg-a", 1, 1); + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", "sg-a"), ("SecurityGroupId.2", "sg-a")], + ), + )); + assert_eq!(err.code(), "InvalidParameterValue"); + } + + #[test] + fn validate_quotas_rejects_too_many_groups() { + let svc = Ec2Service::new(); + let ids: Vec = (0..SECURITY_GROUPS_PER_INTERFACE + 1) + .map(|i| format!("sg-{i}")) + .collect(); + for id in &ids { + seed_sized_group(&svc, id, 1, 1); + } + let query: Vec<(String, String)> = ids + .iter() + .enumerate() + .map(|(i, id)| (format!("SecurityGroupId.{}", i + 1), id.clone())) + .collect(); + let pairs: Vec<(&str, &str)> = query + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req("ValidateSecurityGroupQuotasForInterface", &pairs), + )); + assert_eq!(err.code(), "SecurityGroupsPerInterfaceLimitExceeded"); + } + + #[test] + fn validate_quotas_rejects_a_group_over_the_rule_limit() { + let svc = Ec2Service::new(); + // One rule past the per-direction allowance, with the other direction + // empty: the directions are counted separately, not summed. + seed_sized_group(&svc, "sg-a", RULES_PER_SECURITY_GROUP + 1, 0); + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", "sg-a")], + ), + )); + assert_eq!(err.code(), "RulesPerSecurityGroupLimitExceeded"); + } + + #[test] + fn validate_quotas_dry_run_validates_without_answering() { + let svc = Ec2Service::new(); + seed_sized_group(&svc, "sg-a", 1, 1); + let body = validate_quotas(&svc, &[("SecurityGroupId.1", "sg-a"), ("DryRun", "true")]); + assert!( + !body.contains(""), + "a dry run answers nothing: {body}" + ); + + // A dry run still reports an unknown group rather than succeeding. + let err = crate::test_support::err_of(validate_security_group_quotas_for_interface( + &svc, + &req( + "ValidateSecurityGroupQuotasForInterface", + &[("SecurityGroupId.1", "sg-gone"), ("DryRun", "true")], + ), + )); + assert_eq!(err.code(), "InvalidGroup.NotFound"); + } } diff --git a/crates/fakecloud-kinesis/Cargo.toml b/crates/fakecloud-kinesis/Cargo.toml index 6582524c5..916fc7f6f 100644 --- a/crates/fakecloud-kinesis/Cargo.toml +++ b/crates/fakecloud-kinesis/Cargo.toml @@ -21,6 +21,7 @@ serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +uuid = { workspace = true } [dev-dependencies] bytes = { workspace = true } diff --git a/crates/fakecloud-kinesis/src/service.rs b/crates/fakecloud-kinesis/src/service.rs index 87232fe1e..9b01e012a 100644 --- a/crates/fakecloud-kinesis/src/service.rs +++ b/crates/fakecloud-kinesis/src/service.rs @@ -15,18 +15,24 @@ use fakecloud_core::validation::{ use fakecloud_persistence::SnapshotStore; use crate::state::{ - KinesisConsumer, KinesisRecord, KinesisShard, KinesisSnapshot, KinesisState, KinesisStream, - SharedKinesisState, KINESIS_SNAPSHOT_SCHEMA_VERSION, + KinesisChannel, KinesisChannelDeadLetterQueue, KinesisChannelDestination, + KinesisChannelEncryption, KinesisChannelLogging, KinesisChannelPartitionField, + KinesisChannelS3Storage, KinesisChannelS3Table, KinesisChannelStream, KinesisConsumer, + KinesisRecord, KinesisShard, KinesisSnapshot, KinesisState, KinesisStream, SharedKinesisState, + KINESIS_SNAPSHOT_SCHEMA_VERSION, }; const SUPPORTED_ACTIONS: &[&str] = &[ "AddTagsToStream", + "CreateChannel", "CreateStream", "DecreaseStreamRetentionPeriod", + "DeleteChannel", "DeleteResourcePolicy", "DeleteStream", "DeregisterStreamConsumer", "DescribeAccountSettings", + "DescribeChannel", "DescribeLimits", "DescribeStream", "DescribeStreamConsumer", @@ -37,6 +43,7 @@ const SUPPORTED_ACTIONS: &[&str] = &[ "GetResourcePolicy", "GetShardIterator", "IncreaseStreamRetentionPeriod", + "ListChannels", "ListShards", "ListStreamConsumers", "ListStreams", @@ -55,6 +62,7 @@ const SUPPORTED_ACTIONS: &[&str] = &[ "TagResource", "UntagResource", "UpdateAccountSettings", + "UpdateChannel", "UpdateMaxRecordSize", "UpdateShardCount", "UpdateStreamMode", @@ -192,6 +200,11 @@ impl AwsService for KinesisService { "SplitShard" => self.split_shard(&request), "UpdateShardCount" => self.update_shard_count(&request), "SubscribeToShard" => self.subscribe_to_shard(&request), + "CreateChannel" => self.create_channel(&request), + "DescribeChannel" => self.describe_channel(&request), + "ListChannels" => self.list_channels(&request), + "UpdateChannel" => self.update_channel(&request), + "DeleteChannel" => self.delete_channel(&request), _ => Err(AwsServiceError::action_not_implemented( self.service_name(), &request.action, @@ -489,11 +502,25 @@ impl KinesisService { let mut accounts = self.state.write(); let state = accounts.get_or_create(&request.account_id); let stream_name = resolve_stream_name(state, &body)?; + // A stream cannot be deleted while a channel still draws from it; AWS + // requires the attached channels to be deleted first. + let stream_arn = state.stream_arn(request.region.as_str(), &stream_name); + let attached_channels = state.channels_for_stream(&stream_arn); + if !attached_channels.is_empty() { + return Err(AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "ResourceInUseException", + format!( + "Stream {stream_name} has channels attached to it: {}. \ + Delete them before deleting the stream.", + attached_channels.join(", ") + ), + )); + } let stream = state.streams.remove(&stream_name); if stream.is_none() { return Err(stream_not_found(&state.account_id, &stream_name)); } - let stream_arn = state.stream_arn(request.region.as_str(), &stream_name); state.consumers.retain(|_, c| c.stream_arn != stream_arn); Ok(AwsResponse::ok_json(json!({}))) @@ -2100,6 +2127,220 @@ impl KinesisService { } } +// --- Channels --- + +impl KinesisService { + fn create_channel(&self, request: &AwsRequest) -> Result { + let body = request.json_body(); + let channel_name = require_channel_name(&body)?; + let service_execution_role_arn = + require_channel_member(&body, "ServiceExecutionRoleARN", 2048)?; + let channel_id = uuid::Uuid::new_v4().to_string(); + let destination = parse_channel_destination(&body, channel_name, &channel_id)?; + let encryption = parse_channel_encryption(&body["EncryptionConfiguration"])?; + let logging = + parse_channel_logging(&body["LoggingConfiguration"], channel_name, &channel_id)?; + let tags: std::collections::BTreeMap = body["Tags"] + .as_object() + .map(|map| { + map.iter() + .filter_map(|(key, value)| { + value.as_str().map(|value| (key.clone(), value.to_string())) + }) + .collect() + }) + .unwrap_or_default(); + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&request.account_id); + if state.channels.contains_key(channel_name) { + return Err(AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "ResourceInUseException", + format!( + "Channel {channel_name} under account {} already exists.", + state.account_id + ), + )); + } + let streams = parse_channel_streams(state, &body)?; + + let channel = KinesisChannel { + channel_name: channel_name.to_string(), + channel_arn: state.channel_arn(request.region.as_str(), &channel_id), + channel_id, + // Channel creation is asynchronous on AWS (CREATING then ACTIVE). + // Fakecloud provisions synchronously, so the channel is ACTIVE the + // moment CreateChannel returns. + channel_status: "ACTIVE".to_string(), + channel_creation_timestamp: Utc::now(), + service_execution_role_arn: service_execution_role_arn.to_string(), + streams, + destination, + encryption, + logging, + tags, + }; + let description = channel_description_json(&channel); + state.channels.insert(channel_name.to_string(), channel); + + Ok(AwsResponse::ok_json(json!({ + "ChannelDescription": description, + }))) + } + + fn describe_channel(&self, request: &AwsRequest) -> Result { + let body = request.json_body(); + let channel_arn = require_channel_arn(&body)?; + + let accounts = self.state.read(); + let empty = KinesisState::new(&request.account_id, &request.region); + let state = accounts.get(&request.account_id).unwrap_or(&empty); + let channel_name = state + .channel_name_from_arn(channel_arn) + .ok_or_else(|| resource_not_found_arn(channel_arn))?; + let channel = state + .channels + .get(&channel_name) + .ok_or_else(|| resource_not_found_arn(channel_arn))?; + + Ok(AwsResponse::ok_json(json!({ + "ChannelDescription": channel_description_json(channel), + }))) + } + + fn list_channels(&self, request: &AwsRequest) -> Result { + let body = request.json_body(); + validate_optional_string_length("NextToken", body["NextToken"].as_str(), 1, 1048576)?; + validate_optional_json_range("MaxResults", &body["MaxResults"], 1, 10000)?; + // AWS defaults to 100 and returns at most 100 channels per page even + // when a larger MaxResults is accepted on the wire. + let max_results = body["MaxResults"] + .as_i64() + .unwrap_or(MAX_LIST_CHANNELS_PAGE as i64) + .min(MAX_LIST_CHANNELS_PAGE as i64) as usize; + let filters = parse_channel_stream_filters(&body["StreamFilter"])?; + let resume_after = match body["NextToken"].as_str() { + Some(token) => Some(decode_list_channels_token(token)?), + None => None, + }; + + let accounts = self.state.read(); + let empty = KinesisState::new(&request.account_id, &request.region); + let state = accounts.get(&request.account_id).unwrap_or(&empty); + + // `channels` is keyed by name, so BTreeMap iteration is already the + // name order the cursor resumes against. + let mut matched: Vec<&KinesisChannel> = state + .channels + .values() + .filter(|channel| { + filters.is_empty() || channel_matches_stream_filters(channel, &filters) + }) + .filter(|channel| { + resume_after + .as_deref() + .is_none_or(|cursor| channel.channel_name.as_str() > cursor) + }) + .collect(); + + let has_more = matched.len() > max_results; + matched.truncate(max_results); + let next_token = if has_more { + matched + .last() + .map(|channel| encode_list_channels_token(&channel.channel_name)) + } else { + None + }; + + let mut response = json!({ + "ChannelSummaries": matched + .into_iter() + .map(channel_summary_json) + .collect::>(), + }); + if let Some(token) = next_token { + response["NextToken"] = json!(token); + } + Ok(AwsResponse::ok_json(response)) + } + + fn update_channel(&self, request: &AwsRequest) -> Result { + let body = request.json_body(); + let channel_arn = require_channel_arn(&body)?; + let s3_update = &body["S3DestinationConfiguration"]; + let s3_tables_update = &body["S3TablesDestinationConfiguration"]; + if !s3_update.is_null() && !s3_tables_update.is_null() { + return Err(invalid_argument( + "Specify either S3DestinationConfiguration or \ + S3TablesDestinationConfiguration, but not both", + )); + } + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&request.account_id); + let channel_name = state + .channel_name_from_arn(channel_arn) + .ok_or_else(|| resource_not_found_arn(channel_arn))?; + let channel = state + .channels + .get_mut(&channel_name) + .ok_or_else(|| resource_not_found_arn(channel_arn))?; + + // Only DataFreshnessInSeconds is updatable, and only on the + // destination the channel was created with. + let destination_type = channel.destination.destination_type(); + if !s3_update.is_null() { + if destination_type != "S3" { + return Err(invalid_argument( + "S3DestinationConfiguration cannot update a channel whose \ + destination is S3_TABLES", + )); + } + *channel.destination.data_freshness_mut() = require_channel_data_freshness(s3_update)?; + } + if !s3_tables_update.is_null() { + if destination_type != "S3_TABLES" { + return Err(invalid_argument( + "S3TablesDestinationConfiguration cannot update a channel whose \ + destination is S3", + )); + } + *channel.destination.data_freshness_mut() = + require_channel_data_freshness(s3_tables_update)?; + } + if !body["LoggingConfiguration"].is_null() { + let logging = parse_channel_logging( + &body["LoggingConfiguration"], + &channel.channel_name, + &channel.channel_id, + )?; + channel.logging = logging; + } + + // AWS moves the channel through UPDATING back to ACTIVE; the update is + // applied synchronously here, so the channel stays ACTIVE. + Ok(AwsResponse::ok_json(json!({ + "ChannelDescription": channel_description_json(channel), + }))) + } + + fn delete_channel(&self, request: &AwsRequest) -> Result { + let body = request.json_body(); + let channel_arn = require_channel_arn(&body)?; + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&request.account_id); + let channel_name = state + .channel_name_from_arn(channel_arn) + .ok_or_else(|| resource_not_found_arn(channel_arn))?; + state.channels.remove(&channel_name); + + Ok(AwsResponse::ok_json(json!({}))) + } +} + // --- State helper --- impl crate::state::KinesisState { diff --git a/crates/fakecloud-kinesis/src/service_helpers.rs b/crates/fakecloud-kinesis/src/service_helpers.rs index be4dc19bb..e7831555b 100644 --- a/crates/fakecloud-kinesis/src/service_helpers.rs +++ b/crates/fakecloud-kinesis/src/service_helpers.rs @@ -48,8 +48,11 @@ pub(crate) fn close_shard(stream: &mut KinesisStream, shard_id: &str) { pub(crate) fn is_mutating_action(action: &str) -> bool { matches!( action, - "CreateStream" + "CreateChannel" + | "CreateStream" + | "DeleteChannel" | "DeleteStream" + | "UpdateChannel" | "PutRecord" | "PutRecords" | "AddTagsToStream" @@ -592,6 +595,638 @@ pub(crate) fn shard_discriminator(shard_id: &str) -> u32 { digits.parse::().unwrap_or(0).min(99_999) as u32 } +// --- Channels --- + +/// `DataFreshnessInSeconds` when the caller omits it, and the range AWS +/// accepts for it (5 to 15 minutes). +pub(crate) const DEFAULT_CHANNEL_DATA_FRESHNESS_SECONDS: i64 = 300; +pub(crate) const MIN_CHANNEL_DATA_FRESHNESS_SECONDS: i64 = 300; +pub(crate) const MAX_CHANNEL_DATA_FRESHNESS_SECONDS: i64 = 900; + +/// `S3StorageConfiguration.OutputKeyTemplate` when the caller omits it. +pub(crate) const DEFAULT_CHANNEL_OUTPUT_KEY_TEMPLATE: &str = + "kinesis-channel/!{channel-name}/!{channel-id}/!{yyyy}/!{MM}/!{dd}/!{HH}/\ + !{channel-name}-!{channel-id}-!{yyyy}-!{MM}-!{dd}-!{HH}-!{mm}!{extension}"; + +/// `CloudWatchLogs.LogStreamName` when the caller omits it. +pub(crate) const DEFAULT_CHANNEL_LOG_STREAM_NAME: &str = "DestinationDelivery"; + +/// `ListChannels` returns at most 100 channels per page; a larger +/// `MaxResults` is clamped rather than rejected. +pub(crate) const MAX_LIST_CHANNELS_PAGE: usize = 100; + +pub(crate) fn require_channel_name(body: &Value) -> Result<&str, AwsServiceError> { + let name = body["ChannelName"] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| invalid_argument("ChannelName is required"))?; + validate_string_length("ChannelName", name, 1, 128)?; + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-')) + { + return Err(validation_exception( + "Value at 'channelName' failed to satisfy constraint: \ + Member must satisfy regular expression pattern: ^[a-zA-Z0-9_.-]+$", + )); + } + Ok(name) +} + +pub(crate) fn require_channel_arn(body: &Value) -> Result<&str, AwsServiceError> { + let arn = body["ChannelARN"] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| invalid_argument("ChannelARN is required"))?; + validate_string_length("ChannelARN", arn, 1, 2048)?; + Ok(arn) +} + +/// A required string member of a channel sub-structure. +pub(crate) fn require_channel_member<'a>( + value: &'a Value, + field: &str, + max_len: usize, +) -> Result<&'a str, AwsServiceError> { + let found = value[field] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| invalid_argument(format!("{field} is required")))?; + validate_string_length(field, found, 1, max_len)?; + Ok(found) +} + +/// An optional enum member, defaulted when absent and rejected when it is not +/// one of the values the Smithy model lists. +fn channel_enum_member( + value: &Value, + field: &str, + allowed: &[&str], + default: Option<&str>, +) -> Result { + let found = match value[field].as_str().filter(|value| !value.is_empty()) { + Some(found) => found, + None => { + return default + .map(str::to_string) + .ok_or_else(|| invalid_argument(format!("{field} is required"))) + } + }; + if !allowed.contains(&found) { + return Err(invalid_argument(format!( + "{field} must be one of {}", + allowed.join(", ") + ))); + } + Ok(found.to_string()) +} + +/// `DataFreshnessInSeconds` of an `UpdateChannel` destination update, where +/// the member is required rather than defaulted. +pub(crate) fn require_channel_data_freshness(value: &Value) -> Result { + if value["DataFreshnessInSeconds"].is_null() { + return Err(invalid_argument("DataFreshnessInSeconds is required")); + } + channel_data_freshness(&value["DataFreshnessInSeconds"]) +} + +/// `DataFreshnessInSeconds`, defaulted to 300 and range-checked. +fn channel_data_freshness(value: &Value) -> Result { + validate_optional_json_range( + "DataFreshnessInSeconds", + value, + MIN_CHANNEL_DATA_FRESHNESS_SECONDS, + MAX_CHANNEL_DATA_FRESHNESS_SECONDS, + )?; + Ok(value + .as_i64() + .unwrap_or(DEFAULT_CHANNEL_DATA_FRESHNESS_SECONDS) as i32) +} + +/// Parse `StreamConfigurationList`. Every `StreamARN` must name a stream that +/// exists in this account: AWS reports an unknown source stream as +/// `ResourceNotFoundException`. +pub(crate) fn parse_channel_streams( + state: &crate::state::KinesisState, + body: &Value, +) -> Result, AwsServiceError> { + let entries = body["StreamConfigurationList"] + .as_array() + .ok_or_else(|| invalid_argument("StreamConfigurationList is required"))?; + if entries.is_empty() || entries.len() > 10000 { + return Err(validation_exception( + "Value at 'streamConfigurationList' failed to satisfy constraint: \ + Member must have length between 1 and 10000", + )); + } + + let mut streams = Vec::with_capacity(entries.len()); + for entry in entries { + let stream_arn = require_channel_member(entry, "StreamARN", 2048)?; + let stream_name = state + .stream_name_from_arn(stream_arn) + .ok_or_else(|| resource_not_found_arn(stream_arn))?; + let stream = state + .streams + .get(&stream_name) + .ok_or_else(|| resource_not_found_arn(stream_arn))?; + + let record_configuration = &entry["RecordConfiguration"]; + if !record_configuration.is_object() { + return Err(invalid_argument("RecordConfiguration is required")); + } + let record_format_type = channel_enum_member( + record_configuration, + "RecordFormatType", + &["GSR_JSON", "JSON", "STRING", "BYTE_ARRAY"], + None, + )?; + let gsr_schema_arn = match record_configuration["GSRSchemaARN"] + .as_str() + .filter(|value| !value.is_empty()) + { + Some(arn) => { + validate_string_length("GSRSchemaARN", arn, 1, 2048)?; + Some(arn.to_string()) + } + None => None, + }; + + streams.push(KinesisChannelStream { + stream_arn: stream.stream_arn.clone(), + stream_creation_timestamp: stream.stream_creation_timestamp, + record_format_type, + gsr_schema_arn, + }); + } + Ok(streams) +} + +/// Parse the channel destination. Exactly one of the two destination shapes +/// must be supplied. +pub(crate) fn parse_channel_destination( + body: &Value, + channel_name: &str, + channel_id: &str, +) -> Result { + let s3 = &body["S3DestinationConfiguration"]; + let s3_tables = &body["S3TablesDestinationConfiguration"]; + match (s3.is_null(), s3_tables.is_null()) { + (false, false) => Err(invalid_argument( + "Specify either S3DestinationConfiguration or \ + S3TablesDestinationConfiguration, but not both", + )), + (true, true) => Err(invalid_argument( + "Either S3DestinationConfiguration or S3TablesDestinationConfiguration is required", + )), + (false, true) => { + let storage = parse_channel_storage(&s3["StorageConfiguration"])?; + let dead_letter_queue = parse_channel_dead_letter_queue( + &s3["DeadLetterQueueS3Configuration"], + // A general purpose S3 destination may omit the dead-letter + // queue; it then defaults to the destination bucket under an + // error prefix. + Some(&storage), + channel_name, + channel_id, + )?; + Ok(KinesisChannelDestination::S3 { + data_freshness_in_seconds: channel_data_freshness(&s3["DataFreshnessInSeconds"])?, + dead_letter_queue, + storage, + }) + } + (true, false) => { + let dead_letter_queue = parse_channel_dead_letter_queue( + &s3_tables["DeadLetterQueueS3Configuration"], + // Required for streaming tables: there is no destination + // bucket to fall back to. + None, + channel_name, + channel_id, + )?; + Ok(KinesisChannelDestination::S3Tables { + data_freshness_in_seconds: channel_data_freshness( + &s3_tables["DataFreshnessInSeconds"], + )?, + dead_letter_queue, + tables: parse_channel_tables(&s3_tables["S3TablesConfigurationList"])?, + }) + } + } +} + +fn parse_channel_storage(value: &Value) -> Result { + if !value.is_object() { + return Err(invalid_argument("StorageConfiguration is required")); + } + let output_key_template = match value["OutputKeyTemplate"] + .as_str() + .filter(|value| !value.is_empty()) + { + Some(template) => { + validate_string_length("OutputKeyTemplate", template, 1, 1024)?; + template.to_string() + } + None => DEFAULT_CHANNEL_OUTPUT_KEY_TEMPLATE.to_string(), + }; + Ok(KinesisChannelS3Storage { + bucket_arn: require_channel_member(value, "BucketARN", 2048)?.to_string(), + expected_bucket_owner: require_expected_bucket_owner(value)?, + output_key_template, + storage_class: channel_enum_member( + value, + "StorageClass", + &["STANDARD", "INTELLIGENT_TIERING", "GLACIER_IR"], + Some("STANDARD"), + )?, + compression_type: channel_enum_member( + value, + "CompressionType", + &["NONE", "GZIP", "ZSTD"], + None, + )?, + }) +} + +fn require_expected_bucket_owner(value: &Value) -> Result { + let owner = value["ExpectedBucketOwner"] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| invalid_argument("ExpectedBucketOwner is required"))?; + validate_string_length("ExpectedBucketOwner", owner, 12, 12)?; + Ok(owner.to_string()) +} + +/// Parse `DeadLetterQueueS3Configuration`. When `fallback` is `Some` an absent +/// configuration defaults to that bucket under the channel's error prefix; +/// when it is `None` the configuration is required. +fn parse_channel_dead_letter_queue( + value: &Value, + fallback: Option<&KinesisChannelS3Storage>, + channel_name: &str, + channel_id: &str, +) -> Result { + let default_prefix = format!("kinesis-channel/errors/{channel_name}/{channel_id}/"); + if !value.is_object() { + let fallback = fallback.ok_or_else(|| { + invalid_argument( + "DeadLetterQueueS3Configuration is required for streaming table destinations", + ) + })?; + return Ok(KinesisChannelDeadLetterQueue { + bucket_arn: fallback.bucket_arn.clone(), + expected_bucket_owner: fallback.expected_bucket_owner.clone(), + error_output_prefix: default_prefix, + }); + } + let error_output_prefix = match value["ErrorOutputPrefix"] + .as_str() + .filter(|value| !value.is_empty()) + { + Some(prefix) => { + validate_string_length("ErrorOutputPrefix", prefix, 1, 512)?; + prefix.to_string() + } + None => default_prefix, + }; + Ok(KinesisChannelDeadLetterQueue { + bucket_arn: require_channel_member(value, "BucketARN", 2048)?.to_string(), + expected_bucket_owner: require_expected_bucket_owner(value)?, + error_output_prefix, + }) +} + +fn parse_channel_tables(value: &Value) -> Result, AwsServiceError> { + let entries = value + .as_array() + .ok_or_else(|| invalid_argument("S3TablesConfigurationList is required"))?; + if entries.is_empty() || entries.len() > 10000 { + return Err(validation_exception( + "Value at 's3TablesConfigurationList' failed to satisfy constraint: \ + Member must have length between 1 and 10000", + )); + } + + let mut tables = Vec::with_capacity(entries.len()); + for entry in entries { + let partition_fields = match &entry["PartitionSpec"] { + Value::Null => Vec::new(), + spec => parse_channel_partition_fields(&spec["PartitionFields"])?, + }; + tables.push(KinesisChannelS3Table { + table_bucket_arn: require_channel_member(entry, "TableBucketARN", 2048)?.to_string(), + namespace: require_channel_member(entry, "Namespace", 255)?.to_string(), + table_name: require_channel_member(entry, "TableName", 255)?.to_string(), + compression_type: channel_enum_member( + entry, + "CompressionType", + &["NONE", "ZSTD", "SNAPPY"], + None, + )?, + partition_fields, + }); + } + Ok(tables) +} + +fn parse_channel_partition_fields( + value: &Value, +) -> Result, AwsServiceError> { + let entries = value + .as_array() + .ok_or_else(|| invalid_argument("PartitionFields is required"))?; + if entries.is_empty() || entries.len() > 10 { + return Err(validation_exception( + "Value at 'partitionFields' failed to satisfy constraint: \ + Member must have length between 1 and 10", + )); + } + entries + .iter() + .map(|entry| { + Ok(KinesisChannelPartitionField { + transform: channel_enum_member(entry, "Transform", &["TIME_HOUR"], None)?, + source_name: require_channel_member(entry, "SourceName", 255)?.to_string(), + }) + }) + .collect() +} + +/// Parse `ChannelEncryptionConfiguration`. Absent means the channel uses no +/// customer managed key. +pub(crate) fn parse_channel_encryption( + value: &Value, +) -> Result, AwsServiceError> { + if value.is_null() { + return Ok(None); + } + Ok(Some(KinesisChannelEncryption { + encryption_type: channel_enum_member(value, "EncryptionType", &["KMS"], None)?, + key_id: require_channel_member(value, "KeyId", 2048)?.to_string(), + })) +} + +/// Parse `ChannelLoggingConfiguration`. It is optional on `CreateChannel` but +/// required in `ChannelDescription`, so an absent one resolves to logging +/// disabled under the default log group and stream names. +pub(crate) fn parse_channel_logging( + value: &Value, + channel_name: &str, + channel_id: &str, +) -> Result { + let default_group = format!("/aws/kinesis/{channel_name}/{channel_id}"); + if value.is_null() { + return Ok(KinesisChannelLogging { + enabled: false, + log_group_name: default_group, + log_stream_name: DEFAULT_CHANNEL_LOG_STREAM_NAME.to_string(), + }); + } + let logs = &value["CloudWatchLogs"]; + if !logs.is_object() { + return Err(invalid_argument("CloudWatchLogs is required")); + } + let enabled = logs["Enabled"] + .as_bool() + .ok_or_else(|| invalid_argument("Enabled is required"))?; + let log_group_name = match logs["LogGroupName"] + .as_str() + .filter(|value| !value.is_empty()) + { + Some(name) => { + validate_string_length("LogGroupName", name, 1, 512)?; + name.to_string() + } + None => default_group, + }; + let log_stream_name = match logs["LogStreamName"] + .as_str() + .filter(|value| !value.is_empty()) + { + Some(name) => { + validate_string_length("LogStreamName", name, 1, 512)?; + name.to_string() + } + None => DEFAULT_CHANNEL_LOG_STREAM_NAME.to_string(), + }; + Ok(KinesisChannelLogging { + enabled, + log_group_name, + log_stream_name, + }) +} + +/// Render a channel as the `ChannelDescription` returned by CreateChannel, +/// DescribeChannel and UpdateChannel. +pub(crate) fn channel_description_json(channel: &KinesisChannel) -> Value { + let streams: Vec = channel + .streams + .iter() + .map(|source| { + let mut record_configuration = json!({ "RecordFormatType": source.record_format_type }); + if let Some(ref arn) = source.gsr_schema_arn { + record_configuration["GSRSchemaARN"] = json!(arn); + } + json!({ + "StreamARN": source.stream_arn, + "StreamCreationTimestamp": epoch_seconds(source.stream_creation_timestamp), + "RecordConfiguration": record_configuration, + }) + }) + .collect(); + + let mut description = json!({ + "ChannelName": channel.channel_name, + "ChannelARN": channel.channel_arn, + "ChannelId": channel.channel_id, + "ChannelStatus": channel.channel_status, + "ChannelCreationTimestamp": epoch_seconds(channel.channel_creation_timestamp), + "ServiceExecutionRoleARN": channel.service_execution_role_arn, + "StreamConfigurationList": streams, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": channel.logging.enabled, + "LogGroupName": channel.logging.log_group_name, + "LogStreamName": channel.logging.log_stream_name, + } + }, + }); + + match &channel.destination { + KinesisChannelDestination::S3 { + data_freshness_in_seconds, + dead_letter_queue, + storage, + } => { + description["S3DestinationConfiguration"] = json!({ + "DataFreshnessInSeconds": data_freshness_in_seconds, + "DeadLetterQueueS3Configuration": dead_letter_queue_json(dead_letter_queue), + "StorageConfiguration": { + "BucketARN": storage.bucket_arn, + "ExpectedBucketOwner": storage.expected_bucket_owner, + "OutputKeyTemplate": storage.output_key_template, + "StorageClass": storage.storage_class, + "CompressionType": storage.compression_type, + }, + }); + } + KinesisChannelDestination::S3Tables { + data_freshness_in_seconds, + dead_letter_queue, + tables, + } => { + let tables: Vec = tables + .iter() + .map(|table| { + let mut entry = json!({ + "TableBucketARN": table.table_bucket_arn, + "Namespace": table.namespace, + "TableName": table.table_name, + "CompressionType": table.compression_type, + }); + if !table.partition_fields.is_empty() { + entry["PartitionSpec"] = json!({ + "PartitionFields": table + .partition_fields + .iter() + .map(|field| json!({ + "Transform": field.transform, + "SourceName": field.source_name, + })) + .collect::>(), + }); + } + entry + }) + .collect(); + description["S3TablesDestinationConfiguration"] = json!({ + "DataFreshnessInSeconds": data_freshness_in_seconds, + "DeadLetterQueueS3Configuration": dead_letter_queue_json(dead_letter_queue), + "S3TablesConfigurationList": tables, + }); + } + } + + if let Some(ref encryption) = channel.encryption { + description["EncryptionConfiguration"] = json!({ + "EncryptionType": encryption.encryption_type, + "KeyId": encryption.key_id, + }); + } + description +} + +fn dead_letter_queue_json(dead_letter_queue: &KinesisChannelDeadLetterQueue) -> Value { + json!({ + "BucketARN": dead_letter_queue.bucket_arn, + "ExpectedBucketOwner": dead_letter_queue.expected_bucket_owner, + "ErrorOutputPrefix": dead_letter_queue.error_output_prefix, + }) +} + +/// Render a channel as the `ChannelSummary` returned by ListChannels. +pub(crate) fn channel_summary_json(channel: &KinesisChannel) -> Value { + json!({ + "ChannelName": channel.channel_name, + "ChannelARN": channel.channel_arn, + "ChannelId": channel.channel_id, + "ChannelStatus": channel.channel_status, + "ChannelCreationTimestamp": epoch_seconds(channel.channel_creation_timestamp), + "ChannelDestinationType": channel.destination.destination_type(), + "Streams": channel + .streams + .iter() + .map(|source| json!({ + "StreamARN": source.stream_arn, + "StreamCreationTimestamp": epoch_seconds(source.stream_creation_timestamp), + })) + .collect::>(), + }) +} + +/// Timestamps go on the wire as epoch seconds with millisecond precision, +/// the same encoding the stream and consumer responses use. +fn epoch_seconds(timestamp: chrono::DateTime) -> f64 { + timestamp.timestamp_millis() as f64 / 1000.0 +} + +/// One parsed `ListChannels` `StreamFilter` entry. +pub(crate) struct ChannelStreamFilter { + stream_arn: String, + creation_timestamp_millis: Option, +} + +/// Parse the `StreamFilter` list. Parsing up front (rather than per candidate +/// channel) means a malformed filter is rejected even when the account holds +/// no channels to evaluate it against. +pub(crate) fn parse_channel_stream_filters( + value: &Value, +) -> Result, AwsServiceError> { + if value.is_null() { + return Ok(Vec::new()); + } + let entries = value + .as_array() + .ok_or_else(|| invalid_argument("StreamFilter must be a list"))?; + if entries.is_empty() || entries.len() > 10000 { + return Err(validation_exception( + "Value at 'streamFilter' failed to satisfy constraint: \ + Member must have length between 1 and 10000", + )); + } + entries + .iter() + .map(|entry| { + let creation_timestamp_millis = match &entry["StreamCreationTimestamp"] { + Value::Null => None, + value => { + let seconds = value.as_f64().ok_or_else(|| { + invalid_argument("StreamCreationTimestamp must be an epoch timestamp") + })?; + Some((seconds * 1000.0).round() as i64) + } + }; + Ok(ChannelStreamFilter { + stream_arn: require_channel_member(entry, "StreamARN", 2048)?.to_string(), + creation_timestamp_millis, + }) + }) + .collect() +} + +/// A channel matches the filter list when any of its source streams matches +/// any filter entry. +pub(crate) fn channel_matches_stream_filters( + channel: &KinesisChannel, + filters: &[ChannelStreamFilter], +) -> bool { + filters.iter().any(|filter| { + channel.streams.iter().any(|source| { + source.stream_arn == filter.stream_arn + && filter.creation_timestamp_millis.is_none_or(|wanted| { + wanted == source.stream_creation_timestamp.timestamp_millis() + }) + }) + }) +} + +/// Encode a `ListChannels` continuation token. Like ListStreams, the opaque +/// cursor wraps the last returned channel name so the next page resumes +/// strictly after it. +pub(crate) fn encode_list_channels_token(last_channel_name: &str) -> String { + base64::engine::general_purpose::STANDARD.encode(last_channel_name) +} + +/// Decode a `ListChannels` continuation token. A garbage token is an +/// `InvalidArgumentException`, matching the ListShards cursor. +pub(crate) fn decode_list_channels_token(token: &str) -> Result { + let raw = base64::engine::general_purpose::STANDARD + .decode(token) + .map_err(|_| invalid_argument("Invalid NextToken"))?; + String::from_utf8(raw).map_err(|_| invalid_argument("Invalid NextToken")) +} + #[cfg(test)] mod sequence_discriminator_tests { use super::shard_discriminator; diff --git a/crates/fakecloud-kinesis/src/service_tests.rs b/crates/fakecloud-kinesis/src/service_tests.rs index 940c5b678..bc5f570c1 100644 --- a/crates/fakecloud-kinesis/src/service_tests.rs +++ b/crates/fakecloud-kinesis/src/service_tests.rs @@ -2428,3 +2428,655 @@ fn put_record_on_shardless_stream_errors_without_panic() { )); assert_code_kinesis(res, "InvalidArgumentException"); } + +// ── channel operations ── + +fn stream_arn_for(name: &str) -> String { + format!("arn:aws:kinesis:us-east-1:123456789012:stream/{name}") +} + +/// A minimal-but-valid CreateChannel body with a general purpose S3 +/// destination: one source stream, no dead-letter queue, no logging and no +/// encryption, so the defaults are the thing under test. +fn s3_channel_body(name: &str, stream_name: &str) -> Value { + json!({ + "ChannelName": name, + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/channel", + "StreamConfigurationList": [{ + "StreamARN": stream_arn_for(stream_name), + "RecordConfiguration": { "RecordFormatType": "JSON" }, + }], + "S3DestinationConfiguration": { + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::channel-bucket", + "ExpectedBucketOwner": "123456789012", + "CompressionType": "ZSTD", + } + }, + }) +} + +fn create_channel_action(svc: &KinesisService, name: &str, stream_name: &str) -> Value { + json_response( + svc.create_channel(&request( + "CreateChannel", + s3_channel_body(name, stream_name), + )) + .unwrap(), + ) +} + +#[test] +fn create_channel_returns_active_description_with_defaults() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + let created = create_channel_action(&svc, "deliveries", "orders"); + let description = &created["ChannelDescription"]; + + assert_eq!(description["ChannelName"], "deliveries"); + let channel_id = description["ChannelId"].as_str().unwrap(); + assert!(!channel_id.is_empty()); + // AWS keys the channel ARN off the channel id, not the channel name. + assert_eq!( + description["ChannelARN"], + format!("arn:aws:kinesis:us-east-1:123456789012:channel/{channel_id}") + ); + // Fakecloud provisions synchronously, so the channel never sits in CREATING. + assert_eq!(description["ChannelStatus"], "ACTIVE"); + assert!(description["ChannelCreationTimestamp"].as_f64().unwrap() > 0.0); + assert_eq!( + description["StreamConfigurationList"][0]["StreamARN"], + stream_arn_for("orders") + ); + assert_eq!( + description["StreamConfigurationList"][0]["RecordConfiguration"]["RecordFormatType"], + "JSON" + ); + + let s3 = &description["S3DestinationConfiguration"]; + assert_eq!(s3["DataFreshnessInSeconds"], 300); + assert_eq!(s3["StorageConfiguration"]["StorageClass"], "STANDARD"); + assert_eq!( + s3["StorageConfiguration"]["OutputKeyTemplate"], + DEFAULT_CHANNEL_OUTPUT_KEY_TEMPLATE + ); + // An omitted dead-letter queue defaults to the destination bucket under + // the channel's error prefix. + let channel_id = description["ChannelId"].as_str().unwrap(); + assert_eq!( + s3["DeadLetterQueueS3Configuration"]["BucketARN"], + "arn:aws:s3:::channel-bucket" + ); + assert_eq!( + s3["DeadLetterQueueS3Configuration"]["ErrorOutputPrefix"], + format!("kinesis-channel/errors/deliveries/{channel_id}/") + ); + + let logs = &description["LoggingConfiguration"]["CloudWatchLogs"]; + assert_eq!(logs["Enabled"], false); + assert_eq!( + logs["LogGroupName"], + format!("/aws/kinesis/deliveries/{channel_id}") + ); + assert_eq!(logs["LogStreamName"], "DestinationDelivery"); + assert!(description["EncryptionConfiguration"].is_null()); +} + +#[test] +fn create_channel_stores_tags_and_encryption() { + let (svc, state) = make_service(); + create_stream_action(&svc, "orders", 1); + + let mut body = s3_channel_body("deliveries", "orders"); + body["Tags"] = json!({ "env": "prod" }); + body["EncryptionConfiguration"] = json!({ + "EncryptionType": "KMS", + "KeyId": "arn:aws:kms:us-east-1:123456789012:key/abc", + }); + body["LoggingConfiguration"] = json!({ + "CloudWatchLogs": { "Enabled": true, "LogGroupName": "/aws/kinesis/custom" } + }); + let created = json_response(svc.create_channel(&request("CreateChannel", body)).unwrap()); + + assert_eq!( + created["ChannelDescription"]["EncryptionConfiguration"]["KeyId"], + "arn:aws:kms:us-east-1:123456789012:key/abc" + ); + let logs = &created["ChannelDescription"]["LoggingConfiguration"]["CloudWatchLogs"]; + assert_eq!(logs["Enabled"], true); + assert_eq!(logs["LogGroupName"], "/aws/kinesis/custom"); + assert_eq!(logs["LogStreamName"], "DestinationDelivery"); + + let guard = state.read(); + let stored = &guard.default_ref().channels["deliveries"]; + assert_eq!(stored.tags["env"], "prod"); +} + +#[test] +fn create_channel_accepts_s3_tables_destination() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + let body = json!({ + "ChannelName": "iceberg", + "ServiceExecutionRoleARN": "arn:aws:iam::123456789012:role/channel", + "StreamConfigurationList": [{ + "StreamARN": stream_arn_for("orders"), + "RecordConfiguration": { + "RecordFormatType": "GSR_JSON", + "GSRSchemaARN": "arn:aws:glue:us-east-1:123456789012:schema/s", + }, + }], + "S3TablesDestinationConfiguration": { + "DataFreshnessInSeconds": 600, + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::dlq-bucket", + "ExpectedBucketOwner": "123456789012", + }, + "S3TablesConfigurationList": [{ + "TableBucketARN": "arn:aws:s3tables:us-east-1:123456789012:bucket/tables", + "Namespace": "analytics", + "TableName": "events", + "CompressionType": "SNAPPY", + "PartitionSpec": { + "PartitionFields": [{ "Transform": "TIME_HOUR", "SourceName": "ts" }] + }, + }], + }, + }); + let created = json_response(svc.create_channel(&request("CreateChannel", body)).unwrap()); + let description = &created["ChannelDescription"]; + + let tables = &description["S3TablesDestinationConfiguration"]; + assert_eq!(tables["DataFreshnessInSeconds"], 600); + assert_eq!( + tables["S3TablesConfigurationList"][0]["TableName"], + "events" + ); + assert_eq!( + tables["S3TablesConfigurationList"][0]["PartitionSpec"]["PartitionFields"][0]["Transform"], + "TIME_HOUR" + ); + assert!(description["S3DestinationConfiguration"].is_null()); + assert_eq!( + description["StreamConfigurationList"][0]["RecordConfiguration"]["GSRSchemaARN"], + "arn:aws:glue:us-east-1:123456789012:schema/s" + ); +} + +#[test] +fn create_channel_rejects_duplicate_name() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + create_channel_action(&svc, "deliveries", "orders"); + + let res = svc.create_channel(&request( + "CreateChannel", + s3_channel_body("deliveries", "orders"), + )); + assert_code_kinesis(res, "ResourceInUseException"); +} + +#[test] +fn create_channel_requires_an_existing_source_stream() { + let (svc, _) = make_service(); + let res = svc.create_channel(&request( + "CreateChannel", + s3_channel_body("deliveries", "ghost"), + )); + assert_code_kinesis(res, "ResourceNotFoundException"); +} + +#[test] +fn create_channel_requires_required_members() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + let mut missing_name = s3_channel_body("deliveries", "orders"); + missing_name["ChannelName"] = Value::Null; + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", missing_name)), + "InvalidArgumentException", + ); + + let mut missing_role = s3_channel_body("deliveries", "orders"); + missing_role["ServiceExecutionRoleARN"] = Value::Null; + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", missing_role)), + "InvalidArgumentException", + ); + + let mut missing_streams = s3_channel_body("deliveries", "orders"); + missing_streams["StreamConfigurationList"] = Value::Null; + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", missing_streams)), + "InvalidArgumentException", + ); + + let mut empty_streams = s3_channel_body("deliveries", "orders"); + empty_streams["StreamConfigurationList"] = json!([]); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", empty_streams)), + "ValidationException", + ); + + // ChannelName is ^[a-zA-Z0-9_.-]+$, so a space is a pattern violation. + let bad_name = s3_channel_body("deliver ies", "orders"); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", bad_name)), + "ValidationException", + ); +} + +#[test] +fn create_channel_requires_exactly_one_destination() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + let mut none = s3_channel_body("deliveries", "orders"); + none["S3DestinationConfiguration"] = Value::Null; + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", none)), + "InvalidArgumentException", + ); + + let mut both = s3_channel_body("deliveries", "orders"); + both["S3TablesDestinationConfiguration"] = json!({ + "DeadLetterQueueS3Configuration": { + "BucketARN": "arn:aws:s3:::dlq-bucket", + "ExpectedBucketOwner": "123456789012", + }, + "S3TablesConfigurationList": [{ + "TableBucketARN": "arn:aws:s3tables:us-east-1:123456789012:bucket/tables", + "Namespace": "analytics", + "TableName": "events", + "CompressionType": "ZSTD", + }], + }); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", both)), + "InvalidArgumentException", + ); +} + +#[test] +fn create_channel_validates_destination_members() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + let mut out_of_range = s3_channel_body("deliveries", "orders"); + out_of_range["S3DestinationConfiguration"]["DataFreshnessInSeconds"] = json!(60); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", out_of_range)), + "ValidationException", + ); + + let mut missing_compression = s3_channel_body("deliveries", "orders"); + missing_compression["S3DestinationConfiguration"]["StorageConfiguration"]["CompressionType"] = + Value::Null; + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", missing_compression)), + "InvalidArgumentException", + ); + + let mut bad_storage_class = s3_channel_body("deliveries", "orders"); + bad_storage_class["S3DestinationConfiguration"]["StorageConfiguration"]["StorageClass"] = + json!("DEEP_ARCHIVE"); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", bad_storage_class)), + "InvalidArgumentException", + ); + + // A streaming table destination has no destination bucket to fall back + // to, so its dead-letter queue is required. + let mut tables_without_dlq = s3_channel_body("deliveries", "orders"); + tables_without_dlq["S3DestinationConfiguration"] = Value::Null; + tables_without_dlq["S3TablesDestinationConfiguration"] = json!({ + "S3TablesConfigurationList": [{ + "TableBucketARN": "arn:aws:s3tables:us-east-1:123456789012:bucket/tables", + "Namespace": "analytics", + "TableName": "events", + "CompressionType": "ZSTD", + }], + }); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", tables_without_dlq)), + "InvalidArgumentException", + ); +} + +#[test] +fn describe_channel_returns_stored_description() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap(); + + let described = json_response( + svc.describe_channel(&request("DescribeChannel", json!({ "ChannelARN": arn }))) + .unwrap(), + ); + assert_eq!( + described["ChannelDescription"], + created["ChannelDescription"] + ); +} + +#[test] +fn describe_channel_unknown_arn_errors() { + let (svc, _) = make_service(); + assert_code_kinesis( + svc.describe_channel(&request( + "DescribeChannel", + json!({ "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/ghost" }), + )), + "ResourceNotFoundException", + ); + assert_code_kinesis( + svc.describe_channel(&request("DescribeChannel", json!({}))), + "InvalidArgumentException", + ); +} + +#[test] +fn update_channel_changes_freshness_and_logging() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap(); + + let response = json_response( + svc.update_channel(&request( + "UpdateChannel", + json!({ + "ChannelARN": arn, + "S3DestinationConfiguration": { "DataFreshnessInSeconds": 900 }, + "LoggingConfiguration": { + "CloudWatchLogs": { + "Enabled": true, + "LogGroupName": "/aws/kinesis/updated", + "LogStreamName": "updated-stream", + } + }, + }), + )) + .unwrap(), + ); + let updated = &response["ChannelDescription"]; + + assert_eq!( + updated["S3DestinationConfiguration"]["DataFreshnessInSeconds"], + 900 + ); + let logs = &updated["LoggingConfiguration"]["CloudWatchLogs"]; + assert_eq!(logs["Enabled"], true); + assert_eq!(logs["LogGroupName"], "/aws/kinesis/updated"); + assert_eq!(logs["LogStreamName"], "updated-stream"); + // The destination itself is untouched by the update. + assert_eq!( + updated["S3DestinationConfiguration"]["StorageConfiguration"]["BucketARN"], + "arn:aws:s3:::channel-bucket" + ); + + // The change is persisted, not just echoed. + let described = json_response( + svc.describe_channel(&request( + "DescribeChannel", + json!({ "ChannelARN": updated["ChannelARN"].as_str().unwrap() }), + )) + .unwrap(), + ); + assert_eq!( + described["ChannelDescription"]["S3DestinationConfiguration"]["DataFreshnessInSeconds"], + 900 + ); +} + +#[test] +fn update_channel_rejects_mismatched_destination_and_bad_values() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap(); + + assert_code_kinesis( + svc.update_channel(&request( + "UpdateChannel", + json!({ + "ChannelARN": arn, + "S3TablesDestinationConfiguration": { "DataFreshnessInSeconds": 600 }, + }), + )), + "InvalidArgumentException", + ); + assert_code_kinesis( + svc.update_channel(&request( + "UpdateChannel", + json!({ + "ChannelARN": arn, + "S3DestinationConfiguration": { "DataFreshnessInSeconds": 60 }, + }), + )), + "ValidationException", + ); + assert_code_kinesis( + svc.update_channel(&request( + "UpdateChannel", + json!({ "ChannelARN": arn, "S3DestinationConfiguration": {} }), + )), + "InvalidArgumentException", + ); + assert_code_kinesis( + svc.update_channel(&request( + "UpdateChannel", + json!({ "ChannelARN": "arn:aws:kinesis:us-east-1:123456789012:channel/ghost" }), + )), + "ResourceNotFoundException", + ); +} + +#[test] +fn delete_channel_removes_it() { + let (svc, state) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap(); + + svc.delete_channel(&request("DeleteChannel", json!({ "ChannelARN": arn }))) + .unwrap(); + assert!(state.read().default_ref().channels.is_empty()); + + assert_code_kinesis( + svc.delete_channel(&request("DeleteChannel", json!({ "ChannelARN": arn }))), + "ResourceNotFoundException", + ); +} + +#[test] +fn list_channels_filters_by_stream() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + create_stream_action(&svc, "payments", 1); + create_channel_action(&svc, "orders-channel", "orders"); + create_channel_action(&svc, "payments-channel", "payments"); + + let all = json_response( + svc.list_channels(&request("ListChannels", json!({}))) + .unwrap(), + ); + assert_eq!(all["ChannelSummaries"].as_array().unwrap().len(), 2); + assert_eq!(all["ChannelSummaries"][0]["ChannelName"], "orders-channel"); + assert_eq!(all["ChannelSummaries"][0]["ChannelDestinationType"], "S3"); + assert_eq!( + all["ChannelSummaries"][0]["Streams"][0]["StreamARN"], + stream_arn_for("orders") + ); + + let filtered = json_response( + svc.list_channels(&request( + "ListChannels", + json!({ "StreamFilter": [{ "StreamARN": stream_arn_for("payments") }] }), + )) + .unwrap(), + ); + let summaries = filtered["ChannelSummaries"].as_array().unwrap(); + assert_eq!(summaries.len(), 1); + assert_eq!(summaries[0]["ChannelName"], "payments-channel"); + + // A filter that names no source stream of any channel matches nothing. + let unmatched = json_response( + svc.list_channels(&request( + "ListChannels", + json!({ "StreamFilter": [{ "StreamARN": stream_arn_for("ghost") }] }), + )) + .unwrap(), + ); + assert!(unmatched["ChannelSummaries"].as_array().unwrap().is_empty()); + + // StreamCreationTimestamp, when supplied, must also match. + let creation = all["ChannelSummaries"][0]["Streams"][0]["StreamCreationTimestamp"] + .as_f64() + .unwrap(); + let with_timestamp = json_response( + svc.list_channels(&request( + "ListChannels", + json!({ + "StreamFilter": [{ + "StreamARN": stream_arn_for("orders"), + "StreamCreationTimestamp": creation, + }] + }), + )) + .unwrap(), + ); + assert_eq!( + with_timestamp["ChannelSummaries"].as_array().unwrap().len(), + 1 + ); + let wrong_timestamp = json_response( + svc.list_channels(&request( + "ListChannels", + json!({ + "StreamFilter": [{ + "StreamARN": stream_arn_for("orders"), + "StreamCreationTimestamp": creation + 3600.0, + }] + }), + )) + .unwrap(), + ); + assert!(wrong_timestamp["ChannelSummaries"] + .as_array() + .unwrap() + .is_empty()); +} + +#[test] +fn list_channels_paginates() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + for name in ["a", "b", "c"] { + create_channel_action(&svc, name, "orders"); + } + + let page1 = json_response( + svc.list_channels(&request("ListChannels", json!({ "MaxResults": 2 }))) + .unwrap(), + ); + let names: Vec<&str> = page1["ChannelSummaries"] + .as_array() + .unwrap() + .iter() + .map(|s| s["ChannelName"].as_str().unwrap()) + .collect(); + assert_eq!(names, vec!["a", "b"]); + let token = page1["NextToken"].as_str().unwrap().to_string(); + + let page2 = json_response( + svc.list_channels(&request("ListChannels", json!({ "NextToken": token }))) + .unwrap(), + ); + assert_eq!(page2["ChannelSummaries"][0]["ChannelName"], "c"); + assert_eq!(page2["ChannelSummaries"].as_array().unwrap().len(), 1); + assert!( + page2["NextToken"].is_null(), + "last page must not carry a cursor" + ); +} + +#[test] +fn list_channels_rejects_bad_input() { + let (svc, _) = make_service(); + assert_code_kinesis( + svc.list_channels(&request("ListChannels", json!({ "MaxResults": 0 }))), + "ValidationException", + ); + assert_code_kinesis( + svc.list_channels(&request("ListChannels", json!({ "StreamFilter": [{}] }))), + "InvalidArgumentException", + ); + assert_code_kinesis( + svc.list_channels(&request( + "ListChannels", + json!({ "NextToken": "not base64 !!" }), + )), + "InvalidArgumentException", + ); +} + +#[test] +fn delete_stream_is_blocked_while_a_channel_is_attached() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap(); + + assert_code_kinesis( + svc.delete_stream(&request("DeleteStream", json!({ "StreamName": "orders" }))), + "ResourceInUseException", + ); + + svc.delete_channel(&request("DeleteChannel", json!({ "ChannelARN": arn }))) + .unwrap(); + svc.delete_stream(&request("DeleteStream", json!({ "StreamName": "orders" }))) + .unwrap(); +} + +#[test] +fn channel_actions_are_supported_and_mutating() { + let svc = KinesisService::new(Arc::new(RwLock::new( + fakecloud_core::multi_account::MultiAccountState::new( + "123456789012", + "us-east-1", + "http://localhost:4566", + ), + ))); + for action in [ + "CreateChannel", + "DescribeChannel", + "ListChannels", + "UpdateChannel", + "DeleteChannel", + ] { + assert!( + svc.supported_actions().contains(&action), + "{action} missing from SUPPORTED_ACTIONS" + ); + } + for action in ["CreateChannel", "UpdateChannel", "DeleteChannel"] { + assert!(is_mutating_action(action), "{action} must be snapshotted"); + } + assert!(!is_mutating_action("DescribeChannel")); + assert!(!is_mutating_action("ListChannels")); +} diff --git a/crates/fakecloud-kinesis/src/state.rs b/crates/fakecloud-kinesis/src/state.rs index efc8a3f59..a16eaf751 100644 --- a/crates/fakecloud-kinesis/src/state.rs +++ b/crates/fakecloud-kinesis/src/state.rs @@ -38,6 +38,10 @@ pub struct KinesisState { pub iterator_counter: u64, pub lambda_checkpoints: BTreeMap, pub consumers: BTreeMap, + /// Delivery channels, keyed by channel name (unique per account+region on + /// AWS). Defaulted so snapshots written before channels existed still load. + #[serde(default)] + pub channels: BTreeMap, pub resource_policies: BTreeMap, pub shard_limit: i32, pub on_demand_stream_count_limit: i32, @@ -93,6 +97,129 @@ pub struct ShardIteratorLease { pub expires_at: DateTime, } +/// A delivery channel (`CreateChannel`): it fans records from one or more +/// source streams into a general purpose Amazon S3 bucket or into streaming +/// tables on Apache Iceberg in Amazon S3 Tables. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannel { + pub channel_name: String, + pub channel_arn: String, + pub channel_id: String, + pub channel_status: String, + pub channel_creation_timestamp: DateTime, + pub service_execution_role_arn: String, + pub streams: Vec, + pub destination: KinesisChannelDestination, + pub encryption: Option, + pub logging: KinesisChannelLogging, + pub tags: BTreeMap, +} + +/// One source stream of a channel (`ChannelStreamDescription`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelStream { + pub stream_arn: String, + pub stream_creation_timestamp: DateTime, + pub record_format_type: String, + pub gsr_schema_arn: Option, +} + +/// A channel's destination. Exactly one of the two destination shapes is +/// supplied to `CreateChannel`, so the stored form is an enum rather than two +/// optional structs that could both be set or both be missing. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum KinesisChannelDestination { + /// `S3DestinationConfiguration`: a general purpose Amazon S3 bucket. + S3 { + data_freshness_in_seconds: i32, + dead_letter_queue: KinesisChannelDeadLetterQueue, + storage: KinesisChannelS3Storage, + }, + /// `S3TablesDestinationConfiguration`: streaming tables on Apache Iceberg. + S3Tables { + data_freshness_in_seconds: i32, + dead_letter_queue: KinesisChannelDeadLetterQueue, + tables: Vec, + }, +} + +impl KinesisChannelDestination { + /// `ChannelDestinationType` for this destination, as reported by + /// `ListChannels`. + pub fn destination_type(&self) -> &'static str { + match self { + Self::S3 { .. } => "S3", + Self::S3Tables { .. } => "S3_TABLES", + } + } + + /// The only member `UpdateChannel` may change on either destination. + pub fn data_freshness_mut(&mut self) -> &mut i32 { + match self { + Self::S3 { + data_freshness_in_seconds, + .. + } + | Self::S3Tables { + data_freshness_in_seconds, + .. + } => data_freshness_in_seconds, + } + } +} + +/// `S3StorageConfiguration`: where an S3-destination channel writes records. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelS3Storage { + pub bucket_arn: String, + pub expected_bucket_owner: String, + pub output_key_template: String, + pub storage_class: String, + pub compression_type: String, +} + +/// `DeadLetterQueueS3Configuration`: where undeliverable records land. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelDeadLetterQueue { + pub bucket_arn: String, + pub expected_bucket_owner: String, + pub error_output_prefix: String, +} + +/// `S3TablesConfiguration`: one streaming table of an S3 Tables destination. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelS3Table { + pub table_bucket_arn: String, + pub namespace: String, + pub table_name: String, + pub compression_type: String, + /// `PartitionSpec.PartitionFields`; empty when no spec was supplied. + pub partition_fields: Vec, +} + +/// One `PartitionField` of a streaming table's `PartitionSpec`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelPartitionField { + pub transform: String, + pub source_name: String, +} + +/// `ChannelEncryptionConfiguration`: the customer managed KMS key used for +/// data delivered to the destination. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelEncryption { + pub encryption_type: String, + pub key_id: String, +} + +/// `ChannelLoggingConfiguration.CloudWatchLogs`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KinesisChannelLogging { + pub enabled: bool, + pub log_group_name: String, + pub log_stream_name: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KinesisConsumer { pub consumer_name: String, @@ -112,6 +239,7 @@ impl KinesisState { iterator_counter: 0, lambda_checkpoints: BTreeMap::new(), consumers: BTreeMap::new(), + channels: BTreeMap::new(), resource_policies: BTreeMap::new(), shard_limit: 500, on_demand_stream_count_limit: 50, @@ -124,6 +252,7 @@ impl KinesisState { self.iterators.clear(); self.lambda_checkpoints.clear(); self.consumers.clear(); + self.channels.clear(); self.resource_policies.clear(); self.billing_commitment_status = "DISABLED".to_string(); } @@ -144,6 +273,43 @@ impl KinesisState { ) } + // Like `stream_arn`, the ARN carries the request's credential-scope region. + // AWS puts the channel's id, not its name, in the resource segment. + pub fn channel_arn(&self, region: &str, channel_id: &str) -> String { + format!( + "arn:aws:kinesis:{}:{}:channel/{}", + region, self.account_id, channel_id + ) + } + + /// Resolve a `ChannelARN` to the name of an existing channel. Like + /// [`KinesisState::stream_name_from_arn`] the lookup keys off the ARN's + /// resource segment, so a caller whose credential-scope region differs + /// from the region the channel was created in still resolves it. The + /// segment is the channel id, so the match is against `channel_id`. + pub fn channel_name_from_arn(&self, arn: &str) -> Option { + let (_, channel_id) = arn.rsplit_once(":channel/")?; + self.channels + .values() + .find(|channel| channel.channel_id == channel_id) + .map(|channel| channel.channel_name.clone()) + } + + /// Names of the channels that draw from `stream_arn`. A stream cannot be + /// deleted while any channel is attached to it. + pub fn channels_for_stream(&self, stream_arn: &str) -> Vec { + self.channels + .values() + .filter(|channel| { + channel + .streams + .iter() + .any(|source| source.stream_arn == stream_arn) + }) + .map(|channel| channel.channel_name.clone()) + .collect() + } + pub fn insert_iterator( &mut self, stream_name: &str, @@ -287,6 +453,119 @@ mod tests { ); } + fn test_channel(state: &KinesisState, name: &str) -> KinesisChannel { + KinesisChannel { + channel_name: name.to_string(), + channel_arn: state.channel_arn(&state.region, "11111111-2222-3333-4444-555555555555"), + channel_id: "11111111-2222-3333-4444-555555555555".to_string(), + channel_status: "ACTIVE".to_string(), + channel_creation_timestamp: Utc::now(), + service_execution_role_arn: "arn:aws:iam::123456789012:role/channel".to_string(), + streams: vec![KinesisChannelStream { + stream_arn: state.stream_arn(&state.region, "orders"), + stream_creation_timestamp: Utc::now(), + record_format_type: "JSON".to_string(), + gsr_schema_arn: None, + }], + destination: KinesisChannelDestination::S3 { + data_freshness_in_seconds: 300, + dead_letter_queue: KinesisChannelDeadLetterQueue { + bucket_arn: "arn:aws:s3:::channel-bucket".to_string(), + expected_bucket_owner: "123456789012".to_string(), + error_output_prefix: "errors/".to_string(), + }, + storage: KinesisChannelS3Storage { + bucket_arn: "arn:aws:s3:::channel-bucket".to_string(), + expected_bucket_owner: "123456789012".to_string(), + output_key_template: "kinesis-channel/!{channel-name}".to_string(), + storage_class: "STANDARD".to_string(), + compression_type: "ZSTD".to_string(), + }, + }, + encryption: None, + logging: KinesisChannelLogging { + enabled: false, + log_group_name: format!("/aws/kinesis/{name}"), + log_stream_name: "DestinationDelivery".to_string(), + }, + tags: BTreeMap::new(), + } + } + + #[test] + fn channel_arn_format() { + let state = KinesisState::new("123456789012", "us-east-1"); + assert_eq!( + state.channel_arn(&state.region, "11111111-2222-3333-4444-555555555555"), + "arn:aws:kinesis:us-east-1:123456789012:channel/11111111-2222-3333-4444-555555555555" + ); + } + + #[test] + fn channel_name_from_arn_resolves_only_existing_channels() { + let mut state = KinesisState::new("123456789012", "us-east-1"); + let channel = test_channel(&state, "deliveries"); + let arn = channel.channel_arn.clone(); + state.channels.insert("deliveries".to_string(), channel); + + assert_eq!( + state.channel_name_from_arn(&arn), + Some("deliveries".to_string()) + ); + // Another region's ARN still resolves: the id is region-independent. + assert_eq!( + state.channel_name_from_arn( + "arn:aws:kinesis:eu-west-1:123456789012:channel/11111111-2222-3333-4444-555555555555" + ), + Some("deliveries".to_string()) + ); + assert_eq!( + state.channel_name_from_arn("arn:aws:kinesis:us-east-1:123456789012:channel/ghost"), + None + ); + } + + #[test] + fn channels_survive_snapshot_round_trip() { + let mut state = KinesisState::new("123456789012", "us-east-1"); + let channel = test_channel(&state, "deliveries"); + state.channels.insert("deliveries".to_string(), channel); + + let json = serde_json::to_string(&state).unwrap(); + let restored: KinesisState = serde_json::from_str(&json).unwrap(); + let restored_channel = &restored.channels["deliveries"]; + assert_eq!(restored_channel.channel_status, "ACTIVE"); + assert_eq!(restored_channel.destination.destination_type(), "S3"); + assert_eq!(restored_channel.streams.len(), 1); + } + + #[test] + fn snapshot_without_channels_still_loads() { + // A snapshot written before channels existed has no `channels` key; + // it must still deserialize (into an empty map) rather than fail the + // whole restore. + let state = KinesisState::new("123456789012", "us-east-1"); + let mut json = serde_json::to_value(&state).unwrap(); + json.as_object_mut().unwrap().remove("channels"); + let restored: KinesisState = serde_json::from_value(json).unwrap(); + assert!(restored.channels.is_empty()); + } + + #[test] + fn channels_for_stream_lists_attached_channels() { + let mut state = KinesisState::new("123456789012", "us-east-1"); + let channel = test_channel(&state, "deliveries"); + state.channels.insert("deliveries".to_string(), channel); + + assert_eq!( + state.channels_for_stream(&state.stream_arn(&state.region, "orders")), + vec!["deliveries".to_string()] + ); + assert!(state + .channels_for_stream(&state.stream_arn(&state.region, "other")) + .is_empty()); + } + #[test] fn stream_name_from_arn_unknown_stream_returns_none() { let state = KinesisState::new("123456789012", "us-east-1"); diff --git a/crates/fakecloud-server/src/main.rs b/crates/fakecloud-server/src/main.rs index c2411aa33..76240999d 100644 --- a/crates/fakecloud-server/src/main.rs +++ b/crates/fakecloud-server/src/main.rs @@ -30,6 +30,7 @@ mod runtime; mod ses_smtp; mod sqs_lambda_poller; mod stepfunctions_delivery; +mod support_attachments; use cli::Cli; use dynamodb_streams_lambda_poller::DynamoDbStreamsLambdaPoller; use introspection::{ @@ -4344,6 +4345,9 @@ async fn main() { if let Some(h) = support_service.snapshot_hook() { cfn_snapshot_hooks.insert("support", h); } + // The presigned attachment upload / download routes write uploaded parts + // straight into Support state, so they snapshot with the same hook. + let support_attachment_snapshot_hook = support_service.snapshot_hook(); registry.register(Arc::new(support_service)); let cloudwatch_snapshot_store: Option> = if persistence_config.mode == fakecloud_persistence::StorageMode::Persistent { @@ -11971,6 +11975,16 @@ async fn main() { None => axum::Router::new(), } }) + .merge({ + // AWS Support hands out presigned attachment upload / download + // links; they point back here and are served by these routes, so a + // client that follows the URL really transfers bytes. Authorised + // by the link's own `X-Amz-Signature`, never by SigV4. + support_attachments::router(support_attachments::SupportAttachmentRoutesContext { + support_state: support_state.clone(), + snapshot: support_attachment_snapshot_hook, + }) + }) .fallback(dispatch::dispatch) .layer({ let registry_arc = Arc::new(registry); diff --git a/crates/fakecloud-server/src/support_attachments.rs b/crates/fakecloud-server/src/support_attachments.rs new file mode 100644 index 000000000..185635729 --- /dev/null +++ b/crates/fakecloud-server/src/support_attachments.rs @@ -0,0 +1,229 @@ +//! HTTP endpoints behind the presigned attachment links AWS Support hands out. +//! +//! `GetAttachmentUploadLinks` returns one presigned `PUT` link per part and +//! `GetAttachmentDownloadLink` returns a presigned `GET` link; real AWS points +//! both at S3, fakecloud points them at itself and serves them here so a client +//! that simply follows the URL really does transfer bytes. +//! +//! Presigned URLs are unauthenticated by design, so authorisation is the +//! `X-Amz-Signature` query parameter: `fakecloud_support::dataplane` checks it +//! against the value recorded when the link was issued, along with the link's +//! expiry. All the state handling lives there; this module is the transport +//! shim. + +use std::collections::HashMap; + +use axum::body::Bytes; +use axum::extract::{Path, Query, State}; +use axum::http::{header, StatusCode}; +use axum::response::IntoResponse; +use axum::routing::{get, put}; +use axum::Router; + +use fakecloud_persistence::SnapshotHook; +use fakecloud_support::dataplane::{DownloadOutcome, PutPartOutcome}; +use fakecloud_support::SharedSupportState; + +/// Routes mounted under `/_fakecloud/support/attachments/*`. +#[derive(Clone)] +pub struct SupportAttachmentRoutesContext { + pub support_state: SharedSupportState, + /// Persist hook, `None` in memory mode. Uploaded parts are real state, so + /// a successful `PUT` snapshots like any other Support mutation. + pub snapshot: Option, +} + +pub fn router(ctx: SupportAttachmentRoutesContext) -> Router { + Router::new() + .route( + "/_fakecloud/support/attachments/uploads/{account_id}/{upload_id}/{part_index}", + put(put_part), + ) + .route( + "/_fakecloud/support/attachments/downloads/{account_id}/{attachment_id}", + get(download_attachment), + ) + .with_state(ctx) +} + +fn signature(params: &HashMap) -> String { + params.get("X-Amz-Signature").cloned().unwrap_or_default() +} + +async fn put_part( + Path((account_id, upload_id, part_index)): Path<(String, String, i64)>, + Query(params): Query>, + State(ctx): State, + body: Bytes, +) -> impl IntoResponse { + let outcome = fakecloud_support::dataplane::put_upload_part( + &ctx.support_state, + &account_id, + &upload_id, + part_index, + &signature(¶ms), + &body, + ); + match outcome { + PutPartOutcome::Stored(etag) => { + if let Some(hook) = &ctx.snapshot { + hook().await; + } + (StatusCode::OK, [(header::ETAG, etag)], ()).into_response() + } + PutPartOutcome::NotFound => { + (StatusCode::NOT_FOUND, "no such attachment upload part").into_response() + } + PutPartOutcome::Forbidden => { + (StatusCode::FORBIDDEN, "invalid presigned link signature").into_response() + } + PutPartOutcome::Expired => { + (StatusCode::FORBIDDEN, "presigned link has expired").into_response() + } + PutPartOutcome::AlreadyCompleted => ( + StatusCode::CONFLICT, + "the attachment upload is already complete", + ) + .into_response(), + } +} + +async fn download_attachment( + Path((account_id, attachment_id)): Path<(String, String)>, + Query(params): Query>, + State(ctx): State, +) -> impl IntoResponse { + let outcome = fakecloud_support::dataplane::fetch_attachment( + &ctx.support_state, + &account_id, + &attachment_id, + &signature(¶ms), + ); + match outcome { + DownloadOutcome::Found(file_name, bytes) => ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, "application/octet-stream".to_string()), + ( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{file_name}\""), + ), + ], + bytes, + ) + .into_response(), + DownloadOutcome::NotFound => (StatusCode::NOT_FOUND, "no such attachment").into_response(), + DownloadOutcome::Forbidden => { + (StatusCode::FORBIDDEN, "invalid presigned link signature").into_response() + } + DownloadOutcome::Expired => { + (StatusCode::FORBIDDEN, "presigned link has expired").into_response() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::Request; + use fakecloud_core::multi_account::MultiAccountState; + use fakecloud_support::state::{AttachmentUpload, UploadPart, UPLOAD_NOT_READY}; + use parking_lot::RwLock; + use std::sync::Arc; + use tower::ServiceExt; + + const ACCOUNT: &str = "000000000000"; + + fn context() -> SupportAttachmentRoutesContext { + let state: SharedSupportState = Arc::new(RwLock::new(MultiAccountState::new( + ACCOUNT, + "us-east-1", + "http://localhost:4566", + ))); + { + let mut guard = state.write(); + let data = guard.get_or_create(ACCOUNT); + data.attachment_uploads.insert( + "upload-1".to_string(), + AttachmentUpload { + upload_id: "upload-1".to_string(), + file_name: "log.txt".to_string(), + file_size_bytes: 5, + part_size_bytes: 5 * 1024 * 1024, + total_parts: 1, + status: UPLOAD_NOT_READY.to_string(), + expiry: "2999-01-01T00:00:00.000Z".to_string(), + parts: vec![UploadPart { + part_index: 1, + signature: "sig-1".to_string(), + expiry: "2999-01-01T00:00:00.000Z".to_string(), + etag: None, + data: None, + }], + attachment_id: None, + }, + ); + } + SupportAttachmentRoutesContext { + support_state: state, + snapshot: None, + } + } + + #[tokio::test] + async fn put_with_a_valid_signature_stores_the_part() { + let ctx = context(); + let state = ctx.support_state.clone(); + let response = router(ctx) + .oneshot( + Request::builder() + .method("PUT") + .uri("/_fakecloud/support/attachments/uploads/000000000000/upload-1/1?X-Amz-Signature=sig-1") + .body(Body::from("hello")) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::ETAG).unwrap(), + "\"5d41402abc4b2a76b9719d911017c592\"" + ); + let guard = state.read(); + assert!(guard.get(ACCOUNT).unwrap().attachment_uploads["upload-1"] + .part(1) + .unwrap() + .data + .is_some()); + } + + #[tokio::test] + async fn put_with_a_bad_signature_is_forbidden() { + let response = router(context()) + .oneshot( + Request::builder() + .method("PUT") + .uri("/_fakecloud/support/attachments/uploads/000000000000/upload-1/1?X-Amz-Signature=nope") + .body(Body::from("hello")) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn download_without_a_grant_is_forbidden() { + let response = router(context()) + .oneshot( + Request::builder() + .uri("/_fakecloud/support/attachments/downloads/000000000000/attachment-1?X-Amz-Signature=nope") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } +} diff --git a/crates/fakecloud-ses/src/fanout.rs b/crates/fakecloud-ses/src/fanout.rs index cb52f633d..1774cb75b 100644 --- a/crates/fakecloud-ses/src/fanout.rs +++ b/crates/fakecloud-ses/src/fanout.rs @@ -767,6 +767,7 @@ mod tests { reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); @@ -810,6 +811,7 @@ mod tests { reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); diff --git a/crates/fakecloud-ses/src/service/configuration_sets.rs b/crates/fakecloud-ses/src/service/configuration_sets.rs index 55cb088ef..95587e317 100644 --- a/crates/fakecloud-ses/src/service/configuration_sets.rs +++ b/crates/fakecloud-ses/src/service/configuration_sets.rs @@ -8,7 +8,7 @@ use crate::state::SesState; use super::{ event_destination_to_json, extract_string_array, parse_event_destination_definition, - SesV2Service, + validate_message_security_options, SesV2Service, }; impl SesV2Service { @@ -80,6 +80,21 @@ impl SesV2Service { // the caller sent it at all — that's what GetConfigurationSet // needs to mirror back. let archiving_options_present = body["ArchivingOptions"].is_object(); + // MessageSecurityOptions carries the S/MIME SigningScheme union. + // Validate it up front so a malformed union is rejected rather + // than stored and echoed back on GetConfigurationSet. + let message_security_options = if body["MessageSecurityOptions"].is_object() { + if let Err(msg) = validate_message_security_options(&body["MessageSecurityOptions"]) { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &msg, + )); + } + Some(body["MessageSecurityOptions"].clone()) + } else { + None + }; state.configuration_sets.insert( name.clone(), @@ -95,6 +110,7 @@ impl SesV2Service { reputation_metrics_enabled, vdm_options, archive_arn, + message_security_options, archiving_options_present, }, ); @@ -230,6 +246,10 @@ impl SesV2Service { response["VdmOptions"] = vdm.clone(); } + if let Some(ref security) = cs.message_security_options { + response["MessageSecurityOptions"] = security.clone(); + } + if cs.archiving_options_present || cs.archive_arn.is_some() { let mut archiving = serde_json::Map::new(); if let Some(ref arn) = cs.archive_arn { @@ -252,6 +272,65 @@ impl SesV2Service { Ok(AwsResponse::json(StatusCode::OK, response.to_string())) } + /// UpdateConfigurationSet performs a partial update: only the + /// attributes present in the request body are written, everything + /// else on the stored set is left alone. The configuration set is + /// named in the body (the URI is the fixed + /// `/v2/email/update-configuration-sets`), so an unknown name is a + /// NotFoundException rather than an unroutable path. + pub(super) fn update_configuration_set( + &self, + req: &AwsRequest, + ) -> Result { + let body: Value = Self::parse_body(req)?; + let name = match body["ConfigurationSetName"].as_str() { + Some(n) => n.to_string(), + None => { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + "ConfigurationSetName is required", + )); + } + }; + if name.is_empty() { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + "ConfigurationSetName must not be empty", + )); + } + if body["MessageSecurityOptions"].is_object() { + if let Err(msg) = validate_message_security_options(&body["MessageSecurityOptions"]) { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &msg, + )); + } + } + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&req.account_id); + + let cs = match state.configuration_sets.get_mut(&name) { + Some(cs) => cs, + None => { + return Ok(Self::json_error( + StatusCode::NOT_FOUND, + "NotFoundException", + &format!("Configuration set {} does not exist", name), + )); + } + }; + + if body["MessageSecurityOptions"].is_object() { + cs.message_security_options = Some(body["MessageSecurityOptions"].clone()); + } + + Ok(AwsResponse::json(StatusCode::OK, "{}")) + } + pub(super) fn delete_configuration_set( &self, name: &str, diff --git a/crates/fakecloud-ses/src/service/helpers.rs b/crates/fakecloud-ses/src/service/helpers.rs index 57d9c4c67..f72ab0b2c 100644 --- a/crates/fakecloud-ses/src/service/helpers.rs +++ b/crates/fakecloud-ses/src/service/helpers.rs @@ -75,6 +75,65 @@ pub(crate) fn resolve_identities_action( } } +/// S/MIME certificate associations hang off the singular `identity` +/// collection (not `identities`), and every op is a POST with the +/// EmailIdentity in the body: +/// POST /v2/email/identity/certificates -> AssociateEmailIdentityCertificate +/// POST /v2/email/identity/certificates/delete -> DisassociateEmailIdentityCertificate +/// POST /v2/email/identity/certificates/list -> ListEmailIdentityCertificates +pub(crate) fn resolve_identity_certificates_action( + method: &Method, + segs: &[String], +) -> ResolvedAction { + if segs.get(3).map(|s| s.as_str()) != Some("certificates") { + return None; + } + match (method, segs.len()) { + (&Method::POST, 4) => Some(("AssociateEmailIdentityCertificate", None, None)), + (&Method::POST, 5) if segs[4] == "delete" => { + Some(("DisassociateEmailIdentityCertificate", None, None)) + } + (&Method::POST, 5) if segs[4] == "list" => { + Some(("ListEmailIdentityCertificates", None, None)) + } + _ => None, + } +} + +/// Validate a `MessageSecurityOptions` block. `SigningScheme` is a Smithy +/// union, so at most one member may be set, and the only modeled S/MIME +/// signature format is `DETACHED`. +pub(crate) fn validate_message_security_options(options: &Value) -> Result<(), String> { + let scheme = &options["SigningScheme"]; + if scheme.is_null() { + return Ok(()); + } + let Some(members) = scheme.as_object() else { + return Err("SigningScheme must be a structure".to_string()); + }; + if members.len() > 1 { + return Err(format!( + "SigningScheme is a union and accepts exactly one member, got {}", + members.len() + )); + } + match members.keys().next().map(String::as_str) { + None => Ok(()), + Some("DefaultScheme") => Ok(()), + Some("SmimeScheme") => { + let signature_format = &scheme["SmimeScheme"]["SignatureFormat"]; + match signature_format.as_str() { + None if signature_format.is_null() => Ok(()), + Some("DETACHED") => Ok(()), + _ => Err(format!( + "SignatureFormat {signature_format} is not a valid value, expected DETACHED" + )), + } + } + Some(other) => Err(format!("{other} is not a member of SigningScheme")), + } +} + pub(crate) fn resolve_configuration_sets_action( method: &Method, segs: &[String], @@ -492,6 +551,8 @@ pub(crate) fn event_destination_to_json(dest: &EventDestination) -> Value { pub(crate) fn is_mutating_action(action: &str) -> bool { const MUTATING_PREFIXES: &[&str] = &[ + "Associate", + "Disassociate", "Create", "Update", "Delete", diff --git a/crates/fakecloud-ses/src/service/identities.rs b/crates/fakecloud-ses/src/service/identities.rs index 8705d1c48..d2cacde34 100644 --- a/crates/fakecloud-ses/src/service/identities.rs +++ b/crates/fakecloud-ses/src/service/identities.rs @@ -5,6 +5,7 @@ use serde_json::{json, Value}; use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError}; use crate::state::EmailIdentity; +use crate::state::IdentityCertificate; use crate::state::SesState; use super::SesV2Service; @@ -333,6 +334,9 @@ impl SesV2Service { // Remove policies for this identity state.identity_policies.remove(identity_name); + // Remove S/MIME certificate associations for this identity + state.identity_certificates.remove(identity_name); + Ok(AwsResponse::json(StatusCode::OK, "{}")) } @@ -732,4 +736,313 @@ impl SesV2Service { Ok(AwsResponse::json(StatusCode::OK, "{}")) } + + // --- S/MIME certificate associations --- + + pub(super) fn associate_email_identity_certificate( + &self, + req: &AwsRequest, + ) -> Result { + let body: Value = Self::parse_body(req)?; + let identity_name = match required_body_string(&body, "EmailIdentity") { + Ok(name) => name, + Err(resp) => return Ok(*resp), + }; + let certificate_arn = match required_body_string(&body, "CertificateArn") { + Ok(arn) => arn, + Err(resp) => return Ok(*resp), + }; + if let Err(msg) = validate_certificate_arn(&certificate_arn) { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &msg, + )); + } + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&req.account_id); + + let identity = match state.identities.get(&identity_name) { + Some(id) => id, + None => { + return Ok(Self::json_error( + StatusCode::NOT_FOUND, + "NotFoundException", + &format!("Identity {} does not exist", identity_name), + )); + } + }; + let from_address = match certificate_from_address(identity, body["FromAddress"].as_str()) { + Ok(addr) => addr, + Err(msg) => { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &msg, + )); + } + }; + + let certificates = state + .identity_certificates + .entry(identity_name.clone()) + .or_default(); + // One association per from-address. Real SES rejects a second + // association unless the existing one is on its way out + // (DEPROVISIONING), in which case the new one replaces it. + if let Some(existing) = certificates + .iter_mut() + .find(|c| c.from_address.eq_ignore_ascii_case(&from_address)) + { + if existing.status != "DEPROVISIONING" { + return Ok(Self::json_error( + StatusCode::CONFLICT, + "AlreadyExistsException", + &format!("A certificate is already associated with {from_address}"), + )); + } + existing.certificate_arn = certificate_arn; + existing.status = "PROVISIONING".to_string(); + existing.associated_at = Utc::now(); + } else { + certificates.push(IdentityCertificate { + from_address, + status: "PROVISIONING".to_string(), + certificate_arn, + associated_at: Utc::now(), + }); + certificates.sort_by(|a, b| a.from_address.cmp(&b.from_address)); + } + + Ok(AwsResponse::json(StatusCode::OK, "{}")) + } + + pub(super) fn disassociate_email_identity_certificate( + &self, + req: &AwsRequest, + ) -> Result { + let body: Value = Self::parse_body(req)?; + let identity_name = match required_body_string(&body, "EmailIdentity") { + Ok(name) => name, + Err(resp) => return Ok(*resp), + }; + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&req.account_id); + + let identity = match state.identities.get(&identity_name) { + Some(id) => id, + None => { + return Ok(Self::json_error( + StatusCode::NOT_FOUND, + "NotFoundException", + &format!("Identity {} does not exist", identity_name), + )); + } + }; + let from_address = match certificate_from_address(identity, body["FromAddress"].as_str()) { + Ok(addr) => addr, + Err(msg) => { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &msg, + )); + } + }; + + // Idempotent: an identity that exists but carries no matching + // association succeeds without changing anything. NotFoundException + // is reserved for an unknown identity (handled above). + let mut drained = false; + if let Some(certificates) = state.identity_certificates.get_mut(&identity_name) { + certificates.retain(|c| !c.from_address.eq_ignore_ascii_case(&from_address)); + drained = certificates.is_empty(); + } + if drained { + state.identity_certificates.remove(&identity_name); + } + + Ok(AwsResponse::json(StatusCode::OK, "{}")) + } + + pub(super) fn list_email_identity_certificates( + &self, + req: &AwsRequest, + ) -> Result { + let body: Value = Self::parse_body(req)?; + let identity_name = match required_body_string(&body, "EmailIdentity") { + Ok(name) => name, + Err(resp) => return Ok(*resp), + }; + // NextToken / PageSize travel in the body here (the op is a POST + // with no httpQuery bindings), unlike the GET-style listings. + let page_size = match body.get("PageSize") { + None | Some(Value::Null) => 20usize, + Some(v) => match v.as_i64() { + Some(n) if n >= 1 => n as usize, + _ => { + return Ok(Self::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + "PageSize must be a positive integer", + )); + } + }, + }; + let next_token = body["NextToken"].as_str().map(|s| s.to_string()); + + let mut accounts = self.state.write(); + let state = accounts.get_or_create(&req.account_id); + + if !state.identities.contains_key(&identity_name) { + return Ok(Self::json_error( + StatusCode::NOT_FOUND, + "NotFoundException", + &format!("Identity {} does not exist", identity_name), + )); + } + + let mut page: Vec = Vec::new(); + let mut next_marker: Option = None; + if let Some(certificates) = state.identity_certificates.get_mut(&identity_name) { + // Auto-advance PROVISIONING -> ACTIVE on the next read, matching + // real SES once the certificate finishes provisioning (the same + // convention `mail_from_domain_status` uses). + for certificate in certificates.iter_mut() { + if certificate.status == "PROVISIONING" { + certificate.status = "ACTIVE".to_string(); + } + } + certificates.sort_by(|a, b| a.from_address.cmp(&b.from_address)); + + // The token is the from-address of the first item on the next + // page (an inclusive cursor), so a disassociation between pages + // still advances the listing instead of restarting it. + let start_idx = match next_token { + Some(ref token) => certificates + .iter() + .position(|c| c.from_address.as_str() >= token.as_str()) + .unwrap_or(certificates.len()), + None => 0, + }; + + page = certificates + .iter() + .skip(start_idx) + .take(page_size) + .map(|c| { + // CertificateExpiryTime is sourced from the ACM + // certificate on real SES. fakecloud's SES holds no + // handle on the ACM service, so the field is omitted + // rather than invented. + json!({ + "FromAddress": c.from_address, + "Status": c.status, + "CertificateArn": c.certificate_arn, + }) + }) + .collect(); + next_marker = certificates + .get(start_idx.saturating_add(page_size)) + .map(|c| c.from_address.clone()); + } + + let mut response = json!({ "Certificates": page }); + if let Some(next) = next_marker { + response["NextToken"] = json!(next); + } + + Ok(AwsResponse::json(StatusCode::OK, response.to_string())) + } +} + +/// Read a required string member out of a REST-JSON body, or build the +/// BadRequestException real SES answers with when it is missing or empty. +/// The error is boxed: `AwsResponse` is large enough that returning it inline +/// trips `clippy::result_large_err` at every call site. +fn required_body_string(body: &Value, field: &str) -> Result> { + match body[field].as_str() { + Some(value) if !value.is_empty() => Ok(value.to_string()), + Some(_) => Err(Box::new(SesV2Service::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &format!("{field} must not be empty"), + ))), + None => Err(Box::new(SesV2Service::json_error( + StatusCode::BAD_REQUEST, + "BadRequestException", + &format!("{field} is required"), + ))), + } +} + +/// Validate a `CertificateArn` against the Smithy constraints: 20..=2048 +/// characters shaped `arn:::::certificate/`. +pub(crate) fn validate_certificate_arn(arn: &str) -> Result<(), String> { + if !(20..=2048).contains(&arn.len()) { + return Err("CertificateArn length must be between 20 and 2048".to_string()); + } + let parts: Vec<&str> = arn.splitn(6, ':').collect(); + let well_formed = parts.len() == 6 + && parts[0] == "arn" + && !parts[1].is_empty() + && !parts[2].is_empty() + && !parts[4].is_empty() + && parts[4].chars().all(|c| c.is_ascii_digit()) + && parts[5] + .strip_prefix("certificate/") + .is_some_and(|id| !id.is_empty()); + if !well_formed { + return Err(format!( + "CertificateArn {arn} is not a valid certificate ARN" + )); + } + Ok(()) +} + +/// Resolve the from-address a certificate association applies to. On a +/// domain identity `FromAddress` is required and must live in that domain +/// (or a subdomain); on an email-address identity it is optional and must +/// match the identity exactly when supplied. +pub(crate) fn certificate_from_address( + identity: &EmailIdentity, + from_address: Option<&str>, +) -> Result { + if identity.identity_type == "EMAIL_ADDRESS" { + return match from_address { + None => Ok(identity.identity_name.clone()), + Some(addr) if addr.eq_ignore_ascii_case(&identity.identity_name) => { + Ok(addr.to_string()) + } + Some(addr) => Err(format!( + "FromAddress {addr} does not match email identity {}", + identity.identity_name + )), + }; + } + + let addr = from_address.ok_or_else(|| { + format!( + "FromAddress is required for domain identity {}", + identity.identity_name + ) + })?; + let domain = match addr.rsplit_once('@') { + Some((local, domain)) if !local.is_empty() && !domain.is_empty() => domain, + _ => { + return Err(format!("FromAddress {addr} is not a valid email address")); + } + }; + let identity_domain = identity.identity_name.to_ascii_lowercase(); + let domain = domain.to_ascii_lowercase(); + if domain == identity_domain || domain.ends_with(&format!(".{identity_domain}")) { + Ok(addr.to_string()) + } else { + Err(format!( + "FromAddress {addr} does not belong to domain identity {}", + identity.identity_name + )) + } } diff --git a/crates/fakecloud-ses/src/service/mod.rs b/crates/fakecloud-ses/src/service/mod.rs index 5a59a2ee7..a1b753549 100644 --- a/crates/fakecloud-ses/src/service/mod.rs +++ b/crates/fakecloud-ses/src/service/mod.rs @@ -91,6 +91,10 @@ impl SesV2Service { /// GET /v2/email/configuration-sets -> ListConfigurationSets /// GET /v2/email/configuration-sets/{name} -> GetConfigurationSet /// DELETE /v2/email/configuration-sets/{name} -> DeleteConfigurationSet + /// POST /v2/email/update-configuration-sets -> UpdateConfigurationSet + /// POST /v2/email/identity/certificates -> AssociateEmailIdentityCertificate + /// POST /v2/email/identity/certificates/delete -> DisassociateEmailIdentityCertificate + /// POST /v2/email/identity/certificates/list -> ListEmailIdentityCertificates /// POST /v2/email/templates -> CreateEmailTemplate /// GET /v2/email/templates -> ListEmailTemplates /// GET /v2/email/templates/{name} -> GetEmailTemplate @@ -212,6 +216,10 @@ impl SesV2Service { "account" => resolve_account_action(method, segs), "identities" => resolve_identities_action(method, segs, resource), "configuration-sets" => resolve_configuration_sets_action(method, segs, resource), + "update-configuration-sets" if segs.len() == 3 && *method == Method::POST => { + Some(("UpdateConfigurationSet", None, None)) + } + "identity" => resolve_identity_certificates_action(method, segs), "templates" => resolve_templates_action(method, segs, resource), "contact-lists" => resolve_contact_lists_action(method, segs, resource), "suppression" => resolve_suppression_action(method, segs), @@ -412,7 +420,13 @@ impl fakecloud_core::service::AwsService for SesV2Service { "CreateConfigurationSet" => self.create_configuration_set(&req), "ListConfigurationSets" => self.list_configuration_sets(&req), "GetConfigurationSet" => self.get_configuration_set(res, &req), + "UpdateConfigurationSet" => self.update_configuration_set(&req), "DeleteConfigurationSet" => self.delete_configuration_set(res, &req), + "AssociateEmailIdentityCertificate" => self.associate_email_identity_certificate(&req), + "DisassociateEmailIdentityCertificate" => { + self.disassociate_email_identity_certificate(&req) + } + "ListEmailIdentityCertificates" => self.list_email_identity_certificates(&req), "CreateEmailTemplate" => self.create_email_template(&req), "ListEmailTemplates" => self.list_email_templates(&req), "GetEmailTemplate" => self.get_email_template(res, &req), @@ -580,7 +594,11 @@ impl fakecloud_core::service::AwsService for SesV2Service { "CreateConfigurationSet", "ListConfigurationSets", "GetConfigurationSet", + "UpdateConfigurationSet", "DeleteConfigurationSet", + "AssociateEmailIdentityCertificate", + "DisassociateEmailIdentityCertificate", + "ListEmailIdentityCertificates", "CreateEmailTemplate", "ListEmailTemplates", "GetEmailTemplate", diff --git a/crates/fakecloud-ses/src/service/tests.rs b/crates/fakecloud-ses/src/service/tests.rs index 57fa1d906..e436c35ac 100644 --- a/crates/fakecloud-ses/src/service/tests.rs +++ b/crates/fakecloud-ses/src/service/tests.rs @@ -712,6 +712,7 @@ async fn test_send_email_rejects_when_config_set_paused() { reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); @@ -4525,6 +4526,7 @@ async fn send_email_v2_rejects_when_config_set_sending_paused() { reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); @@ -4800,3 +4802,472 @@ async fn test_create_email_identity_easy_dkim_key_length() { // Easy DKIM still auto-provisions a keypair. assert!(id.dkim_domain_signing_private_key.is_some()); } + +// --- S/MIME certificate associations --- + +const TEST_CERT_ARN: &str = "arn:aws:acm:us-east-1:123456789012:certificate/abc-123"; +const TEST_CERT_ARN_2: &str = "arn:aws:acm:us-east-1:123456789012:certificate/def-456"; + +async fn create_identity(svc: &SesV2Service, name: &str) { + let req = make_request( + Method::POST, + "/v2/email/identities", + &format!(r#"{{"EmailIdentity": "{name}"}}"#), + ); + let resp = svc.handle(req).await.unwrap(); + assert_eq!(resp.status, StatusCode::OK); +} + +async fn associate_certificate(svc: &SesV2Service, body: &str) -> AwsResponse { + svc.handle(make_request( + Method::POST, + "/v2/email/identity/certificates", + body, + )) + .await + .unwrap() +} + +async fn list_certificates(svc: &SesV2Service, body: &str) -> AwsResponse { + svc.handle(make_request( + Method::POST, + "/v2/email/identity/certificates/list", + body, + )) + .await + .unwrap() +} + +async fn disassociate_certificate(svc: &SesV2Service, body: &str) -> AwsResponse { + svc.handle(make_request( + Method::POST, + "/v2/email/identity/certificates/delete", + body, + )) + .await + .unwrap() +} + +#[tokio::test] +async fn test_identity_certificate_lifecycle() { + let state = make_state(); + let svc = SesV2Service::new(state.clone()); + create_identity(&svc, "smime@example.com").await; + + let resp = associate_certificate( + &svc, + &format!( + r#"{{"EmailIdentity": "smime@example.com", "CertificateArn": "{TEST_CERT_ARN}"}}"# + ), + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + + // The association is persisted against the identity, defaulting the + // from-address to the email identity itself, and starts PROVISIONING. + { + let accts = state.read(); + let s = accts.default_ref(); + let certs = s.identity_certificates.get("smime@example.com").unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!(certs[0].from_address, "smime@example.com"); + assert_eq!(certs[0].certificate_arn, TEST_CERT_ARN); + assert_eq!(certs[0].status, "PROVISIONING"); + } + + // List reports it and advances PROVISIONING -> ACTIVE. + let resp = list_certificates(&svc, r#"{"EmailIdentity": "smime@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::OK); + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(body["Certificates"].as_array().unwrap().len(), 1); + assert_eq!(body["Certificates"][0]["FromAddress"], "smime@example.com"); + assert_eq!(body["Certificates"][0]["CertificateArn"], TEST_CERT_ARN); + assert_eq!(body["Certificates"][0]["Status"], "ACTIVE"); + assert!(body["NextToken"].is_null()); + + // Disassociate drops the stored association. + let resp = disassociate_certificate(&svc, r#"{"EmailIdentity": "smime@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::OK); + + let resp = list_certificates(&svc, r#"{"EmailIdentity": "smime@example.com"}"#).await; + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert!(body["Certificates"].as_array().unwrap().is_empty()); + { + let accts = state.read(); + let s = accts.default_ref(); + assert!(!s.identity_certificates.contains_key("smime@example.com")); + } +} + +#[tokio::test] +async fn test_associate_certificate_duplicate_conflicts() { + let state = make_state(); + let svc = SesV2Service::new(state.clone()); + create_identity(&svc, "dup@example.com").await; + + let body = + format!(r#"{{"EmailIdentity": "dup@example.com", "CertificateArn": "{TEST_CERT_ARN}"}}"#); + assert_eq!( + associate_certificate(&svc, &body).await.status, + StatusCode::OK + ); + let resp = associate_certificate(&svc, &body).await; + assert_eq!(resp.status, StatusCode::CONFLICT); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "AlreadyExistsException"); + + // A DEPROVISIONING association is replaced rather than rejected. + { + let mut accts = state.write(); + let s = accts.get_or_create("123456789012"); + s.identity_certificates.get_mut("dup@example.com").unwrap()[0].status = + "DEPROVISIONING".to_string(); + } + let replacement = + format!(r#"{{"EmailIdentity": "dup@example.com", "CertificateArn": "{TEST_CERT_ARN_2}"}}"#); + assert_eq!( + associate_certificate(&svc, &replacement).await.status, + StatusCode::OK + ); + let accts = state.read(); + let s = accts.default_ref(); + let certs = s.identity_certificates.get("dup@example.com").unwrap(); + assert_eq!(certs.len(), 1); + assert_eq!(certs[0].certificate_arn, TEST_CERT_ARN_2); + assert_eq!(certs[0].status, "PROVISIONING"); +} + +#[tokio::test] +async fn test_associate_certificate_unknown_identity_is_404() { + let state = make_state(); + let svc = SesV2Service::new(state); + + let resp = associate_certificate( + &svc, + &format!(r#"{{"EmailIdentity": "nope@example.com", "CertificateArn": "{TEST_CERT_ARN}"}}"#), + ) + .await; + assert_eq!(resp.status, StatusCode::NOT_FOUND); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "NotFoundException"); +} + +#[tokio::test] +async fn test_associate_certificate_validates_input() { + let state = make_state(); + let svc = SesV2Service::new(state); + create_identity(&svc, "val@example.com").await; + create_identity(&svc, "example.org").await; + + // Missing CertificateArn. + let resp = associate_certificate(&svc, r#"{"EmailIdentity": "val@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "BadRequestException"); + + // Missing EmailIdentity. + let resp = + associate_certificate(&svc, &format!(r#"{{"CertificateArn": "{TEST_CERT_ARN}"}}"#)).await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // CertificateArn that isn't an ACM certificate ARN. + let resp = associate_certificate( + &svc, + r#"{"EmailIdentity": "val@example.com", "CertificateArn": "arn:aws:acm:us-east-1:123456789012:key/abc"}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // FromAddress that doesn't match an email-address identity. + let resp = associate_certificate( + &svc, + &format!( + r#"{{"EmailIdentity": "val@example.com", "FromAddress": "other@example.com", "CertificateArn": "{TEST_CERT_ARN}"}}"# + ), + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // Domain identity without a FromAddress. + let resp = associate_certificate( + &svc, + &format!(r#"{{"EmailIdentity": "example.org", "CertificateArn": "{TEST_CERT_ARN}"}}"#), + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // Domain identity with a FromAddress in another domain. + let resp = associate_certificate( + &svc, + &format!( + r#"{{"EmailIdentity": "example.org", "FromAddress": "a@elsewhere.net", "CertificateArn": "{TEST_CERT_ARN}"}}"# + ), + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_associate_certificate_accepts_subdomain_from_address() { + let state = make_state(); + let svc = SesV2Service::new(state.clone()); + create_identity(&svc, "example.org").await; + + let resp = associate_certificate( + &svc, + &format!( + r#"{{"EmailIdentity": "example.org", "FromAddress": "sales@mail.example.org", "CertificateArn": "{TEST_CERT_ARN}"}}"# + ), + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + + let accts = state.read(); + let s = accts.default_ref(); + let certs = s.identity_certificates.get("example.org").unwrap(); + assert_eq!(certs[0].from_address, "sales@mail.example.org"); +} + +#[tokio::test] +async fn test_disassociate_certificate_is_idempotent_but_404s_unknown_identity() { + let state = make_state(); + let svc = SesV2Service::new(state); + create_identity(&svc, "idem@example.com").await; + + // No association yet: AWS documents this as a no-op success. + let resp = disassociate_certificate(&svc, r#"{"EmailIdentity": "idem@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::OK); + + // Unknown identity is the only NotFoundException case. + let resp = disassociate_certificate(&svc, r#"{"EmailIdentity": "ghost@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::NOT_FOUND); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "NotFoundException"); + + // Missing EmailIdentity is a BadRequestException. + let resp = disassociate_certificate(&svc, "{}").await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_list_certificates_paginates() { + let state = make_state(); + let svc = SesV2Service::new(state); + create_identity(&svc, "example.org").await; + + for local in ["a", "b", "c"] { + let resp = associate_certificate( + &svc, + &format!( + r#"{{"EmailIdentity": "example.org", "FromAddress": "{local}@example.org", "CertificateArn": "{TEST_CERT_ARN}"}}"# + ), + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + } + + let resp = list_certificates(&svc, r#"{"EmailIdentity": "example.org", "PageSize": 2}"#).await; + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(body["Certificates"].as_array().unwrap().len(), 2); + assert_eq!(body["Certificates"][0]["FromAddress"], "a@example.org"); + assert_eq!(body["NextToken"], "c@example.org"); + + let resp = list_certificates( + &svc, + r#"{"EmailIdentity": "example.org", "PageSize": 2, "NextToken": "c@example.org"}"#, + ) + .await; + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(body["Certificates"].as_array().unwrap().len(), 1); + assert_eq!(body["Certificates"][0]["FromAddress"], "c@example.org"); + assert!(body["NextToken"].is_null()); +} + +#[tokio::test] +async fn test_list_certificates_errors() { + let state = make_state(); + let svc = SesV2Service::new(state); + create_identity(&svc, "list@example.com").await; + + // Unknown identity. + let resp = list_certificates(&svc, r#"{"EmailIdentity": "ghost@example.com"}"#).await; + assert_eq!(resp.status, StatusCode::NOT_FOUND); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "NotFoundException"); + + // Non-positive PageSize. + let resp = list_certificates( + &svc, + r#"{"EmailIdentity": "list@example.com", "PageSize": 0}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // Missing EmailIdentity. + let resp = list_certificates(&svc, "{}").await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_delete_identity_drops_certificate_associations() { + let state = make_state(); + let svc = SesV2Service::new(state.clone()); + create_identity(&svc, "gone@example.com").await; + associate_certificate( + &svc, + &format!(r#"{{"EmailIdentity": "gone@example.com", "CertificateArn": "{TEST_CERT_ARN}"}}"#), + ) + .await; + + let req = make_request(Method::DELETE, "/v2/email/identities/gone@example.com", ""); + assert_eq!(svc.handle(req).await.unwrap().status, StatusCode::OK); + + let accts = state.read(); + let s = accts.default_ref(); + assert!(!s.identity_certificates.contains_key("gone@example.com")); +} + +// --- UpdateConfigurationSet --- + +async fn create_configuration_set(svc: &SesV2Service, body: &str) -> AwsResponse { + svc.handle(make_request( + Method::POST, + "/v2/email/configuration-sets", + body, + )) + .await + .unwrap() +} + +async fn update_configuration_set(svc: &SesV2Service, body: &str) -> AwsResponse { + svc.handle(make_request( + Method::POST, + "/v2/email/update-configuration-sets", + body, + )) + .await + .unwrap() +} + +#[tokio::test] +async fn test_update_configuration_set_message_security_options() { + let state = make_state(); + let svc = SesV2Service::new(state.clone()); + + let resp = create_configuration_set( + &svc, + r#"{"ConfigurationSetName": "cs-security", "ReputationOptions": {"ReputationMetricsEnabled": true}}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + + let resp = update_configuration_set( + &svc, + r#"{"ConfigurationSetName": "cs-security", + "MessageSecurityOptions": {"SigningScheme": {"SmimeScheme": {"SignatureFormat": "DETACHED"}}}}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + + // Stored on the configuration set... + { + let accts = state.read(); + let s = accts.default_ref(); + let cs = s.configuration_sets.get("cs-security").unwrap(); + assert_eq!( + cs.message_security_options.as_ref().unwrap()["SigningScheme"]["SmimeScheme"] + ["SignatureFormat"], + "DETACHED" + ); + // ... and the partial update left the other attributes alone. + assert!(cs.reputation_metrics_enabled); + } + + // ... and echoed by GetConfigurationSet. + let req = make_request(Method::GET, "/v2/email/configuration-sets/cs-security", ""); + let resp = svc.handle(req).await.unwrap(); + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!( + body["MessageSecurityOptions"]["SigningScheme"]["SmimeScheme"]["SignatureFormat"], + "DETACHED" + ); + assert_eq!(body["ReputationOptions"]["ReputationMetricsEnabled"], true); +} + +#[tokio::test] +async fn test_create_configuration_set_round_trips_message_security_options() { + let state = make_state(); + let svc = SesV2Service::new(state); + + let resp = create_configuration_set( + &svc, + r#"{"ConfigurationSetName": "cs-create-security", + "MessageSecurityOptions": {"SigningScheme": {"DefaultScheme": {}}}}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::OK); + + let req = make_request( + Method::GET, + "/v2/email/configuration-sets/cs-create-security", + "", + ); + let resp = svc.handle(req).await.unwrap(); + let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert!(body["MessageSecurityOptions"]["SigningScheme"]["DefaultScheme"].is_object()); +} + +#[tokio::test] +async fn test_update_configuration_set_errors() { + let state = make_state(); + let svc = SesV2Service::new(state); + create_configuration_set(&svc, r#"{"ConfigurationSetName": "cs-err"}"#).await; + + // Unknown configuration set. + let resp = update_configuration_set(&svc, r#"{"ConfigurationSetName": "cs-missing"}"#).await; + assert_eq!(resp.status, StatusCode::NOT_FOUND); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "NotFoundException"); + + // Missing name. + let resp = update_configuration_set(&svc, "{}").await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + + // SigningScheme is a union: two members set is invalid. + let resp = update_configuration_set( + &svc, + r#"{"ConfigurationSetName": "cs-err", + "MessageSecurityOptions": {"SigningScheme": {"DefaultScheme": {}, "SmimeScheme": {}}}}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "BadRequestException"); + + // DETACHED is the only modeled signature format. + let resp = update_configuration_set( + &svc, + r#"{"ConfigurationSetName": "cs-err", + "MessageSecurityOptions": {"SigningScheme": {"SmimeScheme": {"SignatureFormat": "ATTACHED"}}}}"#, + ) + .await; + assert_eq!(resp.status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_update_configuration_set_route_is_not_a_configuration_set_name() { + // `/v2/email/update-configuration-sets` is a fixed URI, so it must not + // be mistaken for a configuration set named "update-configuration-sets" + // nor fall through to the unknown-operation handler. + let state = make_state(); + let svc = SesV2Service::new(state); + let req = make_request( + Method::POST, + "/v2/email/update-configuration-sets", + r#"{"ConfigurationSetName": "nope"}"#, + ); + let resp = svc.handle(req).await.unwrap(); + assert_eq!(resp.status, StatusCode::NOT_FOUND); + let err: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap(); + assert_eq!(err["__type"], "NotFoundException"); +} diff --git a/crates/fakecloud-ses/src/state.rs b/crates/fakecloud-ses/src/state.rs index 8c5744aec..d2822677f 100644 --- a/crates/fakecloud-ses/src/state.rs +++ b/crates/fakecloud-ses/src/state.rs @@ -80,6 +80,13 @@ pub struct ConfigurationSet { pub vdm_options: Option, // Archiving options pub archive_arn: Option, + /// `MessageSecurityOptions` (the S/MIME `SigningScheme` union) as + /// supplied by CreateConfigurationSet or UpdateConfigurationSet. + /// `None` until configured; GetConfigurationSet only reports the + /// block once it has been set, the same way DeliveryOptions and + /// TrackingOptions are only echoed when non-default. + #[serde(default)] + pub message_security_options: Option, /// Tracks whether `ArchivingOptions` was set on the configuration set /// (via Create or PutConfigurationSetArchivingOptions). AWS surfaces /// the structure on GetConfigurationSet even when only `ArchiveArn` @@ -90,6 +97,24 @@ pub struct ConfigurationSet { pub archiving_options_present: bool, } +/// One S/MIME certificate association between an email identity and an +/// ACM certificate, created by `AssociateEmailIdentityCertificate`. +/// Real SES allows a single association per from-address. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdentityCertificate { + /// Address the certificate signs for. Always populated: for an + /// email-address identity it defaults to the identity itself, for a + /// domain identity the caller must name an address in that domain. + pub from_address: String, + /// PROVISIONING | ACTIVE | INACTIVE | DEPROVISIONING | FAILED. + /// Starts at PROVISIONING and advances to ACTIVE on the next read, + /// the same way `mail_from_domain_status` walks Pending -> Success. + pub status: String, + /// ARN of the ACM certificate, exactly as supplied by the caller. + pub certificate_arn: String, + pub associated_at: DateTime, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomVerificationEmailTemplate { pub template_name: String, @@ -488,6 +513,10 @@ pub struct SesState { pub event_destinations: BTreeMap>, /// Identity policies: identity name → policy name → policy JSON document. pub identity_policies: BTreeMap>, + /// S/MIME certificate associations: identity name -> associations, + /// kept sorted by from-address so pagination is stable. + #[serde(default)] + pub identity_certificates: BTreeMap>, /// Custom verification email templates: template name → template. pub custom_verification_email_templates: BTreeMap, /// Dedicated IP pools: pool name → pool. @@ -610,6 +639,7 @@ impl SesState { suppressed_destinations: BTreeMap::new(), event_destinations: BTreeMap::new(), identity_policies: BTreeMap::new(), + identity_certificates: BTreeMap::new(), custom_verification_email_templates: BTreeMap::new(), dedicated_ip_pools: BTreeMap::new(), dedicated_ips: BTreeMap::new(), @@ -769,6 +799,30 @@ mod tests { ); } + #[test] + fn identity_certificate_round_trips_through_state() { + let mut state = SesState::new("123456789012", "us-east-1"); + state.identity_certificates.insert( + "smime@example.com".to_string(), + vec![IdentityCertificate { + from_address: "smime@example.com".to_string(), + status: "PROVISIONING".to_string(), + certificate_arn: "arn:aws:acm:us-east-1:123456789012:certificate/abc-123" + .to_string(), + associated_at: Utc::now(), + }], + ); + let encoded = serde_json::to_string(&state).unwrap(); + let decoded: SesState = serde_json::from_str(&encoded).unwrap(); + let certs = decoded + .identity_certificates + .get("smime@example.com") + .unwrap(); + assert_eq!(certs[0].status, "PROVISIONING"); + state.reset(); + assert!(state.identity_certificates.is_empty()); + } + #[test] fn reset_preserves_account_region() { let mut state = SesState::new("123456789012", "eu-west-1"); diff --git a/crates/fakecloud-ses/src/v1_helpers.rs b/crates/fakecloud-ses/src/v1_helpers.rs index d258182c7..8e5b4569b 100644 --- a/crates/fakecloud-ses/src/v1_helpers.rs +++ b/crates/fakecloud-ses/src/v1_helpers.rs @@ -2068,6 +2068,7 @@ pub(crate) fn create_configuration_set( reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); diff --git a/crates/fakecloud-ses/src/v1_tests.rs b/crates/fakecloud-ses/src/v1_tests.rs index 02761925f..66df0a977 100644 --- a/crates/fakecloud-ses/src/v1_tests.rs +++ b/crates/fakecloud-ses/src/v1_tests.rs @@ -1966,6 +1966,7 @@ fn send_email_v1_config_set_pause() { reputation_metrics_enabled: false, vdm_options: None, archive_arn: None, + message_security_options: None, archiving_options_present: false, }, ); diff --git a/crates/fakecloud-support/Cargo.toml b/crates/fakecloud-support/Cargo.toml index 2ef19a490..3634b566e 100644 --- a/crates/fakecloud-support/Cargo.toml +++ b/crates/fakecloud-support/Cargo.toml @@ -11,7 +11,9 @@ version.workspace = true fakecloud-core = { workspace = true } fakecloud-persistence = { workspace = true } async-trait = { workspace = true } +base64 = { workspace = true } chrono = { workspace = true } +md-5 = { workspace = true } http = { workspace = true } parking_lot = { workspace = true } regex = { workspace = true } diff --git a/crates/fakecloud-support/model.json b/crates/fakecloud-support/model.json index 8b7210d21..d9b0acb8a 100644 --- a/crates/fakecloud-support/model.json +++ b/crates/fakecloud-support/model.json @@ -39,12 +39,18 @@ { "target": "com.amazonaws.support#AddCommunicationToCase" }, + { + "target": "com.amazonaws.support#CompleteAttachmentUpload" + }, { "target": "com.amazonaws.support#CreateCase" }, { "target": "com.amazonaws.support#DescribeAttachment" }, + { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatus" + }, { "target": "com.amazonaws.support#DescribeCases" }, @@ -75,6 +81,12 @@ { "target": "com.amazonaws.support#DescribeTrustedAdvisorCheckSummaries" }, + { + "target": "com.amazonaws.support#GetAttachmentDownloadLink" + }, + { + "target": "com.amazonaws.support#GetAttachmentUploadLinks" + }, { "target": "com.amazonaws.support#RefreshTrustedAdvisorCheck" }, @@ -94,7 +106,7 @@ "name": "support" }, "aws.protocols#awsJson1_1": {}, - "smithy.api#documentation": "Amazon Web Services Support\n

The Amazon Web Services Support API Reference is intended for programmers who need detailed\n information about the Amazon Web Services Support operations and data types. You can use the API to manage\n your support cases programmatically. The Amazon Web Services Support API uses HTTP methods that return\n results in JSON format.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

You can also use the Amazon Web Services Support API to access features for Trusted Advisor. You can return a list of\n checks and their descriptions, get check results, specify checks to refresh, and get the\n refresh status of checks.

\n

You can manage your support cases with the following Amazon Web Services Support API operations:

\n \n

You can also use the Amazon Web Services Support API to call the Trusted Advisor operations. For more\n information, see Trusted Advisor in the\n Amazon Web Services Support User Guide.

\n

For authentication of requests, Amazon Web Services Support uses Signature Version 4 Signing\n Process.

\n

For more information about this service and the endpoints to use, see About the\n Amazon Web Services Support API in the Amazon Web Services Support User Guide.

", + "smithy.api#documentation": "Amazon Web Services Support\n

The Amazon Web Services Support API Reference is intended for programmers who need detailed\n information about the Amazon Web Services Support operations and data types. You can use the API to manage\n your support cases programmatically. The Amazon Web Services Support API uses HTTP methods that return\n results in JSON format.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

You can also use the Amazon Web Services Support API to access features for Trusted Advisor. You can return a list of\n checks and their descriptions, get check results, specify checks to refresh, and get the\n refresh status of checks.

\n

You can manage your support cases with the following Amazon Web Services Support API operations:

\n \n

You can also use the Amazon Web Services Support API to call the Trusted Advisor operations. For more\n information, see Trusted Advisor in the\n Amazon Web Services Support User Guide.

\n

For authentication of requests, Amazon Web Services Support uses Signature Version 4 Signing\n Process.

\n

For more information about this service and the endpoints to use, see About the\n Amazon Web Services Support API in the Amazon Web Services Support User Guide.

", "smithy.api#title": "AWS Support", "smithy.api#xmlNamespace": { "uri": "http://support.amazonaws.com/doc/2013-04-15/" @@ -1546,12 +1558,15 @@ { "target": "com.amazonaws.support#AttachmentSetSizeLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Adds one or more attachments to an attachment set.

\n

An attachment set is a temporary container for attachments that you add to a case or\n case communication. The set is available for 1 hour after it's created. The\n expiryTime returned in the response is when the set expires.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Adds one or more attachments to an attachment set.

\n

An attachment set is a temporary container for attachments that you add to a case or\n case communication. The set is available for 1 hour after it's created. The\n expiryTime returned in the response is when the set expires.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#AddAttachmentsToSetRequest": { @@ -1569,6 +1584,12 @@ "smithy.api#documentation": "

One or more attachments to add to the set. You can add up to three attachments per\n set. The size limit is 5 MB per attachment.

\n

In the Attachment object, use the data parameter to specify\n the contents of the attachment file. In the previous request syntax, the value for\n data appear as blob, which is represented as a\n base64-encoded string. The value for fileName is the name of the\n attachment, such as troubleshoot-screenshot.png.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually adding the attachments. When set\n to true, the request is validated but no attachments are stored, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } } }, "traits": { @@ -1614,12 +1635,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Adds additional customer communication to an Amazon Web Services Support case. Use the caseId\n parameter to identify the case to which to add communication. You can list a set of\n email addresses to copy on the communication by using the ccEmailAddresses\n parameter. The communicationBody value contains the text of the\n communication.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Adds additional customer communication to a Amazon Web Services Support case. Use the caseId\n parameter to identify the case to which to add communication. To list a set of\n email addresses to copy on the communication, use the ccEmailAddresses\n parameter. The communicationBody value contains the text of the\n communication.

\n

To attach files larger than 5 MB to the communication, use the uploadIds parameter.

\n \n

Amazon Web Services Support automatically redacts sensitive information from support cases to protect your data. The following information is replaced with [REDACTED_BY_Amazon Web Services] and is not stored:

\n
    \n
  • \n

    Amazon Web Services secret keys - The complete key is replaced. Example: [REDACTED_BY_Amazon Web Services]\n

    \n
  • \n
  • \n

    Private keys - The complete key is replaced. Example: [REDACTED_BY_Amazon Web Services]\n

    \n
  • \n
  • \n

    Credit card numbers - The number is redacted, but the last 4 digits remain. Example: [REDACTED_BY_Amazon Web Services]-7016\n

    \n
  • \n
\n

This sensitive information is never required by Amazon Web Services Support.

\n
\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#AddCommunicationToCaseRequest": { @@ -1628,7 +1652,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "communicationBody": { @@ -1647,7 +1671,19 @@ "attachmentSetId": { "target": "com.amazonaws.support#AttachmentSetId", "traits": { - "smithy.api#documentation": "

The ID of a set of one or more attachments for the communication to add to the case.\n Create the set by calling AddAttachmentsToSet\n

" + "smithy.api#documentation": "

The ID of a set of one or more attachments for the communication to add to the case.\n Create the set by calling AddAttachmentsToSet. Each attachment in the\n set must be 5 MB or smaller. To attach files larger than 5 MB, use uploadIds.

" + } + }, + "uploadIds": { + "target": "com.amazonaws.support#UploadIds", + "traits": { + "smithy.api#documentation": "

A list of upload IDs that identify attachments to add to the case. Each\n uploadId is returned by the GetAttachmentUploadLinks\n operation. The upload must reach the attachment-ready state by calling CompleteAttachmentUpload before it can be passed here.\n Use\n uploadIds to attach files of any supported size, including files larger than\n 5 MB.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually adding the communication to the\n case. When set to true, the request is validated but the communication isn't\n added, and the operation returns a DryRunOperationException. When omitted or set\n to false, the request runs normally.

" } } }, @@ -1840,7 +1876,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "displayId": { @@ -1906,12 +1942,12 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } } }, "traits": { - "smithy.api#documentation": "

A JSON-formatted object that contains the metadata for a support case. It is contained\n in the response from a DescribeCases request. CaseDetails contains the following fields:

\n
    \n
  • \n

    \n caseId - The support case ID requested\n or returned in the call. The case ID is an alphanumeric string formatted as\n shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47.

    \n
  • \n
  • \n

    \n categoryCode - The category of problem\n for the support case. Corresponds to the CategoryCode values\n returned by a call to DescribeServices.

    \n
  • \n
  • \n

    \n displayId - The identifier for the case\n on pages in the Amazon Web Services Support Center.

    \n
  • \n
  • \n

    \n language - The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

    \n
  • \n
  • \n

    \n nextToken - A resumption point for\n pagination.

    \n
  • \n
  • \n

    \n recentCommunications - One or more Communication objects. Fields of these objects are\n attachments, body, caseId,\n submittedBy, and timeCreated.

    \n
  • \n
  • \n

    \n serviceCode - The identifier for the\n Amazon Web Services service that corresponds to the service code defined in the call to DescribeServices.

    \n
  • \n
  • \n

    \n severityCode - The severity code\n assigned to the case. Contains one of the values returned by the call to DescribeSeverityLevels. The possible values are:\n low, normal, high,\n urgent, and critical.

    \n
  • \n
  • \n

    \n status - The status of the case in the\n Amazon Web Services Support Center. Valid values:

    \n
      \n
    • \n

      \n all-open\n

      \n
    • \n
    • \n

      \n customer-action-completed\n

      \n
    • \n
    • \n

      \n opened\n

      \n
    • \n
    • \n

      \n pending-customer-action\n

      \n
    • \n
    • \n

      \n reopened\n

      \n
    • \n
    • \n

      \n resolved\n

      \n
    • \n
    • \n

      \n unassigned\n

      \n
    • \n
    • \n

      \n work-in-progress\n

      \n
    • \n
    \n
  • \n
  • \n

    \n subject - The subject line of the\n case.

    \n
  • \n
  • \n

    \n submittedBy - The email address of the\n account that submitted the case.

    \n
  • \n
  • \n

    \n timeCreated - The time the case was\n created, in ISO-8601 format.

    \n
  • \n
" + "smithy.api#documentation": "

A JSON-formatted object that contains the metadata for a support case. It is contained\n in the response from a DescribeCases request. CaseDetails contains the following fields:

\n
    \n
  • \n

    \n caseId - The support case ID requested\n or returned in the call. The case ID is an alphanumeric string formatted as\n shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47.

    \n
  • \n
  • \n

    \n categoryCode - The category of problem\n for the support case. Corresponds to the CategoryCode values\n returned by a call to DescribeServices.

    \n
  • \n
  • \n

    \n displayId - The identifier for the case\n on pages in the Amazon Web Services Support Center.

    \n
  • \n
  • \n

    \n language - The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

    \n
  • \n
  • \n

    \n nextToken - A resumption point for\n pagination.

    \n
  • \n
  • \n

    \n recentCommunications - One or more Communication objects. Fields of these objects are\n attachments, body, caseId,\n submittedBy, and timeCreated.

    \n
  • \n
  • \n

    \n serviceCode - The identifier for the\n Amazon Web Services service that corresponds to the service code defined in the call to DescribeServices.

    \n
  • \n
  • \n

    \n severityCode - The severity code\n assigned to the case. Contains one of the values returned by the call to DescribeSeverityLevels. The possible values are:\n low, normal, high,\n urgent, and critical.

    \n
  • \n
  • \n

    \n status - The status of the case in the\n Amazon Web Services Support Center. Valid values:

    \n
      \n
    • \n

      \n all-open\n

      \n
    • \n
    • \n

      \n customer-action-completed\n

      \n
    • \n
    • \n

      \n opened\n

      \n
    • \n
    • \n

      \n pending-customer-action\n

      \n
    • \n
    • \n

      \n reopened\n

      \n
    • \n
    • \n

      \n resolved\n

      \n
    • \n
    • \n

      \n unassigned\n

      \n
    • \n
    • \n

      \n work-in-progress\n

      \n
    • \n
    \n
  • \n
  • \n

    \n subject - The subject line of the\n case.

    \n
  • \n
  • \n

    \n submittedBy - The email address of the\n account that submitted the case.

    \n
  • \n
  • \n

    \n timeCreated - The time the case was\n created, in ISO-8601 format.

    \n
  • \n
" } }, "com.amazonaws.support#CaseId": { @@ -2009,7 +2045,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } }, "body": { @@ -2030,10 +2066,16 @@ "smithy.api#documentation": "

The time the communication was created.

" } }, + "attachments": { + "target": "com.amazonaws.support#AttachmentSet", + "traits": { + "smithy.api#documentation": "

Information about all attachments on the case communication. This includes attachments added through AddAttachmentsToSet and attachments uploaded through GetAttachmentUploadLinks.

\n

Use this field to enumerate every attachment on the communication. To download an attachment listed in this field, use GetAttachmentDownloadLink. GetAttachmentDownloadLink returns a presigned URL that works for attachments of any size.

" + } + }, "attachmentSet": { "target": "com.amazonaws.support#AttachmentSet", "traits": { - "smithy.api#documentation": "

Information about the attachments to the case communication.

" + "smithy.api#documentation": "

Information about the attachments to the case communication that are 5 MB or smaller.\n This field doesn't include attachments larger than 5 MB. To enumerate every attachment on\n the communication, including attachments larger than 5 MB, use the\n attachments field instead.

" } } }, @@ -2094,6 +2136,106 @@ } } }, + "com.amazonaws.support#CompleteAttachmentUpload": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#CompleteAttachmentUploadRequest" + }, + "output": { + "target": "com.amazonaws.support#CompleteAttachmentUploadResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Completes an attachment upload that was started with GetAttachmentUploadLinks. After you upload a part of the file to its\n presigned Amazon S3 URL, call CompleteAttachmentUpload with the\n partIndex and eTag of that part. You can include one part per\n call, or multiple parts in a single call. After CompleteAttachmentUpload has\n been called for every part of the file, the service processes the upload asynchronously. The\n attachment-ready status might not be reflected immediately. Use DescribeAttachmentUploadStatus to poll for the uploadStatus to\n become attachment-ready before passing the uploadId to CreateCase or AddCommunicationToCase.

" + } + }, + "com.amazonaws.support#CompleteAttachmentUploadRequest": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The identifier associated with the upload to complete.

", + "smithy.api#required": {} + } + }, + "completedUploads": { + "target": "com.amazonaws.support#CompletedUploadList", + "traits": { + "smithy.api#documentation": "

The list of parts being reported as completed in this call. Each entry must contain the partIndex of an uploaded part and the ETag returned by Amazon S3 when that part was uploaded.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually completing the upload. When set\n to true, the request is validated but the upload isn't finalized, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#CompleteAttachmentUploadResponse": { + "type": "structure", + "members": { + "uploadStatus": { + "target": "com.amazonaws.support#UploadStatus", + "traits": { + "smithy.api#documentation": "

The status of the multipart upload after the operation finalizes the\n attachment. Valid values: attachment-ready, attachment-not-ready,\n and failed.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#CompletedUpload": { + "type": "structure", + "members": { + "partIndex": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The index of the uploaded part. This is the same partIndex value returned for the corresponding entry in the uploadUrls field of the GetAttachmentUploadLinks response.

", + "smithy.api#required": {} + } + }, + "eTag": { + "target": "com.amazonaws.support#ETag", + "traits": { + "smithy.api#documentation": "

The ETag returned in the response headers when the part was uploaded to Amazon S3. The ETag value identifies the part contents.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

Identifies a single uploaded part of a multipart attachment upload. Pass a list of\n CompletedUpload objects to CompleteAttachmentUpload to\n finalize the upload.

" + } + }, + "com.amazonaws.support#CompletedUploadList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#CompletedUpload" + } + }, + "com.amazonaws.support#CoralAvailabilityThrottledResource": { + "type": "string" + }, + "com.amazonaws.support#CoralAvailabilityThrottlingReason": { + "type": "string" + }, "com.amazonaws.support#CreateCase": { "type": "operation", "input": { @@ -2112,12 +2254,15 @@ { "target": "com.amazonaws.support#CaseCreationLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Creates a case in the Amazon Web Services Support Center. This operation is similar to how you create a case\n in the Amazon Web Services Support Center Create\n Case page.

\n

The Amazon Web Services Support API doesn't support requesting service limit increases. You can submit a\n service limit increase in the following ways:

\n \n

A successful CreateCase request returns an Amazon Web Services Support case number. You can use\n the DescribeCases operation and specify the case number to get\n existing Amazon Web Services Support cases. After you create a case, use the AddCommunicationToCase operation to add additional communication or\n attachments to an existing case.

\n

The caseId is separate from the displayId that appears in\n the Amazon Web Services Support Center. Use the DescribeCases operation to get the displayId.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Creates a case in the Amazon Web Services Support Center. This operation is similar to how you create a case\n in the Amazon Web Services Support Center Create\n Case page.

\n

The Amazon Web Services Support API doesn't support requesting service limit increases. You can submit a\n service limit increase in the following ways:

\n \n \n

Amazon Web Services Support automatically redacts sensitive information from support cases to protect your data. The following information is replaced with [REDACTED_BY_Amazon Web Services] and is not stored:

\n
    \n
  • \n

    Amazon Web Services secret keys - The complete key is replaced. Example: [REDACTED_BY_Amazon Web Services]\n

    \n
  • \n
  • \n

    Private keys - The complete key is replaced. Example: [REDACTED_BY_Amazon Web Services]\n

    \n
  • \n
  • \n

    Credit card numbers - The number is redacted, but the last 4 digits remain. Example: [REDACTED_BY_Amazon Web Services]-7016\n

    \n
  • \n
\n

This sensitive information is never required by Amazon Web Services Support.

\n
\n

A successful CreateCase request returns a Amazon Web Services Support case number. You can use\n the DescribeCases operation and specify the case number to get\n existing Amazon Web Services Support cases. After you create a case, use the AddCommunicationToCase operation to add additional communication or\n attachments to an existing case.

\n

The caseId is separate from the displayId that appears in\n the Amazon Web Services Support Center. Use the DescribeCases operation to get the displayId.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#CreateCaseRequest": { @@ -2164,7 +2309,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } }, "issueType": { @@ -2176,7 +2321,19 @@ "attachmentSetId": { "target": "com.amazonaws.support#AttachmentSetId", "traits": { - "smithy.api#documentation": "

The ID of a set of one or more attachments for the case. Create the set by using the\n AddAttachmentsToSet operation.

" + "smithy.api#documentation": "

The ID of a set of one or more attachments for the case. Create the set by using the\n AddAttachmentsToSet operation. Each attachment in the set must be 5\n MB or smaller. To attach files larger than 5 MB, use uploadIds.

" + } + }, + "uploadIds": { + "target": "com.amazonaws.support#UploadIds", + "traits": { + "smithy.api#documentation": "

A list of upload IDs that identify attachments to add to the case. Each\n uploadId is returned by the GetAttachmentUploadLinks\n operation. The upload must reach the attachment-ready state by calling CompleteAttachmentUpload before it can be passed here.\n Use\n uploadIds to attach files of any supported size, including files larger than\n 5 MB.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually creating the case. When set to\n true, the request is validated but no case is created, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" } } }, @@ -2190,7 +2347,7 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string in the following format:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string in the following format:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" } } }, @@ -2243,12 +2400,15 @@ { "target": "com.amazonaws.support#DescribeAttachmentLimitExceeded" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the attachment that has the specified ID. Attachments can include screenshots,\n error logs, or other files that describe your issue. Attachment IDs are generated by the\n case management system when you add an attachment to a case or case communication.\n Attachment IDs are returned in the AttachmentDetails objects that are\n returned by the DescribeCommunications operation.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Returns the attachment that has the specified ID. Attachments can include screenshots,\n error logs, or other files that describe your issue. Attachment IDs are generated by the\n case management system when you add an attachment to a case or case communication.\n Attachment IDs are returned in the AttachmentDetails objects that are\n returned by the DescribeCommunications operation.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n \n

\n DescribeAttachment can't return attachments larger than 5 MB. If the\n specified attachmentId refers to an attachment larger than 5 MB, the\n request fails with InvalidParameterValueException.

\n

To download an attachment of any size, including attachments larger than 5 MB, use\n GetAttachmentDownloadLink.\n GetAttachmentDownloadLink returns an Amazon S3 presigned URL that you can\n use to download the attachment directly.

\n
" } }, "com.amazonaws.support#DescribeAttachmentLimitExceeded": { @@ -2272,9 +2432,15 @@ "attachmentId": { "target": "com.amazonaws.support#AttachmentId", "traits": { - "smithy.api#documentation": "

The ID of the attachment to return. Attachment IDs are returned by the DescribeCommunications operation.

", + "smithy.api#documentation": "

The ID of the attachment to return. Attachment IDs are returned by the DescribeCommunications operation.

\n

If the specified attachment is larger than 5 MB, this operation returns\n InvalidParameterValueException. To download attachments larger than 5\n MB, use GetAttachmentDownloadLink.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually retrieving the attachment. When\n set to true, the request is validated but no attachment content is returned, and\n the operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2296,6 +2462,78 @@ "smithy.api#output": {} } }, + "com.amazonaws.support#DescribeAttachmentUploadStatus": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatusRequest" + }, + "output": { + "target": "com.amazonaws.support#DescribeAttachmentUploadStatusResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Returns the current status, file name, and progress of a multipart attachment upload that\n was started with GetAttachmentUploadLinks. Use this operation to track\n where an upload is in the workflow. While parts are still being uploaded and reported through\n CompleteAttachmentUpload, the uploadStatus is\n attachment-not-ready and uploadProgress reports the total number\n of parts and how many have been completed so far. After every part has been reported and the\n service finishes processing the upload asynchronously, the uploadStatus becomes\n attachment-ready and the uploadId can be attached to a case\n through CreateCase or AddCommunicationToCase.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + } + }, + "com.amazonaws.support#DescribeAttachmentUploadStatusRequest": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier for the upload. The uploadId is returned by\n GetAttachmentUploadLinks when you initiate the upload.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning upload status. When\n set to true, the request is validated but no status is returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#DescribeAttachmentUploadStatusResponse": { + "type": "structure", + "members": { + "uploadStatus": { + "target": "com.amazonaws.support#UploadStatus", + "traits": { + "smithy.api#documentation": "

The current status of the multipart upload. Valid values: attachment-ready,\n attachment-not-ready, and failed.

", + "smithy.api#required": {} + } + }, + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the file being uploaded, including the file extension.

", + "smithy.api#required": {} + } + }, + "uploadProgress": { + "target": "com.amazonaws.support#UploadProgress", + "traits": { + "smithy.api#documentation": "

The progress of the multipart upload, including the total number of parts and the number\n of parts that have been successfully uploaded.

" + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, "com.amazonaws.support#DescribeCases": { "type": "operation", "input": { @@ -2308,12 +2546,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns a list of cases that you specify by passing one or more case IDs. You can use\n the afterTime and beforeTime parameters to filter the cases by\n date. You can set values for the includeResolvedCases and\n includeCommunications parameters to specify how much information to\n return.

\n

The response returns the following in JSON format:

\n
    \n
  • \n

    One or more CaseDetails data types.

    \n
  • \n
  • \n

    One or more nextToken values, which specify where to paginate the\n returned records represented by the CaseDetails objects.

    \n
  • \n
\n

Case data is available for 12 months after creation. If a case was created more than\n 12 months ago, a request might return an error.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
", + "smithy.api#documentation": "

Returns a list of cases that you specify by passing one or more case IDs. You can use\n the afterTime and beforeTime parameters to filter the cases by\n date. You can set values for the includeResolvedCases and\n includeCommunications parameters to specify how much information to\n return.

\n

The response returns the following in JSON format:

\n
    \n
  • \n

    One or more CaseDetails data types.

    \n
  • \n
  • \n

    One or more nextToken values, which specify where to paginate the\n returned records represented by the CaseDetails objects.

    \n
  • \n
\n

Case data is available for 24 months after creation. If a case was created more than\n 24 months ago, a request might return an error.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n \n

Each Communication returned by this operation includes\n attachment information in two fields:

\n
    \n
  • \n

    \n attachmentSet: returns only attachments that are 5 MB or\n smaller. Attachments larger than 5 MB are not included in this field.

    \n
  • \n
  • \n

    \n attachments: returns all attachments regardless of size.

    \n
  • \n
\n

Amazon Web Services recommends that you use the attachments field and download each\n attachment with GetAttachmentDownloadLink, which supports\n attachments of any size. The attachmentSet field and DescribeAttachment return only attachments that are 5 MB or\n smaller.

\n
", "smithy.api#paginated": { "inputToken": "nextToken", "outputToken": "nextToken", @@ -2340,13 +2581,13 @@ "afterTime": { "target": "com.amazonaws.support#AfterTime", "traits": { - "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "beforeTime": { "target": "com.amazonaws.support#BeforeTime", "traits": { - "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "includeResolvedCases": { @@ -2371,7 +2612,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" } }, "includeCommunications": { @@ -2379,6 +2620,12 @@ "traits": { "smithy.api#documentation": "

Specifies whether to include communications in the DescribeCases\n response. By default, communications are included.

" } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning case data. When set\n to true, the request is validated but no cases are returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } } }, "traits": { @@ -2418,12 +2665,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns communications and attachments for one or more support cases. Use the\n afterTime and beforeTime parameters to filter by date. You\n can use the caseId parameter to restrict the results to a specific\n case.

\n

Case data is available for 12 months after creation. If a case was created more than\n 12 months ago, a request for data might cause an error.

\n

You can use the maxResults and nextToken parameters to\n control the pagination of the results. Set maxResults to the number of\n cases that you want to display on each page, and use nextToken to specify\n the resumption of pagination.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
", + "smithy.api#documentation": "

Returns communications and attachments for one or more support cases. Use the\n afterTime and beforeTime parameters to filter by date. You\n can use the caseId parameter to restrict the results to a specific\n case.

\n

Case data is available for 24 months after creation. If a case was created more than\n 24 months ago, a request for data might cause an error.

\n

You can use the maxResults and nextToken parameters to\n control the pagination of the results. Set maxResults to the number of\n cases that you want to display on each page, and use nextToken to specify\n the resumption of pagination.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n \n

Each Communication returned by this operation includes\n attachment information in two fields:

\n
    \n
  • \n

    \n attachmentSet: returns only attachments that are 5 MB or\n smaller. Attachments larger than 5 MB are not included in this field.

    \n
  • \n
  • \n

    \n attachments: returns all attachments regardless of size.

    \n
  • \n
\n

Amazon Web Services recommends that you use the attachments field and download each\n attachment with GetAttachmentDownloadLink, which supports\n attachments of any size. The attachmentSet field and DescribeAttachment return only attachments that are 5 MB or\n smaller.

\n
", "smithy.api#paginated": { "inputToken": "nextToken", "outputToken": "nextToken", @@ -2438,20 +2688,20 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

", + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

", "smithy.api#required": {} } }, "beforeTime": { "target": "com.amazonaws.support#BeforeTime", "traits": { - "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The end date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "afterTime": { "target": "com.amazonaws.support#AfterTime", "traits": { - "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 12 months after creation.

" + "smithy.api#documentation": "

The start date for a filtered date search on support case communications. Case\n communications are available for 24 months after creation.

" } }, "nextToken": { @@ -2465,6 +2715,12 @@ "traits": { "smithy.api#documentation": "

The maximum number of results to return before paginating.

" } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning communications. When\n set to true, the request is validated but no communications are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2501,6 +2757,9 @@ "target": "com.amazonaws.support#DescribeCreateCaseOptionsResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" }, @@ -2509,7 +2768,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns a list of CreateCaseOption types along with the \n corresponding supported hours and language availability. You can specify the language\n categoryCode, \n issueType and serviceCode used to retrieve the CreateCaseOptions.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Returns a list of CreateCaseOption types along with the \n corresponding supported hours and language availability. You can specify the language\n categoryCode, \n issueType and serviceCode used to retrieve the CreateCaseOptions.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#DescribeCreateCaseOptionsRequest": { @@ -2532,7 +2791,7 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

", + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

", "smithy.api#required": {} } }, @@ -2542,6 +2801,12 @@ "smithy.api#documentation": "

The category of problem for the support case. You also use the DescribeServices operation to get the category code for a service. Each\n Amazon Web Services service defines its own set of category codes.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning case option data.\n When set to true, the request is validated but no options are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2577,12 +2842,15 @@ "target": "com.amazonaws.support#DescribeServicesResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the current list of Amazon Web Services services and a list of service categories for each\n service. You then use service names and categories in your CreateCase\n requests. Each Amazon Web Services service has its own set of categories.

\n

The service codes and category codes correspond to the values that appear in the\n Service and Category lists on the Amazon Web Services Support Center Create Case page. The values in those fields\n don't necessarily match the service codes and categories returned by the\n DescribeServices operation. Always use the service codes and categories\n that the DescribeServices operation returns, so that you have the most\n recent set of service and category codes.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Returns the current list of Amazon Web Services services and a list of service categories for each\n service. You then use service names and categories in your CreateCase\n requests. Each Amazon Web Services service has its own set of categories.

\n

The service codes and category codes correspond to the values that appear in the\n Service and Category lists on the Amazon Web Services Support Center Create Case page. The values in those fields\n don't necessarily match the service codes and categories returned by the\n DescribeServices operation. Always use the service codes and categories\n that the DescribeServices operation returns, so that you have the most\n recent set of service and category codes.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#DescribeServicesRequest": { @@ -2597,7 +2865,13 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning the list of services.\n When set to true, the request is validated but no services are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" } } }, @@ -2629,12 +2903,15 @@ "target": "com.amazonaws.support#DescribeSeverityLevelsResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Returns the list of severity levels that you can assign to a support case. The\n severity level for a case is also a field in the CaseDetails data type\n that you include for a CreateCase request.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Returns the list of severity levels that you can assign to a support case. The\n severity level for a case is also a field in the CaseDetails data type\n that you include for a CreateCase request.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#DescribeSeverityLevelsRequest": { @@ -2643,7 +2920,13 @@ "language": { "target": "com.amazonaws.support#Language", "traits": { - "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") and Korean (\u201cko\u201d). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + "smithy.api#documentation": "

The language in which Amazon Web Services Support handles the case. Amazon Web Services Support\ncurrently supports Chinese (\u201czh\u201d), English (\"en\"), Japanese (\"ja\") , Chinese (\"zh\"), Spanish (\"es\"), Portuguese (\"pt\"), French (\"fr\"), Korean (\u201cko\u201d), and Turkish (\"tr\"). You must specify the ISO 639-1\ncode for the language parameter if you want support in that language.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning severity levels. When\n set to true, the request is validated but no severity levels are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" } } }, @@ -2675,6 +2958,9 @@ "target": "com.amazonaws.support#DescribeSupportedLanguagesResponse" }, "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" }, @@ -2683,7 +2969,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns a list of supported languages for a specified categoryCode, \n issueType and serviceCode. The returned supported languages will \n include a ISO 639-1 code for the language, and the language display name.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Returns a list of supported languages for a specified categoryCode, \n issueType and serviceCode. The returned supported languages will \n include a ISO 639-1 code for the language, and the language display name.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#DescribeSupportedLanguagesRequest": { @@ -2709,6 +2995,12 @@ "smithy.api#documentation": "

The category of problem for the support case. You also use the DescribeServices operation to get the category code for a service. Each\n Amazon Web Services service defines its own set of category codes.

", "smithy.api#required": {} } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning supported languages.\n When set to true, the request is validated but no languages are returned, and the\n operation returns a DryRunOperationException. When omitted or set to\n false, the request runs normally.

" + } } }, "traits": { @@ -2746,7 +3038,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the refresh status of the Trusted Advisor checks that have the specified check\n IDs. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

Some checks are refreshed automatically, and you can't return their refresh statuses\n by using the DescribeTrustedAdvisorCheckRefreshStatuses operation. If you\n call this operation for these checks, you might see an\n InvalidParameterValue error.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the refresh status of the Trusted Advisor checks that have the specified check\n IDs. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

Some checks are refreshed automatically, and you can't return their refresh statuses\n by using the DescribeTrustedAdvisorCheckRefreshStatuses operation. If you\n call this operation for these checks, you might see an\n InvalidParameterValue error.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckRefreshStatusesRequest": { @@ -2797,7 +3089,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the results of the Trusted Advisor check that has the specified check ID. You\n can get the check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

The response contains a TrustedAdvisorCheckResult object, which\n contains these three objects:

\n \n

In addition, the response contains these fields:

\n
    \n
  • \n

    \n status - The alert status of the check\n can be ok (green), warning (yellow),\n error (red), or not_available.

    \n
  • \n
  • \n

    \n timestamp - The time of the last refresh\n of the check.

    \n
  • \n
  • \n

    \n checkId - The unique identifier for the\n check.

    \n
  • \n
\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the results of the Trusted Advisor check that has the specified check ID. You\n can get the check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

The response contains a TrustedAdvisorCheckResult object, which\n contains these three objects:

\n \n

In addition, the response contains these fields:

\n
    \n
  • \n

    \n status - The alert status of the check\n can be ok (green), warning (yellow),\n error (red), or not_available.

    \n
  • \n
  • \n

    \n timestamp - The time of the last refresh\n of the check.

    \n
  • \n
  • \n

    \n checkId - The unique identifier for the\n check.

    \n
  • \n
\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckResultRequest": { @@ -2854,7 +3146,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns the results for the Trusted Advisor check summaries for the check IDs that you\n specified. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

The response contains an array of TrustedAdvisorCheckSummary\n objects.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns the results for the Trusted Advisor check summaries for the check IDs that you\n specified. You can get the check IDs by calling the DescribeTrustedAdvisorChecks operation.

\n

The response contains an array of TrustedAdvisorCheckSummary\n objects.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

\n

\n Understanding the Trusted Advisor Resources processed value\n

\n

The Resources processed value, resourcesProcessed, usually shows both flagged resources (those with warnings or errors) and resources in good standing (ok status resources). However, some checks report flagged resources only. To understand what a specific check reports, review the detailed check information in the Trusted Advisor check reference. If you see a Green criterion listed in the Alert criteria, then the check reports all resources. If there's no Green criterion listed in the Alert criteria, then the check reports only flagged resources. For example, the Amazon EC2 Reserved Instance optimization check (cX3c2R1chu) doesn't list a Green criterion in the Alert criteria. So, this check only reports flagged resources.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorCheckSummariesRequest": { @@ -2905,7 +3197,7 @@ } ], "traits": { - "smithy.api#documentation": "

Returns information about all available Trusted Advisor checks, including the name, ID,\n category, description, and metadata. You must specify a language code.

\n

The response contains a TrustedAdvisorCheckDescription object for\n each check. You must set the Amazon Web Services Region to us-east-1.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the SubscriptionRequiredException error\n message appears. For information about changing your support plan, see\n Amazon Web Services Support.

    \n
  • \n
  • \n

    The names and descriptions for Trusted Advisor checks are subject to change. We\n recommend that you specify the check ID in your code to uniquely identify a\n check.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Returns information about all available Trusted Advisor checks, including the name, ID,\n category, description, and metadata. You must specify a language code.

\n

The response contains a TrustedAdvisorCheckDescription object for\n each check. You must set the Amazon Web Services Region to us-east-1.

\n \n
    \n
  • \n

    You must have a Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the SubscriptionRequiredException error\n message appears. For information about changing your support plan, see\n Amazon Web Services Support.

    \n
  • \n
  • \n

    The names and descriptions for Trusted Advisor checks are subject to change. We\n recommend that you specify the check ID in your code to uniquely identify a\n check.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#DescribeTrustedAdvisorChecksRequest": { @@ -2951,6 +3243,57 @@ "smithy.api#default": 0 } }, + "com.amazonaws.support#DownloadUrl": { + "type": "structure", + "members": { + "url": { + "target": "com.amazonaws.support#HttpsUrl", + "traits": { + "smithy.api#documentation": "

The presigned HTTPS URL that you can use to download the attachment. Download URLs are\n served from downloadv1.attachments.support.{region}.amazonaws.com. The\n downloadv1 prefix is subject to change.

", + "smithy.api#required": {} + } + }, + "expiryDate": { + "target": "com.amazonaws.support#ValidatedDateTime", + "traits": { + "smithy.api#documentation": "

The date and time, in ISO-8601 format, when the presigned URL expires. Download the\n attachment before this time.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A presigned URL for downloading an attachment, along with the date and time the URL\n expires. Returned by GetAttachmentDownloadLink.

" + } + }, + "com.amazonaws.support#DryRunOperationException": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.support#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The request was valid, but the operation wasn't performed because dryRun was\n set to true.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.support#ETag": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 256 + } + } + }, + "com.amazonaws.support#EndIndex": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 2 + } + } + }, "com.amazonaws.support#EndTime": { "type": "string" }, @@ -2960,9 +3303,202 @@ "com.amazonaws.support#ExpiryTime": { "type": "string" }, + "com.amazonaws.support#FieldIntegerValue": { + "type": "integer" + }, "com.amazonaws.support#FileName": { "type": "string" }, + "com.amazonaws.support#FileSize": { + "type": "long", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 157286400 + } + } + }, + "com.amazonaws.support#GetAttachmentDownloadLink": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#GetAttachmentDownloadLinkRequest" + }, + "output": { + "target": "com.amazonaws.support#GetAttachmentDownloadLinkResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#AttachmentIdNotFound" + }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + } + ], + "traits": { + "smithy.api#documentation": "

Returns a presigned download URL for an attachment that is associated with a case\n communication. The download link works for an attachment of any size, including attachments\n added through AddAttachmentsToSet and attachments uploaded through GetAttachmentUploadLinks. The download URL is time-limited and expires at the\n date and time indicated in the downloadUrl response field. Download the\n attachment from the URL before it expires.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + } + }, + "com.amazonaws.support#GetAttachmentDownloadLinkRequest": { + "type": "structure", + "members": { + "attachmentId": { + "target": "com.amazonaws.support#AttachmentId", + "traits": { + "smithy.api#documentation": "

The unique identifier of the attachment for which to retrieve a download link. Attachment\n IDs are returned in the AttachmentDetails objects in the attachments\n field of a Communication returned by DescribeCommunications\n or DescribeCases.

", + "smithy.api#required": {} + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually returning a download link. When\n set to true, the request is validated but no URL is returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#GetAttachmentDownloadLinkResponse": { + "type": "structure", + "members": { + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the attachment file, including the file extension.

", + "smithy.api#required": {} + } + }, + "downloadUrl": { + "target": "com.amazonaws.support#DownloadUrl", + "traits": { + "smithy.api#documentation": "

The presigned download URL and the date and time the URL expires.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#GetAttachmentUploadLinks": { + "type": "operation", + "input": { + "target": "com.amazonaws.support#GetAttachmentUploadLinksRequest" + }, + "output": { + "target": "com.amazonaws.support#GetAttachmentUploadLinksResponse" + }, + "errors": [ + { + "target": "com.amazonaws.support#DryRunOperationException" + }, + { + "target": "com.amazonaws.support#InternalServerError" + }, + { + "target": "com.amazonaws.support#UploadIdNotFound" + } + ], + "traits": { + "smithy.api#documentation": "

Returns one or more presigned upload URLs for uploading a large file attachment to a\n support case by using a multipart upload workflow. The maximum file size that you can upload\n with this workflow is 150 MB, and parts can be up to 100 MB each. Initiate a new upload by\n providing fileName and fileSizeBytes; the response returns a unique\n uploadId, the part size, the total number of parts, and a list of presigned\n upload URLs for the requested range of parts. A maximum of 10 upload URLs are returned per\n call. To retrieve more upload URLs for an upload\n that's already in progress, call GetAttachmentUploadLinks again with the existing\n uploadId and a new uploadRange.

\n

Upload each part to its presigned URL by using HTTP PUT and capture the ETag\n from the response. After you upload all parts, call CompleteAttachmentUpload\n with the uploadId and the list of part indexes and ETags to finalize the upload.\n You can then attach the upload to a case by passing the uploadId in the\n uploadIds parameter of CreateCase or AddCommunicationToCase. To monitor progress before completion, call DescribeAttachmentUploadStatus.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + } + }, + "com.amazonaws.support#GetAttachmentUploadLinksRequest": { + "type": "structure", + "members": { + "fileName": { + "target": "com.amazonaws.support#FileName", + "traits": { + "smithy.api#documentation": "

The name of the file to upload, including the file extension. This value is required when\n you initiate a new upload.

", + "smithy.api#required": {} + } + }, + "fileSizeBytes": { + "target": "com.amazonaws.support#FileSize", + "traits": { + "smithy.api#documentation": "

The total size of the file in bytes. The service uses this value to calculate the total\n number of parts and the size of each part. Required when you initiate a new upload (when\n uploadId isn't provided). Valid range: 1 to 157,286,400 bytes (approximately\n 150 MB).

" + } + }, + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier of an in-progress multipart upload, returned by a previous call to\n GetAttachmentUploadLinks. Specify uploadId to retrieve additional\n presigned upload URLs for an upload that has already been initiated. Required when\n fileSizeBytes isn't provided. Length: 1 to 2,048 characters.

" + } + }, + "uploadRange": { + "target": "com.amazonaws.support#UploadRange", + "traits": { + "smithy.api#documentation": "

The range of part indexes for which to return presigned upload URLs. Use this parameter\n to page through the upload URLs for a large file across multiple calls. If you omit this\n parameter, the service determines the range to return.

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually generating upload URLs. When\n set to true, the request is validated but no URLs are returned, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.amazonaws.support#GetAttachmentUploadLinksResponse": { + "type": "structure", + "members": { + "uploadId": { + "target": "com.amazonaws.support#UploadId", + "traits": { + "smithy.api#documentation": "

The unique identifier for the multipart upload. Use this value in subsequent calls to\n GetAttachmentUploadLinks, DescribeAttachmentUploadStatus,\n and CompleteAttachmentUpload, and to attach the upload to a case through the\n uploadIds parameter on CreateCase or AddCommunicationToCase.

", + "smithy.api#required": {} + } + }, + "partSizeBytes": { + "target": "com.amazonaws.support#PartSizeBytes", + "traits": { + "smithy.api#documentation": "

The size, in bytes, of each part. Split the file into parts of this size before you upload\n them to the presigned URLs. For an upload with n total parts, parts 1 through\n n - 1 are exactly this size; the last part may be smaller. Maximum:\n 104,857,600 bytes (approximately 100 MB).

", + "smithy.api#required": {} + } + }, + "totalParts": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The total number of parts that the file is split into. Upload one part to each presigned\n URL.

", + "smithy.api#required": {} + } + }, + "nextIndex": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The next part index to request presigned URLs for. If all upload URLs for the file have\n been returned, this field is null. Use this value as the startIndex in\n uploadRange on a subsequent call to GetAttachmentUploadLinks to\n retrieve the next batch of upload URLs.

" + } + }, + "uploadUrls": { + "target": "com.amazonaws.support#UploadUrlList", + "traits": { + "smithy.api#documentation": "

The list of presigned upload URLs for the requested range of parts. The list contains at\n most 10 URLs per call. Upload each part to its corresponding URL by using HTTP\n PUT before the URL expires.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.amazonaws.support#HttpsUrl": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 8 + }, + "smithy.api#pattern": "^https://[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]\\.[a-zA-Z]{2,}(/.*)?$" + } + }, "com.amazonaws.support#IncludeCommunications": { "type": "boolean" }, @@ -2972,6 +3508,12 @@ "smithy.api#default": false } }, + "com.amazonaws.support#Integer": { + "type": "integer", + "traits": { + "smithy.api#default": 0 + } + }, "com.amazonaws.support#InternalServerError": { "type": "structure", "members": { @@ -3011,6 +3553,18 @@ "com.amazonaws.support#NextToken": { "type": "string" }, + "com.amazonaws.support#NullableBooleanType": { + "type": "boolean" + }, + "com.amazonaws.support#PartSizeBytes": { + "type": "long", + "traits": { + "smithy.api#range": { + "min": 1, + "max": 104857600 + } + } + }, "com.amazonaws.support#RecentCaseCommunications": { "type": "structure", "members": { @@ -3045,7 +3599,7 @@ } ], "traits": { - "smithy.api#documentation": "

Refreshes the Trusted Advisor check that you specify using the check ID. You can get the\n check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

Some checks are refreshed automatically. If you call the\n RefreshTrustedAdvisorCheck operation to refresh them, you might see\n the InvalidParameterValue error.

\n

The response contains a TrustedAdvisorCheckRefreshStatus\n object.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" + "smithy.api#documentation": "

Refreshes the Trusted Advisor check that you specify using the check ID. You can get the\n check IDs by calling the DescribeTrustedAdvisorChecks\n operation.

\n

Some checks are refreshed automatically. If you call the\n RefreshTrustedAdvisorCheck operation to refresh them, you might see\n the InvalidParameterValue error.

\n

The response contains a TrustedAdvisorCheckRefreshStatus\n object.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
\n

To call the Trusted Advisor operations in\nthe Amazon Web Services Support API, you must use the US East (N. Virginia) endpoint. Currently, the US West (Oregon) and Europe (Ireland) \nendpoints don't support the Trusted Advisor operations. For more information, see About the Amazon Web Services Support\nAPI in the Amazon Web Services Support User Guide.

" } }, "com.amazonaws.support#RefreshTrustedAdvisorCheckRequest": { @@ -3092,12 +3646,15 @@ { "target": "com.amazonaws.support#CaseIdNotFound" }, + { + "target": "com.amazonaws.support#DryRunOperationException" + }, { "target": "com.amazonaws.support#InternalServerError" } ], "traits": { - "smithy.api#documentation": "

Resolves a support case. This operation takes a caseId and returns the\n initial and final state of the case.

\n \n
    \n
  • \n

    You must have a Business, Enterprise On-Ramp, or Enterprise Support plan to use the Amazon Web Services Support\n API.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have a\n Business, Enterprise On-Ramp, or Enterprise Support plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" + "smithy.api#documentation": "

Resolves a support case. This operation takes a caseId and returns the\n initial and final state of the case.

\n \n
    \n
  • \n

    You must have an Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan to use the Amazon Web Services Support\n API. If you're in an Amazon Web Services Region that doesn't offer one of these Amazon Web Services Support plans, or if you haven't transitioned to one of these plans, you can use the Amazon Web Services Support API with a Business, Enterprise On-Ramp, or Enterprise Support plan.

    \n
  • \n
  • \n

    If you call the Amazon Web Services Support API from an account that doesn't have an\n Amazon Web Services Business Support+, Amazon Web Services Enterprise Support, or Amazon Web Services Unified Operations plan, the\n SubscriptionRequiredException error message appears. For\n information about changing your support plan, see Amazon Web Services Support.

    \n
  • \n
\n
" } }, "com.amazonaws.support#ResolveCaseRequest": { @@ -3106,7 +3663,13 @@ "caseId": { "target": "com.amazonaws.support#CaseId", "traits": { - "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-2013-c4c1d2bf33c5cf47\n

" + "smithy.api#documentation": "

The support case ID requested or returned in the call. The case ID is an alphanumeric\n string formatted as shown in this example:\n case-12345678910-exen-2025-c4c1d2bf33c5cf47\n

" + } + }, + "dryRun": { + "target": "com.amazonaws.support#NullableBooleanType", + "traits": { + "smithy.api#documentation": "

Specifies whether to validate the request without actually resolving the case. When set\n to true, the request is validated but the case isn't resolved, and the operation\n returns a DryRunOperationException. When omitted or set to false, the\n request runs normally.

" } } }, @@ -3232,6 +3795,14 @@ "target": "com.amazonaws.support#SeverityLevel" } }, + "com.amazonaws.support#StartIndex": { + "type": "integer", + "traits": { + "smithy.api#range": { + "min": 1 + } + } + }, "com.amazonaws.support#StartTime": { "type": "string" }, @@ -3325,6 +3896,12 @@ "members": { "message": { "target": "com.amazonaws.support#AvailabilityErrorMessage" + }, + "throttlingReasons": { + "target": "com.amazonaws.support#ThrottlingReasonList", + "traits": { + "smithy.api#documentation": "

A list of one or more reasons that the request was throttled.

" + } } }, "traits": { @@ -3337,6 +3914,32 @@ "smithy.api#httpError": 400 } }, + "com.amazonaws.support#ThrottlingReason": { + "type": "structure", + "members": { + "reason": { + "target": "com.amazonaws.support#CoralAvailabilityThrottlingReason", + "traits": { + "smithy.api#documentation": "

The reason that the request was throttled.

" + } + }, + "resource": { + "target": "com.amazonaws.support#CoralAvailabilityThrottledResource", + "traits": { + "smithy.api#documentation": "

The resource that caused the request to be throttled.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

Information about why a request was throttled.

" + } + }, + "com.amazonaws.support#ThrottlingReasonList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#ThrottlingReason" + } + }, "com.amazonaws.support#TimeCreated": { "type": "string" }, @@ -3658,6 +4261,139 @@ "com.amazonaws.support#Type": { "type": "string" }, + "com.amazonaws.support#UploadId": { + "type": "string", + "traits": { + "smithy.api#length": { + "min": 1, + "max": 2048 + } + } + }, + "com.amazonaws.support#UploadIdNotFound": { + "type": "structure", + "members": { + "message": { + "target": "com.amazonaws.support#ErrorMessage" + } + }, + "traits": { + "smithy.api#documentation": "

The specified uploadId couldn't be located.

", + "smithy.api#error": "client" + } + }, + "com.amazonaws.support#UploadIds": { + "type": "list", + "member": { + "target": "com.amazonaws.support#UploadId" + }, + "traits": { + "smithy.api#length": { + "min": 0, + "max": 10 + } + } + }, + "com.amazonaws.support#UploadProgress": { + "type": "structure", + "members": { + "totalParts": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The total number of parts that the file is split into.

" + } + }, + "completedPartsCount": { + "target": "com.amazonaws.support#FieldIntegerValue", + "traits": { + "smithy.api#documentation": "

The number of parts that have been successfully uploaded.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The progress of a multipart attachment upload, returned by DescribeAttachmentUploadStatus.

" + } + }, + "com.amazonaws.support#UploadRange": { + "type": "structure", + "members": { + "startIndex": { + "target": "com.amazonaws.support#StartIndex", + "traits": { + "smithy.api#documentation": "

The starting part index of the range, inclusive. Part indexes start at 1.

", + "smithy.api#required": {} + } + }, + "endIndex": { + "target": "com.amazonaws.support#EndIndex", + "traits": { + "smithy.api#documentation": "

The ending part index of the range, exclusive. The range is half-open:\n startIndex is inclusive and endIndex is exclusive. For example,\n a range with startIndex of 1 and endIndex of 4 requests URLs for\n parts 1, 2, and 3. The range size (endIndex - startIndex)\n must not exceed 10. If you omit endIndex, the service defaults to\n startIndex + 10, capped by the total number of parts.

" + } + } + }, + "traits": { + "smithy.api#documentation": "

The range of part indexes for which to return presigned upload URLs from GetAttachmentUploadLinks.

" + } + }, + "com.amazonaws.support#UploadStatus": { + "type": "enum", + "members": { + "ATTACHMENT_READY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "attachment-ready" + } + }, + "ATTACHMENT_NOT_READY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "attachment-not-ready" + } + }, + "FAILED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "failed" + } + } + } + }, + "com.amazonaws.support#UploadUrl": { + "type": "structure", + "members": { + "url": { + "target": "com.amazonaws.support#HttpsUrl", + "traits": { + "smithy.api#documentation": "

The presigned HTTPS URL that you use to upload a single part with HTTP\n PUT. Upload URLs are served from\n uploadv1.attachments.support.{region}.amazonaws.com. The\n uploadv1 prefix is subject to change.

", + "smithy.api#required": {} + } + }, + "partIndex": { + "target": "com.amazonaws.support#Integer", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

The index of the part that this URL uploads.

", + "smithy.api#required": {} + } + }, + "expiryDate": { + "target": "com.amazonaws.support#ValidatedDateTime", + "traits": { + "smithy.api#documentation": "

The date and time, in ISO-8601 format, when the presigned URL expires. Upload the part\n before this time.

", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#documentation": "

A presigned URL for uploading a single part of a multipart attachment upload, along with\n the part index and the date and time the URL expires. Returned by GetAttachmentUploadLinks.

" + } + }, + "com.amazonaws.support#UploadUrlList": { + "type": "list", + "member": { + "target": "com.amazonaws.support#UploadUrl" + } + }, "com.amazonaws.support#ValidatedCategoryCode": { "type": "string", "traits": { diff --git a/crates/fakecloud-support/src/dataplane.rs b/crates/fakecloud-support/src/dataplane.rs new file mode 100644 index 000000000..8ae7d36d1 --- /dev/null +++ b/crates/fakecloud-support/src/dataplane.rs @@ -0,0 +1,326 @@ +//! The attachment data plane behind the presigned links AWS Support hands out. +//! +//! `GetAttachmentUploadLinks` mints one presigned `PUT` link per part and +//! `GetAttachmentDownloadLink` mints a presigned `GET` link for a stored +//! attachment. Real AWS points those links at S3; fakecloud points them at its +//! own endpoint and serves them here, so a client that follows the URL it was +//! handed really does transfer bytes. +//! +//! Both entry points are reached by unauthenticated HTTP (that is the point of +//! a presigned URL), so authorisation is the `X-Amz-Signature` in the query +//! string: the value recorded in state when the link was issued, compared in +//! constant time, plus the link's own expiry. A link that was never issued, was +//! tampered with, or has expired is refused. +//! +//! The server crate mounts these as routes; everything that touches Support +//! state lives here so the transport layer stays a thin shim. + +use base64::Engine; + +use crate::shared::{iso_now, part_etag}; +use crate::state::{SharedSupportState, UPLOAD_NOT_READY}; + +/// Result of a presigned part upload. +#[derive(Debug, PartialEq, Eq)] +pub enum PutPartOutcome { + /// The part was stored. Carries the `ETag` the client must echo back in + /// `CompleteAttachmentUpload`. + Stored(String), + /// No such account / upload id / part index. + NotFound, + /// The link's signature is not the one issued for this part. + Forbidden, + /// The link expired, or the upload it belongs to did. + Expired, + /// The upload was already completed; its links no longer accept bytes. + AlreadyCompleted, +} + +/// Result of a presigned attachment download. +#[derive(Debug, PartialEq, Eq)] +pub enum DownloadOutcome { + /// `(fileName, bytes)` of the stored attachment. + Found(String, Vec), + /// No such account, grant, or attachment. + NotFound, + /// The link's signature is not one that was issued. + Forbidden, + /// The link expired. + Expired, +} + +/// Compare two signatures without leaking their contents through timing. +fn signatures_match(a: &str, b: &str) -> bool { + let (a, b) = (a.as_bytes(), b.as_bytes()); + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +/// Store the bytes a client `PUT` to one part's presigned link. +pub fn put_upload_part( + state: &SharedSupportState, + account_id: &str, + upload_id: &str, + part_index: i64, + signature: &str, + body: &[u8], +) -> PutPartOutcome { + let now = iso_now(); + let mut guard = state.write(); + let Some(data) = guard.get_mut(account_id) else { + return PutPartOutcome::NotFound; + }; + let Some(upload) = data.attachment_uploads.get_mut(upload_id) else { + return PutPartOutcome::NotFound; + }; + if upload.status != UPLOAD_NOT_READY { + return PutPartOutcome::AlreadyCompleted; + } + if upload.expiry <= now { + return PutPartOutcome::Expired; + } + let Some(part) = upload.part_mut(part_index) else { + return PutPartOutcome::NotFound; + }; + if !signatures_match(&part.signature, signature) { + return PutPartOutcome::Forbidden; + } + if part.expiry <= now { + return PutPartOutcome::Expired; + } + let etag = part_etag(body); + part.data = Some(base64::engine::general_purpose::STANDARD.encode(body)); + part.etag = Some(etag.clone()); + PutPartOutcome::Stored(etag) +} + +/// Serve the attachment behind a presigned download link. +pub fn fetch_attachment( + state: &SharedSupportState, + account_id: &str, + attachment_id: &str, + signature: &str, +) -> DownloadOutcome { + let now = iso_now(); + let guard = state.read(); + let Some(data) = guard.get(account_id) else { + return DownloadOutcome::NotFound; + }; + let Some(grant) = data.attachment_downloads.get(signature) else { + // An unknown signature is indistinguishable from a forged one; the + // grant map is keyed by signature so there is nothing to compare in + // constant time here. + return DownloadOutcome::Forbidden; + }; + if grant.attachment_id != attachment_id { + return DownloadOutcome::Forbidden; + } + if grant.expiry <= now { + return DownloadOutcome::Expired; + } + let Some(attachment) = data.attachments.get(attachment_id) else { + return DownloadOutcome::NotFound; + }; + let file_name = attachment + .get("fileName") + .and_then(serde_json::Value::as_str) + .unwrap_or("attachment") + .to_string(); + let encoded = attachment + .get("data") + .and_then(serde_json::Value::as_str) + .unwrap_or_default(); + let bytes = base64::engine::general_purpose::STANDARD + .decode(encoded) + .unwrap_or_default(); + DownloadOutcome::Found(file_name, bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shared::{iso_in, LINK_TTL_SECONDS}; + use crate::state::{AttachmentUpload, DownloadGrant, SupportData, UploadPart, UPLOAD_READY}; + use fakecloud_core::multi_account::MultiAccountState; + use parking_lot::RwLock; + use serde_json::json; + use std::sync::Arc; + + const ACCOUNT: &str = "000000000000"; + + fn state() -> SharedSupportState { + Arc::new(RwLock::new(MultiAccountState::new( + ACCOUNT, + "us-east-1", + "http://localhost:4566", + ))) + } + + fn seed_upload(state: &SharedSupportState, expiry: &str) { + let mut guard = state.write(); + let data: &mut SupportData = guard.get_or_create(ACCOUNT); + data.attachment_uploads.insert( + "upload-1".into(), + AttachmentUpload { + upload_id: "upload-1".into(), + file_name: "log.txt".into(), + file_size_bytes: 5, + part_size_bytes: 5 * 1024 * 1024, + total_parts: 1, + status: UPLOAD_NOT_READY.into(), + expiry: expiry.into(), + parts: vec![UploadPart { + part_index: 1, + signature: "sig-1".into(), + expiry: expiry.into(), + etag: None, + data: None, + }], + attachment_id: None, + }, + ); + } + + #[test] + fn put_stores_bytes_and_returns_an_etag() { + let state = state(); + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + let outcome = put_upload_part(&state, ACCOUNT, "upload-1", 1, "sig-1", b"hello"); + // MD5 of "hello", quoted the way S3 quotes an ETag. + assert_eq!( + outcome, + PutPartOutcome::Stored("\"5d41402abc4b2a76b9719d911017c592\"".into()) + ); + let guard = state.read(); + let part = guard.get(ACCOUNT).unwrap().attachment_uploads["upload-1"] + .part(1) + .unwrap() + .clone(); + assert_eq!(part.data.unwrap(), "aGVsbG8="); + } + + #[test] + fn put_rejects_a_forged_signature() { + let state = state(); + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 1, "sig-2", b"hello"), + PutPartOutcome::Forbidden + ); + } + + #[test] + fn put_rejects_unknown_ids_and_expired_links() { + let state = state(); + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + assert_eq!( + put_upload_part(&state, "999999999999", "upload-1", 1, "sig-1", b"x"), + PutPartOutcome::NotFound + ); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-missing", 1, "sig-1", b"x"), + PutPartOutcome::NotFound + ); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 7, "sig-1", b"x"), + PutPartOutcome::NotFound + ); + + seed_upload(&state, "2000-01-01T00:00:00.000Z"); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 1, "sig-1", b"x"), + PutPartOutcome::Expired + ); + } + + #[test] + fn put_rejects_a_completed_upload() { + let state = state(); + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + state + .write() + .get_mut(ACCOUNT) + .unwrap() + .attachment_uploads + .get_mut("upload-1") + .unwrap() + .status = UPLOAD_READY.into(); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 1, "sig-1", b"x"), + PutPartOutcome::AlreadyCompleted + ); + } + + fn seed_attachment(state: &SharedSupportState, expiry: &str) { + let mut guard = state.write(); + let data: &mut SupportData = guard.get_or_create(ACCOUNT); + data.attachments.insert( + "attachment-1".into(), + json!({ "fileName": "log.txt", "data": "aGVsbG8=" }), + ); + data.attachment_downloads.insert( + "dl-sig".into(), + DownloadGrant { + attachment_id: "attachment-1".into(), + expiry: expiry.into(), + }, + ); + } + + #[test] + fn download_returns_the_stored_bytes() { + let state = state(); + seed_attachment(&state, &iso_in(LINK_TTL_SECONDS)); + assert_eq!( + fetch_attachment(&state, ACCOUNT, "attachment-1", "dl-sig"), + DownloadOutcome::Found("log.txt".into(), b"hello".to_vec()) + ); + } + + #[test] + fn download_rejects_forged_expired_and_unknown_links() { + let state = state(); + seed_attachment(&state, &iso_in(LINK_TTL_SECONDS)); + assert_eq!( + fetch_attachment(&state, ACCOUNT, "attachment-1", "nope"), + DownloadOutcome::Forbidden + ); + // A valid signature cannot be replayed against another attachment. + assert_eq!( + fetch_attachment(&state, ACCOUNT, "attachment-2", "dl-sig"), + DownloadOutcome::Forbidden + ); + assert_eq!( + fetch_attachment(&state, "999999999999", "attachment-1", "dl-sig"), + DownloadOutcome::NotFound + ); + + seed_attachment(&state, "2000-01-01T00:00:00.000Z"); + assert_eq!( + fetch_attachment(&state, ACCOUNT, "attachment-1", "dl-sig"), + DownloadOutcome::Expired + ); + } + + #[test] + fn download_of_a_deleted_attachment_is_not_found() { + let state = state(); + seed_attachment(&state, &iso_in(LINK_TTL_SECONDS)); + state + .write() + .get_mut(ACCOUNT) + .unwrap() + .attachments + .remove("attachment-1"); + assert_eq!( + fetch_attachment(&state, ACCOUNT, "attachment-1", "dl-sig"), + DownloadOutcome::NotFound + ); + } +} diff --git a/crates/fakecloud-support/src/lib.rs b/crates/fakecloud-support/src/lib.rs index eb5930359..b43e703dc 100644 --- a/crates/fakecloud-support/src/lib.rs +++ b/crates/fakecloud-support/src/lib.rs @@ -1,9 +1,12 @@ //! AWS Support (`support`) awsJson1_1 service for fakecloud. //! -//! The full 16-operation AWS Support Smithy model: the Support Cases API +//! The full 20-operation AWS Support Smithy model: the Support Cases API //! (`CreateCase` / `DescribeCases` / `DescribeCommunications` / //! `AddCommunicationToCase` / `ResolveCase`), attachment sets -//! (`AddAttachmentsToSet` / `DescribeAttachment`), the Trusted Advisor API +//! (`AddAttachmentsToSet` / `DescribeAttachment`), presigned attachment +//! uploads (`GetAttachmentUploadLinks` / `CompleteAttachmentUpload` / +//! `DescribeAttachmentUploadStatus` / `GetAttachmentDownloadLink`), the +//! Trusted Advisor API //! (`DescribeTrustedAdvisorChecks` / `DescribeTrustedAdvisorCheckResult` / //! `DescribeTrustedAdvisorCheckSummaries` / //! `DescribeTrustedAdvisorCheckRefreshStatuses` / `RefreshTrustedAdvisorCheck`), @@ -26,6 +29,14 @@ //! `resolved`, returning the initial and final status. Attachment sets are real //! (`AddAttachmentsToSet` mints/extends an `attachmentSetId` with an //! `expiryTime`; `DescribeAttachment` returns a stored attachment by id). +//! Attachment uploads are real end to end: `GetAttachmentUploadLinks` records +//! an upload and issues one presigned `PUT` link per 5 MiB part pointing back +//! at this server, the [`dataplane`] routes the server mounts store the bytes +//! and return an `ETag` per part, `CompleteAttachmentUpload` verifies every +//! part and its `ETag` before assembling the attachment, and +//! `GetAttachmentDownloadLink` issues a presigned `GET` link that serves it +//! back. A completed upload attaches to a case or communication through +//! `uploadIds`. //! Severity levels and the Trusted Advisor check catalogue are faithful static //! AWS reference data; the Trusted Advisor refresh status is a real per-check //! `none -> enqueued -> processing -> success` state machine. @@ -40,13 +51,16 @@ //! real, account-partitioned, and persisted. mod catalog; +pub mod dataplane; pub mod persistence; pub mod service; pub mod shared; pub mod state; mod validate; +pub use dataplane::{fetch_attachment, put_upload_part, DownloadOutcome, PutPartOutcome}; pub use service::{SupportService, SUPPORT_ACTIONS}; pub use state::{ - SharedSupportState, SupportData, SupportSnapshot, SUPPORT_SNAPSHOT_SCHEMA_VERSION, + AttachmentUpload, DownloadGrant, SharedSupportState, SupportData, SupportSnapshot, UploadPart, + SUPPORT_SNAPSHOT_SCHEMA_VERSION, UPLOAD_FAILED, UPLOAD_NOT_READY, UPLOAD_READY, }; diff --git a/crates/fakecloud-support/src/persistence.rs b/crates/fakecloud-support/src/persistence.rs index 9b332da10..59a8ba50b 100644 --- a/crates/fakecloud-support/src/persistence.rs +++ b/crates/fakecloud-support/src/persistence.rs @@ -39,8 +39,9 @@ pub fn load_into( supported: SUPPORT_SNAPSHOT_SCHEMA_VERSION, }); } - // There is no timer-driven lifecycle to resume, but keep the reconcile call - // for symmetry with the other services (it is a no-op). + // Attachment uploads have a wall-clock lifecycle: reconcile sweeps any + // upload whose presigned links expired while the server was down to + // `failed`, and drops expired download grants. for (_account_id, account) in snapshot.accounts.iter_mut() { account.reconcile(); } @@ -111,6 +112,60 @@ mod tests { ); } + #[test] + fn round_trip_restores_attachment_uploads_and_sweeps_expired_ones() { + use crate::state::{AttachmentUpload, UploadPart, UPLOAD_FAILED, UPLOAD_NOT_READY}; + + let mut accounts: MultiAccountState = + MultiAccountState::new("000000000000", "us-east-1", "http://localhost:4566"); + let data = accounts.get_or_create("111122223333"); + let mut upload = AttachmentUpload { + upload_id: "upload-live".into(), + file_name: "log.txt".into(), + file_size_bytes: 5, + part_size_bytes: 5 * 1024 * 1024, + total_parts: 1, + status: UPLOAD_NOT_READY.into(), + expiry: "2999-01-01T00:00:00.000Z".into(), + parts: vec![UploadPart { + part_index: 1, + signature: "sig".into(), + expiry: "2999-01-01T00:00:00.000Z".into(), + etag: Some("\"abc\"".into()), + data: Some("aGVsbG8=".into()), + }], + attachment_id: None, + }; + data.attachment_uploads + .insert(upload.upload_id.clone(), upload.clone()); + upload.upload_id = "upload-stale".into(); + upload.expiry = "2000-01-01T00:00:00.000Z".into(); + data.attachment_uploads + .insert(upload.upload_id.clone(), upload); + + let snap = SupportSnapshot { + schema_version: SUPPORT_SNAPSHOT_SCHEMA_VERSION, + accounts, + }; + let store = MemStore(Mutex::new(Some(serde_json::to_vec(&snap).unwrap()))); + let restored = state(); + assert!(matches!( + load_into(&store, &restored).unwrap(), + LoadOutcome::Loaded(_) + )); + let guard = restored.read(); + let uploads = &guard.get("111122223333").unwrap().attachment_uploads; + // The live upload keeps its status and its already-uploaded bytes. + assert_eq!(uploads["upload-live"].status, UPLOAD_NOT_READY); + assert_eq!( + uploads["upload-live"].part(1).unwrap().data.as_deref(), + Some("aGVsbG8=") + ); + // The one whose links expired while the server was down cannot be + // completed any more. + assert_eq!(uploads["upload-stale"].status, UPLOAD_FAILED); + } + #[test] fn round_trip_restores_cases() { let mut accounts: MultiAccountState = diff --git a/crates/fakecloud-support/src/service.rs b/crates/fakecloud-support/src/service.rs index bad20bb05..201459354 100644 --- a/crates/fakecloud-support/src/service.rs +++ b/crates/fakecloud-support/src/service.rs @@ -8,17 +8,26 @@ //! returns `AttachmentIdNotFound`, and an unknown attachment set id returns //! `AttachmentSetIdNotFound`. //! +//! Attachment uploads are the presigned flow: `GetAttachmentUploadLinks` +//! records an upload and hands out one presigned `PUT` link per part, +//! `CompleteAttachmentUpload` verifies the parts and their `ETag`s and +//! assembles the attachment, `DescribeAttachmentUploadStatus` reports the +//! recorded progress, and `GetAttachmentDownloadLink` mints a presigned `GET` +//! link for a stored attachment. The links point back at this fakecloud and +//! are served by [`crate::dataplane`], so they really do transfer bytes. +//! //! Honest gap: fakecloud runs no Trusted Advisor analysis engine and attaches //! no live support agent. `DescribeTrustedAdvisorCheckResult` / //! `DescribeTrustedAdvisorCheckSummaries` return well-formed all-clear result //! shapes (zero flagged resources) rather than fabricating findings, and no //! automated agent reply is generated. Cases, communications, attachment sets, -//! severity levels, the check catalogue, and the refresh state machine are all -//! real. +//! attachment uploads, severity levels, the check catalogue, and the refresh +//! state machine are all real. use std::sync::Arc; use async_trait::async_trait; +use base64::Engine; use http::StatusCode; use serde_json::{json, Value}; use tokio::sync::Mutex as AsyncMutex; @@ -28,17 +37,23 @@ use fakecloud_persistence::SnapshotStore; use crate::catalog; use crate::shared::{ - current_year, display_id, iso_now, new_attachment_id, new_attachment_set_id, new_case_id, - str_member, + attachment_download_path, current_year, display_id, iso_in, iso_now, new_attachment_id, + new_attachment_set_id, new_case_id, new_signature, new_upload_id, presigned_url, str_member, + upload_part_path, DEFAULT_PART_SIZE_BYTES, EXAMPLE_ACCESS_KEY_ID, LINK_TTL_SECONDS, +}; +use crate::state::{ + AttachmentUpload, DownloadGrant, SharedSupportState, SupportData, UploadPart, UPLOAD_FAILED, + UPLOAD_NOT_READY, UPLOAD_READY, }; -use crate::state::{SharedSupportState, SupportData}; -/// Every operation name in the AWS Support Smithy model (16 operations). +/// Every operation name in the AWS Support Smithy model (20 operations). pub const SUPPORT_ACTIONS: &[&str] = &[ "AddAttachmentsToSet", "AddCommunicationToCase", + "CompleteAttachmentUpload", "CreateCase", "DescribeAttachment", + "DescribeAttachmentUploadStatus", "DescribeCases", "DescribeCommunications", "DescribeCreateCaseOptions", @@ -49,16 +64,43 @@ pub const SUPPORT_ACTIONS: &[&str] = &[ "DescribeTrustedAdvisorCheckResult", "DescribeTrustedAdvisorCheckSummaries", "DescribeTrustedAdvisorChecks", + "GetAttachmentDownloadLink", + "GetAttachmentUploadLinks", "RefreshTrustedAdvisorCheck", "ResolveCase", ]; +/// Operations whose Smithy shape declares `DryRunOperationException`. A +/// `dryRun: true` request against one of these is validated and then rejected +/// with that error instead of taking effect, which is what AWS does; the +/// Trusted Advisor operations do not model it and ignore the member. +const DRY_RUN_ACTIONS: &[&str] = &[ + "AddAttachmentsToSet", + "AddCommunicationToCase", + "CompleteAttachmentUpload", + "CreateCase", + "DescribeAttachment", + "DescribeAttachmentUploadStatus", + "DescribeCases", + "DescribeCommunications", + "DescribeCreateCaseOptions", + "DescribeServices", + "DescribeSeverityLevels", + "DescribeSupportedLanguages", + "GetAttachmentDownloadLink", + "GetAttachmentUploadLinks", + "ResolveCase", +]; + /// Read-only verbs; any other action mutates persisted state and triggers a /// snapshot after success. The inverse formulation guarantees no mutation is -/// ever missed if a new op is added. `DescribeTrustedAdvisorCheckRefreshStatuses` -/// advances the refresh state machine in memory but is intentionally treated as -/// read-only (the transition is re-derived on the next read, so persisting it is -/// not required). +/// ever missed if a new op is added: the attachment-upload operations +/// (`GetAttachmentUploadLinks`, `CompleteAttachmentUpload`, +/// `GetAttachmentDownloadLink`) all record issued links or assembled +/// attachments and are correctly classified as mutating by it. +/// `DescribeTrustedAdvisorCheckRefreshStatuses` advances the refresh state +/// machine in memory but is intentionally treated as read-only (the transition +/// is re-derived on the next read, so persisting it is not required). fn is_mutating_action(action: &str) -> bool { !action.starts_with("Describe") } @@ -123,9 +165,15 @@ impl AwsService for SupportService { async fn handle(&self, req: AwsRequest) -> Result { let action = req.action.clone(); - if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) { + let body = req.json_body(); + if let Err(msg) = crate::validate::validate_input(&action, &body) { return Err(validation_error(msg)); } + // A dry run is validated like any other request and then refused + // without touching state, so nothing is persisted for it. + if is_dry_run(&body) && DRY_RUN_ACTIONS.contains(&action.as_str()) { + return Err(dry_run_error(&action)); + } let result = self.dispatch(&action, &req); if is_mutating_action(&action) && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) @@ -152,6 +200,11 @@ impl SupportService { "ResolveCase" => self.resolve_case(req, &body), "AddAttachmentsToSet" => self.add_attachments_to_set(req, &body), "DescribeAttachment" => self.describe_attachment(req, &body), + // Presigned attachment uploads / downloads + "GetAttachmentUploadLinks" => self.get_attachment_upload_links(req, &body), + "CompleteAttachmentUpload" => self.complete_attachment_upload(req, &body), + "DescribeAttachmentUploadStatus" => self.describe_attachment_upload_status(req, &body), + "GetAttachmentDownloadLink" => self.get_attachment_download_link(req, &body), // Trusted Advisor "DescribeTrustedAdvisorChecks" => self.describe_ta_checks(&body), "DescribeTrustedAdvisorCheckResult" => self.describe_ta_check_result(&body), @@ -195,6 +248,7 @@ impl SupportService { .cloned() .unwrap_or_else(|| json!([])); let attachment_set_id = str_member(body, "attachmentSetId").map(str::to_string); + let upload_ids = string_list(body, "uploadIds"); let submitted_by = format!("arn:aws:iam::{account}:root"); let now = iso_now(); @@ -210,6 +264,10 @@ impl SupportService { } None => json!([]), }; + let attachments = communication_attachments( + &attachment_set, + uploads_to_attachment_details(d, &upload_ids)?, + ); let case_id = new_case_id(&account, current_year()); let disp = display_id(&case_id); @@ -232,6 +290,7 @@ impl SupportService { "submittedBy": submitted_by, "timeCreated": now, "attachmentSet": attachment_set, + "attachments": attachments, }); d.cases.insert(case_id.clone(), case); d.communications.insert(case_id.clone(), vec![comm]); @@ -387,6 +446,7 @@ impl SupportService { .unwrap_or_default() .to_string(); let attachment_set_id = str_member(body, "attachmentSetId").map(str::to_string); + let upload_ids = string_list(body, "uploadIds"); let submitted_by = format!("arn:aws:iam::{account}:root"); let now = iso_now(); @@ -404,12 +464,17 @@ impl SupportService { } None => json!([]), }; + let attachments = communication_attachments( + &attachment_set, + uploads_to_attachment_details(d, &upload_ids)?, + ); let comm = json!({ "caseId": case_id, "body": comm_body, "submittedBy": submitted_by, "timeCreated": now, "attachmentSet": attachment_set, + "attachments": attachments, }); d.communications .entry(case_id.clone()) @@ -519,6 +584,328 @@ impl SupportService { }) } + // ---- Presigned attachment uploads / downloads ------------------------- + + /// Start (or resume) a multipart attachment upload and hand out one + /// presigned `PUT` link per part. Every link is recorded in state with its + /// own signature and expiry, so the data-plane route can authorise the + /// `PUT` that follows. + fn get_attachment_upload_links( + &self, + req: &AwsRequest, + body: &Value, + ) -> Result { + let account = req.account_id.clone(); + let file_name = str_member(body, "fileName").unwrap_or_default().to_string(); + let file_size = body + .get("fileSizeBytes") + .and_then(Value::as_i64) + .unwrap_or(0); + let requested_upload = str_member(body, "uploadId").map(str::to_string); + let range_start = body + .pointer("/uploadRange/startIndex") + .and_then(Value::as_i64); + let range_end = body + .pointer("/uploadRange/endIndex") + .and_then(Value::as_i64); + let access_key = req + .access_key_id + .clone() + .unwrap_or_else(|| EXAMPLE_ACCESS_KEY_ID.to_string()); + let now = iso_now(); + let expiry = iso_in(LINK_TTL_SECONDS); + + self.with_account_mut(req, |d| { + let endpoint = link_endpoint(req, d); + let region = link_region(req, d); + + let upload_id = match &requested_upload { + Some(id) => { + let existing = d + .attachment_uploads + .get(id) + .ok_or_else(|| upload_id_not_found(id))?; + if existing.status == UPLOAD_READY { + return Err(upload_already_completed(id)); + } + if existing.status == UPLOAD_FAILED || existing.expiry <= now { + return Err(upload_expired(id)); + } + id.clone() + } + None => { + let id = new_upload_id(); + // An undeclared size is a single-part upload; a declared + // one is split into 5 MiB parts like the console does. + let total_parts = if file_size <= 0 { + 1 + } else { + (file_size as u64).div_ceil(DEFAULT_PART_SIZE_BYTES as u64) as i64 + }; + d.attachment_uploads.insert( + id.clone(), + AttachmentUpload { + upload_id: id.clone(), + file_name: file_name.clone(), + file_size_bytes: file_size.max(0), + part_size_bytes: DEFAULT_PART_SIZE_BYTES, + total_parts, + status: UPLOAD_NOT_READY.to_string(), + expiry: expiry.clone(), + parts: Vec::new(), + attachment_id: None, + }, + ); + id + } + }; + + let upload = d + .attachment_uploads + .get_mut(&upload_id) + .expect("upload was just inserted or looked up"); + let total_parts = upload.total_parts; + // Without an explicit range, hand out links for everything that is + // still outstanding. + let start = range_start + .unwrap_or_else(|| upload.next_index().max(1)) + .clamp(1, total_parts); + let end = range_end.unwrap_or(total_parts).clamp(start, total_parts); + + let mut upload_urls = Vec::new(); + for part_index in start..=end { + let signature = new_signature(); + let url = presigned_url( + &endpoint, + &upload_part_path(&account, &upload_id, part_index), + ®ion, + &access_key, + &signature, + LINK_TTL_SECONDS, + ); + // Re-issuing a link for a part that already has one replaces + // its signature: the old link stops working, the uploaded + // bytes (if any) are kept so a resume does not lose them. + if upload.part(part_index).is_some() { + let part = upload.part_mut(part_index).expect("checked above"); + part.signature = signature; + part.expiry = expiry.clone(); + } else { + upload.parts.push(UploadPart { + part_index, + signature, + expiry: expiry.clone(), + etag: None, + data: None, + }); + } + upload_urls.push(json!({ + "url": url, + "partIndex": part_index, + "expiryDate": expiry, + })); + } + upload.parts.sort_by_key(|p| p.part_index); + upload.expiry = expiry.clone(); + + Ok(ok(json!({ + "uploadId": upload_id, + "partSizeBytes": upload.part_size_bytes, + "totalParts": total_parts, + "nextIndex": upload.next_index(), + "uploadUrls": upload_urls, + }))) + }) + } + + /// Finalise an upload: every part must have been `PUT` to its link and the + /// client must echo back the `ETag` each `PUT` returned. On success the + /// parts are concatenated into a real attachment, retrievable with + /// `DescribeAttachment` / `GetAttachmentDownloadLink` and attachable to a + /// case through `uploadIds`. + fn complete_attachment_upload( + &self, + req: &AwsRequest, + body: &Value, + ) -> Result { + let upload_id = str_member(body, "uploadId").unwrap_or_default().to_string(); + let claimed: Vec<(i64, String)> = body + .get("completedUploads") + .and_then(Value::as_array) + .map(|a| { + a.iter() + .map(|c| { + ( + c.get("partIndex").and_then(Value::as_i64).unwrap_or(0), + c.get("eTag") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + ) + }) + .collect() + }) + .unwrap_or_default(); + let now = iso_now(); + + self.with_account_mut(req, |d| { + // Clone the record so the checks below can run while the + // attachment map is mutated afterwards. + let upload = d + .attachment_uploads + .get(&upload_id) + .cloned() + .ok_or_else(|| upload_id_not_found(&upload_id))?; + if upload.status == UPLOAD_READY { + return Err(upload_already_completed(&upload_id)); + } + if upload.status == UPLOAD_FAILED || upload.expiry <= now { + return Err(upload_expired(&upload_id)); + } + + for (part_index, _) in &claimed { + if upload.part(*part_index).is_none() { + return Err(validation_error(format!( + "Upload {upload_id} has no part {part_index}." + ))); + } + } + + let mut bytes: Vec = Vec::new(); + for part_index in 1..=upload.total_parts { + let part = upload.part(part_index).ok_or_else(|| { + validation_error(format!( + "No upload link was issued for part {part_index} of upload {upload_id}." + )) + })?; + let (Some(stored_etag), Some(data)) = (&part.etag, &part.data) else { + return Err(validation_error(format!( + "Part {part_index} of upload {upload_id} was never uploaded." + ))); + }; + let claimed_etag = claimed + .iter() + .find(|(i, _)| *i == part_index) + .map(|(_, tag)| tag.as_str()) + .ok_or_else(|| { + validation_error(format!( + "completedUploads is missing part {part_index} of upload {upload_id}." + )) + })?; + if !etags_match(stored_etag, claimed_etag) { + return Err(validation_error(format!( + "The eTag given for part {part_index} of upload {upload_id} does not match the uploaded part." + ))); + } + let mut decoded = base64::engine::general_purpose::STANDARD + .decode(data) + .map_err(|err| { + internal_server_error(format!( + "part {part_index} of upload {upload_id} could not be decoded: {err}" + )) + })?; + bytes.append(&mut decoded); + } + + let attachment_id = new_attachment_id(); + d.attachments.insert( + attachment_id.clone(), + json!({ + "fileName": upload.file_name, + "data": base64::engine::general_purpose::STANDARD.encode(&bytes), + }), + ); + let stored = d + .attachment_uploads + .get_mut(&upload_id) + .expect("looked up above"); + stored.status = UPLOAD_READY.to_string(); + stored.attachment_id = Some(attachment_id); + + Ok(ok(json!({ "uploadStatus": UPLOAD_READY }))) + }) + } + + fn describe_attachment_upload_status( + &self, + req: &AwsRequest, + body: &Value, + ) -> Result { + let upload_id = str_member(body, "uploadId").unwrap_or_default().to_string(); + let now = iso_now(); + self.with_account_mut(req, |d| { + let upload = d + .attachment_uploads + .get(&upload_id) + .ok_or_else(|| upload_id_not_found(&upload_id))?; + // An upload whose links expired before it was completed can never + // be completed, so report it as failed even though the sweep that + // records that only runs on load. + let status = if upload.status == UPLOAD_NOT_READY && upload.expiry <= now { + UPLOAD_FAILED + } else { + upload.status.as_str() + }; + Ok(ok(json!({ + "uploadStatus": status, + "fileName": upload.file_name, + "uploadProgress": { + "totalParts": upload.total_parts, + "completedPartsCount": upload.completed_parts(), + }, + }))) + }) + } + + fn get_attachment_download_link( + &self, + req: &AwsRequest, + body: &Value, + ) -> Result { + let account = req.account_id.clone(); + let attachment_id = str_member(body, "attachmentId") + .unwrap_or_default() + .to_string(); + let access_key = req + .access_key_id + .clone() + .unwrap_or_else(|| EXAMPLE_ACCESS_KEY_ID.to_string()); + let expiry = iso_in(LINK_TTL_SECONDS); + + self.with_account_mut(req, |d| { + let file_name = d + .attachments + .get(&attachment_id) + .ok_or_else(|| attachment_id_not_found(&attachment_id))? + .get("fileName") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + let endpoint = link_endpoint(req, d); + let region = link_region(req, d); + let signature = new_signature(); + let url = presigned_url( + &endpoint, + &attachment_download_path(&account, &attachment_id), + ®ion, + &access_key, + &signature, + LINK_TTL_SECONDS, + ); + d.attachment_downloads.insert( + signature, + DownloadGrant { + attachment_id: attachment_id.clone(), + expiry: expiry.clone(), + }, + ); + Ok(ok(json!({ + "fileName": file_name, + "downloadUrl": { "url": url, "expiryDate": expiry }, + }))) + }) + } + // ---- Trusted Advisor -------------------------------------------------- fn describe_ta_checks(&self, _body: &Value) -> Result { @@ -715,6 +1102,88 @@ fn attachment_set_summary(d: &SupportData, set: &Value) -> Value { json!(details) } +/// The endpoint presigned attachment links should point at: the endpoint this +/// server was started with, falling back to the request's own `Host` (a +/// snapshot written before the upload flow existed carries no endpoint). +fn link_endpoint(req: &AwsRequest, d: &SupportData) -> String { + if !d.endpoint.is_empty() { + return d.endpoint.clone(); + } + req.headers + .get("host") + .and_then(|v| v.to_str().ok()) + .map(|host| format!("http://{host}")) + .unwrap_or_else(|| "http://localhost:4566".to_string()) +} + +/// The region to name in a presigned link's credential scope. +fn link_region(req: &AwsRequest, d: &SupportData) -> String { + if !d.region.is_empty() { + d.region.clone() + } else { + req.region.clone() + } +} + +/// Compare an `ETag` the client echoed back against the one the data plane +/// issued. Clients quote it, strip the quotes, or (via XML-encoding layers in +/// some SDKs and Terraform) send the quotes as numeric entities, so normalise +/// all of those away before comparing. +fn etags_match(stored: &str, claimed: &str) -> bool { + normalize_etag(stored) == normalize_etag(claimed) +} + +fn normalize_etag(tag: &str) -> String { + tag.replace(""", "") + .replace(""", "") + .replace(""", "") + .replace('"', "") + .trim() + .to_ascii_lowercase() +} + +/// The `attachments` member of a communication: the attachment set's entries +/// plus anything attached through `uploadIds`. The model added `attachments` +/// alongside the older `attachmentSet` so large, upload-based attachments have +/// somewhere to appear; both members are populated so either client works. +fn communication_attachments(attachment_set: &Value, upload_details: Vec) -> Value { + let mut all = attachment_set.as_array().cloned().unwrap_or_default(); + all.extend(upload_details); + Value::Array(all) +} + +/// Whether the request asked for a dry run. +fn is_dry_run(body: &Value) -> bool { + body.get("dryRun").and_then(Value::as_bool).unwrap_or(false) +} + +/// Resolve `uploadIds` to the attachments their completed uploads produced. +/// Neither `CreateCase` nor `AddCommunicationToCase` models `UploadIdNotFound`, +/// so an unknown or unfinished upload is reported as a request-validation +/// failure, which both operations can express. +fn uploads_to_attachment_details( + d: &SupportData, + upload_ids: &[String], +) -> Result, AwsServiceError> { + let mut details = Vec::with_capacity(upload_ids.len()); + for upload_id in upload_ids { + let upload = d + .attachment_uploads + .get(upload_id) + .ok_or_else(|| validation_error(format!("Upload {upload_id} was not found.")))?; + let attachment_id = upload.attachment_id.as_ref().ok_or_else(|| { + validation_error(format!( + "Upload {upload_id} has not been completed; call CompleteAttachmentUpload first." + )) + })?; + details.push(json!({ + "attachmentId": attachment_id, + "fileName": upload.file_name, + })); + } + Ok(details) +} + fn string_list(body: &Value, name: &str) -> Vec { body.get(name) .and_then(Value::as_array) @@ -756,6 +1225,49 @@ fn attachment_set_id_not_found(id: &str) -> AwsServiceError { ) } +/// `UploadIdNotFound` is the only upload-lookup error the three upload +/// operations model, so it carries every reason an upload id cannot be acted +/// on; the message says which. +fn upload_id_not_found(id: &str) -> AwsServiceError { + AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "UploadIdNotFound", + format!("Upload {id} was not found."), + ) +} + +fn upload_already_completed(id: &str) -> AwsServiceError { + AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "UploadIdNotFound", + format!("Upload {id} has already been completed."), + ) +} + +fn upload_expired(id: &str) -> AwsServiceError { + AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "UploadIdNotFound", + format!("Upload {id} has expired."), + ) +} + +fn dry_run_error(action: &str) -> AwsServiceError { + AwsServiceError::aws_error( + StatusCode::BAD_REQUEST, + "DryRunOperationException", + format!("Request would have succeeded, but DryRun flag is set for {action}."), + ) +} + +fn internal_server_error(msg: impl Into) -> AwsServiceError { + AwsServiceError::aws_error( + StatusCode::INTERNAL_SERVER_ERROR, + "InternalServerError", + msg.into(), + ) +} + fn ok(value: Value) -> AwsResponse { AwsResponse::ok_json(value) } @@ -969,6 +1481,457 @@ mod tests { assert_eq!(step2["statuses"][0]["status"], "success"); } + /// Drive the real upload data plane for `body`'s worth of bytes and return + /// `(uploadId, completedUploads)` ready for `CompleteAttachmentUpload`. + fn upload_one_part(svc: &SupportService, file_name: &str, bytes: &[u8]) -> (String, Value) { + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": file_name, "fileSizeBytes": bytes.len() }), + ) + .unwrap(), + ); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let signature = signature_of(links["uploadUrls"][0]["url"].as_str().unwrap()); + let outcome = crate::dataplane::put_upload_part( + &svc.state, + "000000000000", + &upload_id, + 1, + &signature, + bytes, + ); + let etag = match outcome { + crate::dataplane::PutPartOutcome::Stored(tag) => tag, + other => panic!("expected the part to be stored, got {other:?}"), + }; + (upload_id, json!([{ "partIndex": 1, "eTag": etag }])) + } + + /// Pull `X-Amz-Signature` out of a presigned URL. + fn signature_of(url: &str) -> String { + url.rsplit("X-Amz-Signature=") + .next() + .unwrap() + .split('&') + .next() + .unwrap() + .to_string() + } + + #[test] + fn attachment_upload_round_trips_through_the_data_plane() { + let svc = service(); + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "log.txt", "fileSizeBytes": 5 }), + ) + .unwrap(), + ); + assert!(links["uploadId"].as_str().unwrap().starts_with("upload-")); + assert_eq!(links["partSizeBytes"], 5 * 1024 * 1024); + assert_eq!(links["totalParts"], 1); + // Nothing uploaded yet, so part 1 is still outstanding. + assert_eq!(links["nextIndex"], 1); + let url = links["uploadUrls"][0]["url"].as_str().unwrap(); + assert!(url.contains("/_fakecloud/support/attachments/uploads/000000000000/")); + assert!(url.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256")); + assert!(links["uploadUrls"][0]["expiryDate"].is_string()); + assert_eq!(links["uploadUrls"][0]["partIndex"], 1); + + // Upload the bytes over the presigned link, then complete. + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let signature = signature_of(url); + let etag = match crate::dataplane::put_upload_part( + &svc.state, + "000000000000", + &upload_id, + 1, + &signature, + b"hello", + ) { + crate::dataplane::PutPartOutcome::Stored(tag) => tag, + other => panic!("expected the part to be stored, got {other:?}"), + }; + + // Progress is visible before completion. + let status = body_of( + &svc.describe_attachment_upload_status( + &req("DescribeAttachmentUploadStatus", json!({})), + &json!({ "uploadId": upload_id }), + ) + .unwrap(), + ); + assert_eq!(status["uploadStatus"], "attachment-not-ready"); + assert_eq!(status["fileName"], "log.txt"); + assert_eq!(status["uploadProgress"]["totalParts"], 1); + assert_eq!(status["uploadProgress"]["completedPartsCount"], 1); + + let completed = body_of( + &svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": etag }], + }), + ) + .unwrap(), + ); + assert_eq!(completed["uploadStatus"], "attachment-ready"); + + let status = body_of( + &svc.describe_attachment_upload_status( + &req("DescribeAttachmentUploadStatus", json!({})), + &json!({ "uploadId": upload_id }), + ) + .unwrap(), + ); + assert_eq!(status["uploadStatus"], "attachment-ready"); + + // The assembled attachment is retrievable and downloadable. + let attachment_id = svc + .state + .read() + .get("000000000000") + .unwrap() + .attachment_uploads[&upload_id] + .attachment_id + .clone() + .unwrap(); + let described = body_of( + &svc.describe_attachment( + &req("DescribeAttachment", json!({})), + &json!({ "attachmentId": attachment_id }), + ) + .unwrap(), + ); + assert_eq!(described["attachment"]["fileName"], "log.txt"); + assert_eq!(described["attachment"]["data"], "aGVsbG8="); + + let link = body_of( + &svc.get_attachment_download_link( + &req("GetAttachmentDownloadLink", json!({})), + &json!({ "attachmentId": attachment_id }), + ) + .unwrap(), + ); + assert_eq!(link["fileName"], "log.txt"); + let download_url = link["downloadUrl"]["url"].as_str().unwrap(); + assert!(download_url.contains(&format!( + "/_fakecloud/support/attachments/downloads/000000000000/{attachment_id}" + ))); + assert!(link["downloadUrl"]["expiryDate"].is_string()); + assert_eq!( + crate::dataplane::fetch_attachment( + &svc.state, + "000000000000", + &attachment_id, + &signature_of(download_url), + ), + crate::dataplane::DownloadOutcome::Found("log.txt".into(), b"hello".to_vec()), + ); + } + + #[test] + fn multipart_upload_splits_into_five_mib_parts() { + let svc = service(); + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "big.bin", "fileSizeBytes": 12 * 1024 * 1024 }), + ) + .unwrap(), + ); + assert_eq!(links["totalParts"], 3); + assert_eq!(links["uploadUrls"].as_array().unwrap().len(), 3); + + // A resume asks for a sub-range of the same upload. + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let resumed = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ + "fileName": "big.bin", + "uploadId": upload_id, + "uploadRange": { "startIndex": 2, "endIndex": 3 }, + }), + ) + .unwrap(), + ); + assert_eq!(resumed["uploadId"], upload_id); + let parts: Vec = resumed["uploadUrls"] + .as_array() + .unwrap() + .iter() + .map(|u| u["partIndex"].as_i64().unwrap()) + .collect(); + assert_eq!(parts, vec![2, 3]); + } + + #[test] + fn unknown_upload_id_is_upload_id_not_found() { + let svc = service(); + for result in [ + svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "log.txt", "uploadId": "upload-missing" }), + ), + svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": "upload-missing", "completedUploads": [] }), + ), + svc.describe_attachment_upload_status( + &req("DescribeAttachmentUploadStatus", json!({})), + &json!({ "uploadId": "upload-missing" }), + ), + ] { + let err = expect_err(result); + assert!(format!("{err:?}").contains("UploadIdNotFound")); + } + } + + #[test] + fn completing_twice_is_rejected() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + let args = json!({ "uploadId": upload_id, "completedUploads": completed_uploads }); + svc.complete_attachment_upload(&req("CompleteAttachmentUpload", json!({})), &args) + .unwrap(); + let err = expect_err( + svc.complete_attachment_upload(&req("CompleteAttachmentUpload", json!({})), &args), + ); + let rendered = format!("{err:?}"); + assert!(rendered.contains("UploadIdNotFound"), "{rendered}"); + assert!(rendered.contains("already been completed"), "{rendered}"); + // Re-issuing links for a completed upload is refused too. + let err = expect_err(svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "log.txt", "uploadId": upload_id }), + )); + assert!(format!("{err:?}").contains("UploadIdNotFound")); + } + + #[test] + fn completing_an_expired_upload_is_rejected() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + svc.state + .write() + .get_mut("000000000000") + .unwrap() + .attachment_uploads + .get_mut(&upload_id) + .unwrap() + .expiry = "2000-01-01T00:00:00.000Z".to_string(); + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": upload_id, "completedUploads": completed_uploads }), + )); + let rendered = format!("{err:?}"); + assert!(rendered.contains("UploadIdNotFound"), "{rendered}"); + assert!(rendered.contains("expired"), "{rendered}"); + // And the status read reports it as failed rather than pending. + let status = body_of( + &svc.describe_attachment_upload_status( + &req("DescribeAttachmentUploadStatus", json!({})), + &json!({ "uploadId": upload_id }), + ) + .unwrap(), + ); + assert_eq!(status["uploadStatus"], "failed"); + } + + #[test] + fn completing_rejects_missing_parts_and_bad_etags() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + + // Part uploaded, but the client claims nothing. + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": upload_id, "completedUploads": [] }), + )); + assert!(format!("{err:?}").contains("missing part 1")); + + // Part index that was never issued. + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 9, "eTag": "\"x\"" }], + }), + )); + assert!(format!("{err:?}").contains("has no part 9")); + + // Right part, wrong eTag. + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": "\"deadbeef\"" }], + }), + )); + assert!(format!("{err:?}").contains("does not match")); + + // The genuine eTag still works, including unquoted. + let unquoted = completed_uploads[0]["eTag"] + .as_str() + .unwrap() + .trim_matches('"') + .to_string(); + let completed = body_of( + &svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": unquoted }], + }), + ) + .unwrap(), + ); + assert_eq!(completed["uploadStatus"], "attachment-ready"); + } + + #[test] + fn completing_before_every_part_is_uploaded_is_rejected() { + let svc = service(); + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "big.bin", "fileSizeBytes": 6 * 1024 * 1024 }), + ) + .unwrap(), + ); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let signature = signature_of(links["uploadUrls"][0]["url"].as_str().unwrap()); + let etag = match crate::dataplane::put_upload_part( + &svc.state, + "000000000000", + &upload_id, + 1, + &signature, + b"first", + ) { + crate::dataplane::PutPartOutcome::Stored(tag) => tag, + other => panic!("expected the part to be stored, got {other:?}"), + }; + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": etag }], + }), + )); + assert!(format!("{err:?}").contains("Part 2")); + } + + #[test] + fn download_link_for_unknown_attachment_is_attachment_id_not_found() { + let svc = service(); + let err = expect_err(svc.get_attachment_download_link( + &req("GetAttachmentDownloadLink", json!({})), + &json!({ "attachmentId": "attachment-missing" }), + )); + assert!(format!("{err:?}").contains("AttachmentIdNotFound")); + } + + #[test] + fn completed_uploads_attach_to_a_case() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": upload_id, "completedUploads": completed_uploads }), + ) + .unwrap(); + + let created = body_of( + &svc.create_case( + &req("CreateCase", json!({})), + &json!({ + "subject": "logs attached", + "communicationBody": "see attached", + "uploadIds": [upload_id], + }), + ) + .unwrap(), + ); + let case_id = created["caseId"].as_str().unwrap().to_string(); + let comms = body_of( + &svc.describe_communications( + &req("DescribeCommunications", json!({})), + &json!({ "caseId": case_id }), + ) + .unwrap(), + ); + let attachments = &comms["communications"][0]["attachments"]; + assert_eq!(attachments.as_array().unwrap().len(), 1); + assert_eq!(attachments[0]["fileName"], "log.txt"); + + // An upload that was never completed cannot be attached. + let (pending, _) = upload_one_part(&svc, "pending.txt", b"x"); + let err = expect_err(svc.add_communication_to_case( + &req("AddCommunicationToCase", json!({})), + &json!({ + "caseId": case_id, + "communicationBody": "one more", + "uploadIds": [pending], + }), + )); + assert!(format!("{err:?}").contains("has not been completed")); + } + + #[tokio::test] + async fn dry_run_refuses_without_touching_state() { + let svc = service(); + let err = expect_err( + svc.handle(req( + "CreateCase", + json!({ + "subject": "s", + "communicationBody": "b", + "dryRun": true, + }), + )) + .await, + ); + assert!(format!("{err:?}").contains("DryRunOperationException")); + assert!(svc + .state + .read() + .get("000000000000") + .unwrap() + .cases + .is_empty()); + + // Trusted Advisor does not model the exception, so dryRun is ignored. + let resp = svc + .handle(req( + "DescribeTrustedAdvisorChecks", + json!({ "language": "en", "dryRun": true }), + )) + .await + .unwrap(); + assert!(resp.status.is_success()); + } + + #[test] + fn every_supported_action_dispatches() { + let svc = service(); + for action in SUPPORT_ACTIONS { + let result = svc.dispatch(action, &req(action, json!({}))); + // Every action must be routed; a missing dispatch arm surfaces as + // the catch-all error rather than a domain error. + if let Err(err) = result { + assert!( + !matches!(err, AwsServiceError::ActionNotImplemented { .. }), + "{action} has no dispatch arm" + ); + } + } + } + #[test] fn describe_cases_paginates() { let svc = service(); diff --git a/crates/fakecloud-support/src/shared.rs b/crates/fakecloud-support/src/shared.rs index 820d7afb8..3076f896f 100644 --- a/crates/fakecloud-support/src/shared.rs +++ b/crates/fakecloud-support/src/shared.rs @@ -1,7 +1,9 @@ //! Primitives shared across the AWS Support handlers: id synthesis, the -//! submitter identity, and ISO-8601 timestamps. Kept in one place so the -//! create / describe paths cannot diverge on wire format. +//! submitter identity, ISO-8601 timestamps, and the presigned attachment +//! links. Kept in one place so the create / describe paths cannot diverge on +//! wire format. +use md5::{Digest, Md5}; use serde_json::Value; /// Current time as an ISO-8601 UTC string with millisecond precision, e.g. @@ -61,7 +63,138 @@ pub fn new_attachment_id() -> String { format!("attachment-{}", hex32()) } +/// A fresh attachment-upload id (`upload-{32 hex}`). +pub fn new_upload_id() -> String { + format!("upload-{}", hex32()) +} + +/// A fresh 64-hex `X-Amz-Signature` for a presigned attachment link. The +/// signature is recorded in state when the link is issued and checked by the +/// data-plane route, so an unissued or tampered link is rejected the same way +/// a bad SigV4 signature would be. +pub fn new_signature() -> String { + format!("{}{}", hex32(), hex32()) +} + +/// How long a presigned attachment link stays valid. AWS gives attachment +/// links an hour, the same lifetime as an attachment set. +pub const LINK_TTL_SECONDS: i64 = 3600; + +/// Default part size for a multipart attachment upload: 5 MiB, S3's minimum +/// part size and the value the Support console uses. +pub const DEFAULT_PART_SIZE_BYTES: i64 = 5 * 1024 * 1024; + +/// The `X-Amz-Credential` access-key id used when the caller presented none +/// (unsigned requests are accepted by default). Matches AWS's documented +/// example key so the link keeps a realistic shape. +pub const EXAMPLE_ACCESS_KEY_ID: &str = "AKIAIOSFODNN7EXAMPLE"; + +/// An ISO-8601 UTC timestamp `seconds` from now, in the same format as +/// [`iso_now`]. +pub fn iso_in(seconds: i64) -> String { + (chrono::Utc::now() + chrono::Duration::seconds(seconds)) + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string() +} + +/// Path of the presigned `PUT` link for one part of an attachment upload. +pub fn upload_part_path(account_id: &str, upload_id: &str, part_index: i64) -> String { + format!("/_fakecloud/support/attachments/uploads/{account_id}/{upload_id}/{part_index}") +} + +/// Path of the presigned `GET` link for a stored attachment. +pub fn attachment_download_path(account_id: &str, attachment_id: &str) -> String { + format!("/_fakecloud/support/attachments/downloads/{account_id}/{attachment_id}") +} + +/// Build a SigV4-shaped presigned URL for `path` against this fakecloud's own +/// endpoint. The query string carries the same parameters a real presigned S3 +/// attachment link does, so a client that simply follows the URL works +/// unchanged; `signature` is the opaque value recorded in state for this link. +pub fn presigned_url( + endpoint: &str, + path: &str, + region: &str, + access_key_id: &str, + signature: &str, + expires_in: i64, +) -> String { + let now = chrono::Utc::now(); + let date = now.format("%Y%m%d"); + let amz_date = now.format("%Y%m%dT%H%M%SZ"); + let endpoint = endpoint.trim_end_matches('/'); + format!( + "{endpoint}{path}?X-Amz-Algorithm=AWS4-HMAC-SHA256\ + &X-Amz-Credential={access_key_id}%2F{date}%2F{region}%2Fsupport%2Faws4_request\ + &X-Amz-Date={amz_date}&X-Amz-Expires={expires_in}\ + &X-Amz-SignedHeaders=host&X-Amz-Signature={signature}" + ) +} + +/// The `ETag` a part upload reports back, matching S3's quoted 32-hex MD5. +pub fn part_etag(bytes: &[u8]) -> String { + format!("\"{:x}\"", Md5::digest(bytes)) +} + /// Read a string member from a request body. pub fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> { body.get(name).and_then(Value::as_str) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ids_have_aws_shapes() { + assert!(new_upload_id().starts_with("upload-")); + assert_eq!(new_upload_id().len(), "upload-".len() + 32); + assert_eq!(new_signature().len(), 64); + assert_ne!(new_signature(), new_signature()); + } + + #[test] + fn presigned_url_carries_the_sigv4_query() { + let url = presigned_url( + "http://localhost:4566/", + "/_fakecloud/support/attachments/downloads/000000000000/attachment-1", + "us-east-1", + EXAMPLE_ACCESS_KEY_ID, + "abc123", + LINK_TTL_SECONDS, + ); + assert!( + url.starts_with( + "http://localhost:4566/_fakecloud/support/attachments/downloads/000000000000/attachment-1?" + ), + "{url}" + ); + assert!(url.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256"), "{url}"); + assert!( + url.contains(&format!("{EXAMPLE_ACCESS_KEY_ID}%2F")), + "{url}" + ); + assert!( + url.contains("%2Fus-east-1%2Fsupport%2Faws4_request"), + "{url}" + ); + assert!(url.contains("X-Amz-Expires=3600"), "{url}"); + assert!(url.ends_with("X-Amz-Signature=abc123"), "{url}"); + // No doubled slash from the trailing slash on the endpoint. + assert!(!url.contains("4566//"), "{url}"); + } + + #[test] + fn part_etag_matches_s3() { + // S3 reports a single-part ETag as the quoted MD5 of the bytes. + assert_eq!(part_etag(b"hello"), "\"5d41402abc4b2a76b9719d911017c592\""); + } + + #[test] + fn iso_in_is_in_the_future_and_comparable() { + let now = iso_now(); + let later = iso_in(LINK_TTL_SECONDS); + assert!(later > now, "{later} vs {now}"); + assert!(later.ends_with('Z')); + } +} diff --git a/crates/fakecloud-support/src/state.rs b/crates/fakecloud-support/src/state.rs index 84d1c4916..55143c1c1 100644 --- a/crates/fakecloud-support/src/state.rs +++ b/crates/fakecloud-support/src/state.rs @@ -11,6 +11,15 @@ //! (`none -> enqueued -> processing -> success`) stored in `ta_refresh`; //! `RefreshTrustedAdvisorCheck` enqueues and each //! `DescribeTrustedAdvisorCheckRefreshStatuses` read advances it one step. +//! +//! The presigned attachment-upload flow (`GetAttachmentUploadLinks` -> +//! `PUT` each part -> `CompleteAttachmentUpload`) keeps its own bookkeeping in +//! [`AttachmentUpload`]: one record per `uploadId` holding the issued part +//! links (each with its own signature and expiry), the bytes and `ETag` +//! actually uploaded to each link, and the terminal upload status. Download +//! grants minted by `GetAttachmentDownloadLink` live in `attachment_downloads` +//! keyed by the URL signature, so the data-plane route can authorise a +//! download without re-deriving anything. use std::collections::BTreeMap; use std::sync::Arc; @@ -23,6 +32,92 @@ use fakecloud_core::multi_account::{AccountState, MultiAccountState}; pub const SUPPORT_SNAPSHOT_SCHEMA_VERSION: u32 = 1; +/// The upload is still accepting parts; `CompleteAttachmentUpload` has not +/// succeeded yet. +pub const UPLOAD_NOT_READY: &str = "attachment-not-ready"; +/// Every part was uploaded and `CompleteAttachmentUpload` assembled the +/// attachment; it is retrievable with `DescribeAttachment`. +pub const UPLOAD_READY: &str = "attachment-ready"; +/// The upload can no longer be completed (its links expired). +pub const UPLOAD_FAILED: &str = "failed"; + +/// One part link issued by `GetAttachmentUploadLinks`, plus whatever the +/// client has since `PUT` to it. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadPart { + /// 1-based part index, matching `UploadUrl.partIndex`. + pub part_index: i64, + /// The `X-Amz-Signature` embedded in this part's presigned URL. The + /// data-plane route accepts a `PUT` only when it presents this value. + pub signature: String, + /// ISO-8601 instant after which the link stops working. + pub expiry: String, + /// The `ETag` returned for the uploaded bytes, `None` until the part is + /// uploaded. + #[serde(default)] + pub etag: Option, + /// The uploaded bytes, base64-encoded, `None` until the part is uploaded. + #[serde(default)] + pub data: Option, +} + +/// A multipart attachment upload keyed by its `uploadId`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AttachmentUpload { + pub upload_id: String, + pub file_name: String, + /// Size the client declared in `GetAttachmentUploadLinks`; `0` when it did + /// not declare one (a single-part upload of unknown length). + #[serde(default)] + pub file_size_bytes: i64, + pub part_size_bytes: i64, + pub total_parts: i64, + /// One of [`UPLOAD_NOT_READY`], [`UPLOAD_READY`], [`UPLOAD_FAILED`]. + pub status: String, + /// ISO-8601 instant after which the upload can no longer be completed. + pub expiry: String, + /// Issued part links, ordered by `part_index`. + #[serde(default)] + pub parts: Vec, + /// Set once `CompleteAttachmentUpload` assembled the attachment. + #[serde(default)] + pub attachment_id: Option, +} + +impl AttachmentUpload { + /// The issued link for `part_index`, if one was ever handed out. + pub fn part(&self, part_index: i64) -> Option<&UploadPart> { + self.parts.iter().find(|p| p.part_index == part_index) + } + + /// Mutable access to the issued link for `part_index`. + pub fn part_mut(&mut self, part_index: i64) -> Option<&mut UploadPart> { + self.parts.iter_mut().find(|p| p.part_index == part_index) + } + + /// How many parts have been uploaded so far. + pub fn completed_parts(&self) -> i64 { + self.parts.iter().filter(|p| p.data.is_some()).count() as i64 + } + + /// The lowest part index that has not been uploaded yet, or `0` when every + /// part is in (matching the `nextIndex` sentinel the model defaults to). + pub fn next_index(&self) -> i64 { + (1..=self.total_parts) + .find(|i| self.part(*i).map(|p| p.data.is_none()).unwrap_or(true)) + .unwrap_or(0) + } +} + +/// A presigned download link minted by `GetAttachmentDownloadLink`, keyed in +/// state by the URL signature. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DownloadGrant { + pub attachment_id: String, + /// ISO-8601 instant after which the link stops working. + pub expiry: String, +} + /// Per-account AWS Support state. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SupportData { @@ -32,6 +127,12 @@ pub struct SupportData { /// The region this partition belongs to. #[serde(default)] pub region: String, + /// The server's own endpoint (e.g. `http://localhost:4566`), used to build + /// presigned attachment upload / download links that point back at this + /// fakecloud. Empty on snapshots written before the upload flow existed; + /// callers fall back to the request's `Host`. + #[serde(default)] + pub endpoint: String, /// Support cases keyed by `caseId`; each value is the `CaseDetails` wire /// object. @@ -51,6 +152,13 @@ pub struct SupportData { #[serde(default)] pub attachments: BTreeMap, + /// Multipart attachment uploads keyed by `uploadId`. + #[serde(default)] + pub attachment_uploads: BTreeMap, + /// Presigned download grants keyed by the URL's `X-Amz-Signature`. + #[serde(default)] + pub attachment_downloads: BTreeMap, + /// Trusted Advisor per-check refresh status keyed by `checkId`; one of /// `none` / `enqueued` / `processing` / `success`. #[serde(default)] @@ -58,22 +166,38 @@ pub struct SupportData { } impl AccountState for SupportData { - fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self { + fn new_for_account(account_id: &str, region: &str, endpoint: &str) -> Self { Self { account_id: account_id.to_string(), region: region.to_string(), + endpoint: endpoint.to_string(), ..Default::default() } } } impl SupportData { - /// There is no timer-driven lifecycle to resume: the Trusted Advisor - /// refresh state machine advances only on an explicit read, so a restart - /// leaves it exactly as persisted. Kept for symmetry with the other - /// services' persistence hook. Always returns `false` (nothing settled). + /// The Trusted Advisor refresh state machine advances only on an explicit + /// read, so a restart leaves it exactly as persisted. Attachment uploads + /// do have a wall-clock lifecycle: their presigned links expire, and an + /// upload whose links expired before it was completed can never be + /// completed. Sweep those to `failed` on load so a restart does not + /// resurrect an upload the client can no longer finish. Returns `true` + /// when anything settled. pub fn reconcile(&mut self) -> bool { - false + let now = crate::shared::iso_now(); + let mut changed = false; + for upload in self.attachment_uploads.values_mut() { + if upload.status == UPLOAD_NOT_READY && upload.expiry <= now { + upload.status = UPLOAD_FAILED.to_string(); + changed = true; + } + } + let grants_before = self.attachment_downloads.len(); + self.attachment_downloads + .retain(|_, grant| grant.expiry > now); + changed |= self.attachment_downloads.len() != grants_before; + changed } /// Advance one check's refresh status one step toward `success`, returning @@ -126,8 +250,87 @@ mod tests { } #[test] - fn reconcile_is_noop() { + fn reconcile_is_noop_without_uploads() { let mut d = data(); assert!(!d.reconcile()); } + + fn upload(expiry: &str, status: &str) -> AttachmentUpload { + AttachmentUpload { + upload_id: "upload-1".into(), + file_name: "log.txt".into(), + file_size_bytes: 4, + part_size_bytes: 5 * 1024 * 1024, + total_parts: 2, + status: status.into(), + expiry: expiry.into(), + parts: vec![ + UploadPart { + part_index: 1, + signature: "sig1".into(), + expiry: expiry.into(), + etag: Some("\"abc\"".into()), + data: Some("aGk=".into()), + }, + UploadPart { + part_index: 2, + signature: "sig2".into(), + expiry: expiry.into(), + etag: None, + data: None, + }, + ], + attachment_id: None, + } + } + + #[test] + fn reconcile_fails_expired_uploads_and_prunes_grants() { + let mut d = data(); + d.attachment_uploads.insert( + "upload-1".into(), + upload("2000-01-01T00:00:00.000Z", UPLOAD_NOT_READY), + ); + d.attachment_downloads.insert( + "sig".into(), + DownloadGrant { + attachment_id: "attachment-1".into(), + expiry: "2000-01-01T00:00:00.000Z".into(), + }, + ); + assert!(d.reconcile()); + assert_eq!(d.attachment_uploads["upload-1"].status, UPLOAD_FAILED); + assert!(d.attachment_downloads.is_empty()); + } + + #[test] + fn reconcile_leaves_live_uploads_alone() { + let mut d = data(); + d.attachment_uploads.insert( + "upload-1".into(), + upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY), + ); + assert!(!d.reconcile()); + assert_eq!(d.attachment_uploads["upload-1"].status, UPLOAD_NOT_READY); + } + + #[test] + fn upload_tracks_part_progress() { + let u = upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY); + assert_eq!(u.completed_parts(), 1); + // Part 1 is in, so part 2 is the next one the client should upload. + assert_eq!(u.next_index(), 2); + assert_eq!(u.part(1).unwrap().signature, "sig1"); + assert!(u.part(3).is_none()); + } + + #[test] + fn fully_uploaded_has_no_next_index() { + let mut u = upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY); + let part = u.part_mut(2).unwrap(); + part.data = Some("eA==".into()); + part.etag = Some("\"def\"".into()); + assert_eq!(u.completed_parts(), 2); + assert_eq!(u.next_index(), 0); + } } diff --git a/crates/fakecloud-support/src/validate.rs b/crates/fakecloud-support/src/validate.rs index 127de7c40..eb783a92c 100644 --- a/crates/fakecloud-support/src/validate.rs +++ b/crates/fakecloud-support/src/validate.rs @@ -24,7 +24,7 @@ const MODEL_JSON: &str = include_str!("../model.json"); /// The embedded model parses to exactly this many operations; a mismatch means /// the vendored model drifted from the implemented surface. #[cfg(test)] -pub const OPERATION_COUNT: usize = 16; +pub const OPERATION_COUNT: usize = 20; #[derive(Debug, Default)] struct MemberConstraint { @@ -292,6 +292,40 @@ mod tests { assert!(err.contains("checkId")); } + #[test] + fn upload_links_require_a_file_name() { + let err = validate_input("GetAttachmentUploadLinks", &json!({})).unwrap_err(); + assert!(err.contains("fileName")); + } + + #[test] + fn complete_upload_requires_the_completed_parts() { + let err = validate_input( + "CompleteAttachmentUpload", + &json!({ "uploadId": "upload-1" }), + ) + .unwrap_err(); + assert!(err.contains("completedUploads")); + } + + #[test] + fn upload_status_requires_an_upload_id() { + let err = validate_input("DescribeAttachmentUploadStatus", &json!({})).unwrap_err(); + assert!(err.contains("uploadId")); + } + + #[test] + fn upload_links_reject_an_out_of_range_file_size() { + // FileSize is modelled as 1..=157286400, so zero is refused before any + // handler runs. + let err = validate_input( + "GetAttachmentUploadLinks", + &json!({ "fileName": "log.txt", "fileSizeBytes": 0 }), + ) + .unwrap_err(); + assert!(err.contains("fileSizeBytes")); + } + #[test] fn describe_severity_levels_needs_nothing() { assert!(validate_input("DescribeSeverityLevels", &json!({})).is_ok()); diff --git a/crates/fakecloud-transcribe/model.json b/crates/fakecloud-transcribe/model.json index e26c4d7d8..6956d3cb0 100644 --- a/crates/fakecloud-transcribe/model.json +++ b/crates/fakecloud-transcribe/model.json @@ -5323,6 +5323,114 @@ "smithy.api#enumValue": "SSN" } }, + "DATE_TIME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DATE_TIME" + } + }, + "PASSPORT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PASSPORT_NUMBER" + } + }, + "DRIVER_ID": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "DRIVER_ID" + } + }, + "URL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "URL" + } + }, + "AGE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AGE" + } + }, + "USERNAME": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "USERNAME" + } + }, + "PASSWORD": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "PASSWORD" + } + }, + "AWS_ACCESS_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_ACCESS_KEY" + } + }, + "AWS_SECRET_KEY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "AWS_SECRET_KEY" + } + }, + "IP_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "IP_ADDRESS" + } + }, + "MAC_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "MAC_ADDRESS" + } + }, + "LICENSE_PLATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "LICENSE_PLATE" + } + }, + "VEHICLE_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "VEHICLE_IDENTIFICATION_NUMBER" + } + }, + "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER" + } + }, + "CA_HEALTH_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_HEALTH_NUMBER" + } + }, + "CA_SOCIAL_INSURANCE_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "CA_SOCIAL_INSURANCE_NUMBER" + } + }, + "INTERNATIONAL_BANK_ACCOUNT_NUMBER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "INTERNATIONAL_BANK_ACCOUNT_NUMBER" + } + }, + "SWIFT_CODE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "SWIFT_CODE" + } + }, "ALL": { "target": "smithy.api#Unit", "traits": { @@ -5339,7 +5447,7 @@ "traits": { "smithy.api#length": { "min": 0, - "max": 11 + "max": 29 } } }, diff --git a/website/content/docs/about/conformance.md b/website/content/docs/about/conformance.md index 437a7210b..43e158531 100644 --- a/website/content/docs/about/conformance.md +++ b/website/content/docs/about/conformance.md @@ -35,7 +35,7 @@ Every response is validated against the operation's Smithy output shape. Missing ## Current coverage -248,557/248,557 generated test variants across all 105 services (7,491 operations) pass on every commit — true 100% conformance with no flake margin and no skipped services. The exact pass/total per service is checked into [`conformance-baseline.json`](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). +248,557/248,557 generated test variants across all 105 services (7,505 operations) pass on every commit — true 100% conformance with no flake margin and no skipped services. The exact pass/total per service is checked into [`conformance-baseline.json`](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). See the harness and methodology at [`crates/fakecloud-conformance/`](https://github.com/faiscadev/fakecloud/tree/main/crates/fakecloud-conformance). diff --git a/website/content/docs/migration-from-localstack.md b/website/content/docs/migration-from-localstack.md index 6cc8c7c35..e99c5e164 100644 --- a/website/content/docs/migration-from-localstack.md +++ b/website/content/docs/migration-from-localstack.md @@ -9,7 +9,7 @@ If your local development workflow is blocked by LocalStack's account requiremen ## Key Differences - **No API Key**: fakecloud is fully functional offline. No `ACTIVATE_PRO` or account login required. - **Single Binary**: Replace heavy Docker-in-Docker setups with a ~19MB binary that starts in <300ms. -- **Parity**: 100% API conformance across 7,491 operations, including features LocalStack gates behind Pro (like ECR and Bedrock). +- **Parity**: 100% API conformance across 7,505 operations, including features LocalStack gates behind Pro (like ECR and Bedrock). ## Service Mapping | Feature | LocalStack | fakecloud | diff --git a/website/content/docs/operations/_index.md b/website/content/docs/operations/_index.md index 3dd3a621b..d4e0b68ce 100644 --- a/website/content/docs/operations/_index.md +++ b/website/content/docs/operations/_index.md @@ -1099,6 +1099,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s ## [SES](@/docs/services/ses.md) +- `AssociateEmailIdentityCertificate` - `BatchGetMetricData` - `CancelExportJob` - `CreateConfigurationSet` @@ -1129,6 +1130,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `DeleteSuppressedDestination` - `DeleteTenant` - `DeleteTenantResourceAssociation` +- `DisassociateEmailIdentityCertificate` - `GetAccount` - `GetBlacklistReports` - `GetConfigurationSet` @@ -1162,6 +1164,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `ListDeliverabilityTestReports` - `ListDomainDeliverabilityCampaigns` - `ListEmailIdentities` +- `ListEmailIdentityCertificates` - `ListEmailTemplates` - `ListExportJobs` - `ListImportJobs` @@ -1203,6 +1206,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `TagResource` - `TestRenderEmailTemplate` - `UntagResource` +- `UpdateConfigurationSet` - `UpdateConfigurationSetEventDestination` - `UpdateContact` - `UpdateContactList` @@ -1376,12 +1380,15 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s ## [Kinesis](@/docs/services/kinesis.md) - `AddTagsToStream` +- `CreateChannel` - `CreateStream` - `DecreaseStreamRetentionPeriod` +- `DeleteChannel` - `DeleteResourcePolicy` - `DeleteStream` - `DeregisterStreamConsumer` - `DescribeAccountSettings` +- `DescribeChannel` - `DescribeLimits` - `DescribeStream` - `DescribeStreamConsumer` @@ -1392,6 +1399,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `GetResourcePolicy` - `GetShardIterator` - `IncreaseStreamRetentionPeriod` +- `ListChannels` - `ListShards` - `ListStreamConsumers` - `ListStreams` @@ -1410,6 +1418,7 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `TagResource` - `UntagResource` - `UpdateAccountSettings` +- `UpdateChannel` - `UpdateMaxRecordSize` - `UpdateShardCount` - `UpdateStreamMode` @@ -7047,8 +7056,10 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `AddAttachmentsToSet` - `AddCommunicationToCase` +- `CompleteAttachmentUpload` - `CreateCase` - `DescribeAttachment` +- `DescribeAttachmentUploadStatus` - `DescribeCases` - `DescribeCommunications` - `DescribeCreateCaseOptions` @@ -7059,6 +7070,8 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `DescribeTrustedAdvisorCheckResult` - `DescribeTrustedAdvisorCheckSummaries` - `DescribeTrustedAdvisorChecks` +- `GetAttachmentDownloadLink` +- `GetAttachmentUploadLinks` - `RefreshTrustedAdvisorCheck` - `ResolveCase` @@ -7930,5 +7943,6 @@ This is a surface listing, not an implementation manifest. For fakecloud's per-s - `UpdateInterruptibleCapacityReservationAllocation` - `UpdateSecurityGroupRuleDescriptionsEgress` - `UpdateSecurityGroupRuleDescriptionsIngress` +- `ValidateSecurityGroupQuotasForInterface` - `WithdrawByoipCidr` diff --git a/website/content/docs/parity.md b/website/content/docs/parity.md index 5f7542764..e706482c0 100644 --- a/website/content/docs/parity.md +++ b/website/content/docs/parity.md @@ -4,7 +4,7 @@ description = "Service-by-service behavior parity: what is real, what is synthes weight = 1 +++ -fakecloud implements **105 AWS services** with **7,491 operations**. **248,557/248,557 generated Smithy conformance variants pass** on every commit — true 100% across every implemented service, no flake margin and no skipped services. Conformance checks request/response shapes, field names, and error codes against [AWS's own Smithy models](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). Behavior parity varies by service — some run real infrastructure (Postgres, Redis, Docker containers), some run a real control plane but return synthesized data for complex queries, and a few have control-plane-only coverage with no data-plane enforcement. +fakecloud implements **105 AWS services** with **7,505 operations**. **248,557/248,557 generated Smithy conformance variants pass** on every commit — true 100% across every implemented service, no flake margin and no skipped services. Conformance checks request/response shapes, field names, and error codes against [AWS's own Smithy models](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). Behavior parity varies by service — some run real infrastructure (Postgres, Redis, Docker containers), some run a real control plane but return synthesized data for complex queries, and a few have control-plane-only coverage with no data-plane enforcement. | Service | Ops | Protocol | Control plane | Data plane | Known limitations | | --- | --- | --- | --- | --- | --- | @@ -24,10 +24,10 @@ fakecloud implements **105 AWS services** with **7,491 operations**. **248,557/2 | [KMS](@/docs/services/kms.md) | 53 | JSON 1.1 | Full | Full | Real ECDSA P-256, P-384, and P-521 signing. | | [CloudFormation](@/docs/services/cloudformation.md) | 90 | JSON 1.1 (Query) | Full | Full | Custom resources execute real Lambda-backed custom resource providers. | | [Cloud Control API](@/docs/services/cloudcontrol.md) | 8 | JSON 1.0 | Full | Full | `CreateResource`, `UpdateResource`, and `DeleteResource` drive the same CloudFormation resource provisioners (including container-backed resources), so a resource created through Cloud Control is real, not a parallel fake. `UpdateResource` applies the full RFC 6902 JSON Patch operation set (`add`/`remove`/`replace`/`move`/`copy`/`test`) to desired state. `ClientToken` idempotency replays the original terminal `ProgressEvent`. Request tracking (`GetResourceRequestStatus`, `ListResourceRequests`, `CancelResourceRequest`) records every mutating request. State is account-partitioned and persists across restarts in persistent mode. | -| [SES](@/docs/services/ses.md) | 112 | JSON 1.1 | Full | Full | v2 sending + v1 inbound receipt rules are both real. DKIM signing is real. `GetMessageInsights` returns real delivery tracking data. Bounce simulator addresses are available for testing. SMTP credential issuance is implemented via IAM service-specific credentials, and an opt-in SMTP submission listener (`FAKECLOUD_SES_SMTP_PORT`) accepts mail authenticated with those credentials. Outbound SMTP relay is supported when `FAKECLOUD_SMTP_RELAY_*` env is configured. | +| [SES](@/docs/services/ses.md) | 116 | JSON 1.1 | Full | Full | v2 sending + v1 inbound receipt rules are both real. DKIM signing is real. `GetMessageInsights` returns real delivery tracking data. Bounce simulator addresses are available for testing. SMTP credential issuance is implemented via IAM service-specific credentials, and an opt-in SMTP submission listener (`FAKECLOUD_SES_SMTP_PORT`) accepts mail authenticated with those credentials. Outbound SMTP relay is supported when `FAKECLOUD_SMTP_RELAY_*` env is configured. | | [Cognito User Pools](@/docs/services/cognito.md) | 132 | JSON 1.1 | Full | Full | Real RSA-2048 RS256 JWT signing. JWKS + OIDC discovery endpoints serve real JWKs. `/oauth2/token`, `/oauth2/authorize`, `/oauth2/userInfo`, and `/oauth2/revoke` are all implemented. Refresh token rotation is supported when enabled. `PreTokenGeneration` trigger invokes the configured Lambda and merges claims. `CompromisedCredentialsRiskConfiguration` is enforced. WebAuthn `packed` attestation format is verified. `GetSigningCertificate` returns real X.509 certificates. | | [Cognito Identity](@/docs/services/cognito.md) | 23 | JSON 1.1 | Full | Full | Identity pools, federated identities, developer identities, and real STS-style credential issuance are implemented. | -| [Kinesis](@/docs/services/kinesis.md) | 39 | JSON 1.1 | Full | Full | — | +| [Kinesis](@/docs/services/kinesis.md) | 44 | JSON 1.1 | Full | Full | — | | [RDS](@/docs/services/rds.md) | 163 | JSON 1.1 (Query) | Full | Full | Real Postgres, MySQL, MariaDB, Oracle, SQL Server, and Db2 via Docker. PostgreSQL `aws_lambda` + `aws_s3` extensions and Aurora-compatible MySQL/MariaDB `mysql.lambda_async`/`mysql.lambda_sync` invoke fakecloud Lambda and import/export S3 objects from SQL. | | [DocumentDB](@/docs/services/docdb.md) | 55 | Query (XML) | Full | None | RDS-shaped Query API served on the shared `rds` SigV4 scope (the DocumentDB SDK is disambiguated from RDS by its `api/docdb` user-agent token). Full 55-op control plane: DB clusters (writer + reader endpoints, `cluster-XXXX` resource ids, ARNs), instances that attach to a cluster as writer/reader members, cluster snapshots with copy / restore-from-snapshot / point-in-time restore, cluster parameter groups (values round-trip), subnet groups, global clusters, event subscriptions, pending-maintenance / certificate / engine-version / orderable-option catalog ops, and tagging. Account-partitioned and persisted. No data plane: fakecloud ships no MongoDB-compatible DocumentDB engine image (unlike RDS's real Postgres containers), so clusters and instances are control-plane records with well-formed endpoints that accept no wire connections. | | [Neptune](@/docs/services/neptune.md) | 70 | Query (XML) | Full | None | RDS-shaped Query API served on the shared `rds` SigV4 scope (the Neptune SDK is disambiguated from RDS by its `api/neptune` user-agent token). Full 70-op control plane: DB clusters (writer + reader endpoints, `cluster-XXXX` resource ids, ARNs, promote-read-replica), instances that attach to a cluster as writer/reader members, custom/reader cluster endpoints, IAM role associations, cluster snapshots with copy / restore-from-snapshot / point-in-time restore, cluster + DB parameter groups (values round-trip), subnet groups, global clusters, event subscriptions, pending-maintenance / engine-version / orderable-option catalog ops, and tagging. Account-partitioned and persisted. No data plane: fakecloud ships no Neptune (Gremlin/SPARQL) graph engine image (unlike RDS's real Postgres containers), so clusters and instances are control-plane records with well-formed endpoints that accept no wire connections. | @@ -79,7 +79,7 @@ fakecloud implements **105 AWS services** with **7,491 operations**. **248,557/2 | [Verified Permissions](@/docs/services/verifiedpermissions.md) | 34 | JSON 1.0 | Full | Full | Complete 34-op Cedar authorization control plane: policy stores, Cedar schemas (`PutSchema`/`GetSchema`), static and template-linked policies, policy templates, identity sources (Cognito/OIDC), policy-store aliases, and tagging. `IsAuthorized`/`IsAuthorizedWithToken`/`BatchIsAuthorized`/`BatchIsAuthorizedWithToken` compute **real Cedar decisions** via the official `cedar-policy` engine — the store's policies are compiled into a Cedar `PolicySet`, the request principal/action/resource/context/entities are translated to Cedar values, and the `ALLOW`/`DENY` decision, determining policies and evaluation errors are returned. `*WithToken` resolves the principal from the JWT `sub` claim per the identity source. `@length`/`@range`/enum constraints enforced. Account-partitioned and persisted. | | [Step Functions](@/docs/services/stepfunctions.md) | 37 | JSON 1.1 | Full | Full | Full ASL interpreter with `.sync` wait patterns, `waitForTaskToken`, and generic `aws-sdk:*` integrations. | | [Amazon SWF](@/docs/services/swf.md) | 39 | JSON 1.0 | Full | Full (state machine) | Full 39-op Simple Workflow Service surface: domains (`Register`/`Deprecate`/`Undeprecate`/`Describe`/`ListDomains`, `REGISTERED`/`DEPRECATED` status, `/domain/` ARNs), versioned activity + workflow types (`Register`/`Deprecate`/`Undeprecate`/`Delete`/`Describe`/`List`, `default*` configuration echoed back, `Delete` requires prior deprecation), and workflow executions driven by a real decider/worker state machine — `StartWorkflowExecution` mints a `runId` and seeds `WorkflowExecutionStarted` + `DecisionTaskScheduled`; `PollForDecisionTask` returns the next decision task with the full history (appending `DecisionTaskStarted`); `RespondDecisionTaskCompleted` applies decisions (`ScheduleActivityTask`, `CompleteWorkflowExecution`, `Fail`/`Cancel`/`ContinueAsNew`, `RecordMarker`, `StartTimer`/`CancelTimer`, `RequestCancelActivityTask`, signal/child/lambda) into the right history events; `PollForActivityTask` hands out the scheduled activity; `RespondActivityTask{Completed,Failed,Canceled}` records the outcome and schedules the next decision task; `RecordActivityTaskHeartbeat`. Plus `Describe`/`GetHistory`/`List`+`CountOpen`/`ClosedWorkflowExecutions`, `CountPending{Activity,Decision}Tasks`, `Signal`/`RequestCancel`/`TerminateWorkflowExecution`, and ARN-keyed domain tagging. Model-driven `required`/`length`/`range`/`enum` validation with SWF's declared faults (`UnknownResourceFault`/`DomainAlreadyExistsFault`/`TypeAlreadyExistsFault`/`TypeDeprecatedFault`/`TypeNotDeprecatedFault`/`DefaultUndefinedFault`/`WorkflowExecutionAlreadyStartedFault`). Account-partitioned and persisted. Honest gap: no autonomous clock fires timer/task/execution timeouts — those transitions are decider/worker-driven. | -| [AWS Support](@/docs/services/support.md) | 16 | JSON 1.1 | Full | None (TA/agent gap) | Full 16-op Support surface: the support-case API (`CreateCase` mints an AWS-shaped `case-{account}-{year}-{hex}` id + numeric `displayId`, opens `opened`, and seeds the communication thread; `DescribeCases` filters by case-id list / `displayId` / time window / `includeResolvedCases` / `includeCommunications` / language with a round-tripping `nextToken`; `AddCommunicationToCase` appends and returns `result: true`; `DescribeCommunications` pages the thread; `ResolveCase` returns `initialCaseStatus` + `finalCaseStatus` (`resolved`)); attachment sets (`AddAttachmentsToSet` mints/extends an `attachmentSetId` with an `expiryTime`; `DescribeAttachment` returns a stored attachment); the severity levels (`DescribeSeverityLevels`) and service/category catalogues (`DescribeServices` / `DescribeCreateCaseOptions` / `DescribeSupportedLanguages`); and the Trusted Advisor API (`DescribeTrustedAdvisorChecks` returns the vendored check catalogue, `DescribeTrustedAdvisorCheckResult` / `DescribeTrustedAdvisorCheckSummaries` return well-formed all-clear results, and `RefreshTrustedAdvisorCheck` + `DescribeTrustedAdvisorCheckRefreshStatuses` drive a real per-check `none` -> `enqueued` -> `processing` -> `success` refresh state machine). Model-driven `required`/`length`/`range` validation with Support's declared exceptions (`CaseIdNotFound` / `AttachmentIdNotFound` / `AttachmentSetIdNotFound`). Account-partitioned and persisted. Honest gap: no Trusted Advisor analysis engine runs and no live support agent replies, so check results report zero flagged resources and cases receive no automated agent response. | +| [AWS Support](@/docs/services/support.md) | 20 | JSON 1.1 | Full | None (TA/agent gap) | Full 20-op Support surface: the support-case API (`CreateCase` mints an AWS-shaped `case-{account}-{year}-{hex}` id + numeric `displayId`, opens `opened`, and seeds the communication thread; `DescribeCases` filters by case-id list / `displayId` / time window / `includeResolvedCases` / `includeCommunications` / language with a round-tripping `nextToken`; `AddCommunicationToCase` appends and returns `result: true`; `DescribeCommunications` pages the thread; `ResolveCase` returns `initialCaseStatus` + `finalCaseStatus` (`resolved`)); attachment sets (`AddAttachmentsToSet` mints/extends an `attachmentSetId` with an `expiryTime`; `DescribeAttachment` returns a stored attachment); presigned attachment uploads (`GetAttachmentUploadLinks` records an upload and issues one presigned `PUT` link per 5 MiB part pointing back at fakecloud, which serves them for real; `CompleteAttachmentUpload` verifies every part and its `ETag` before assembling the attachment; `DescribeAttachmentUploadStatus` reports the recorded progress; `GetAttachmentDownloadLink` issues a presigned `GET` link that serves the bytes back, and a completed upload attaches to a case through `uploadIds`); the severity levels (`DescribeSeverityLevels`) and service/category catalogues (`DescribeServices` / `DescribeCreateCaseOptions` / `DescribeSupportedLanguages`); and the Trusted Advisor API (`DescribeTrustedAdvisorChecks` returns the vendored check catalogue, `DescribeTrustedAdvisorCheckResult` / `DescribeTrustedAdvisorCheckSummaries` return well-formed all-clear results, and `RefreshTrustedAdvisorCheck` + `DescribeTrustedAdvisorCheckRefreshStatuses` drive a real per-check `none` -> `enqueued` -> `processing` -> `success` refresh state machine). Model-driven `required`/`length`/`range` validation with Support's declared exceptions (`CaseIdNotFound` / `AttachmentIdNotFound` / `AttachmentSetIdNotFound` / `UploadIdNotFound` / `DryRunOperationException`, with `dryRun` honoured on every operation that models it). Account-partitioned and persisted. Honest gap: no Trusted Advisor analysis engine runs and no live support agent replies, so check results report zero flagged resources and cases receive no automated agent response. | | [AWS Serverless Application Repository](@/docs/services/serverlessrepo.md) | 14 | REST-JSON | Full | Control plane | Complete 14-op AWS Serverless Application Repository control plane: applications (`CreateApplication` mints the `arn:aws:serverlessrepo:::applications/` ARN that doubles as the `applicationId`, stores author/description/name/homePageUrl/labels/license/readme/spdxLicenseId/sourceCodeUrl and — when a `semanticVersion` + template is supplied — seeds an initial version; `GetApplication` returns the app plus its `Version` block with `parameterDefinitions` parsed from the SAM/CloudFormation template, `requiredCapabilities`, and `resourcesSupported`, optionally pinned to a `semanticVersion`; `ListApplications` paginates with a round-tripping `nextToken`; `UpdateApplication` patches the mutable metadata; `DeleteApplication`); versions (`CreateApplicationVersion` — a `PUT` carrying the semantic version in the path — stores the template and parses `parameterDefinitions`/`requiredCapabilities`/`resourcesSupported`, plus `sourceCodeUrl`/`sourceCodeArchiveUrl`; `ListApplicationVersions`); sharing policy (`PutApplicationPolicy`/`GetApplicationPolicy` over principals/actions/`principalOrgIDs` statements each assigned a `statementId`; `UnshareApplication` removes an organisation share); CloudFormation templates (`CreateCloudFormationTemplate` mints a `templateId` + expiry and a `templateUrl` pointing back at the fakecloud host, status `PREPARING` settling to `ACTIVE` on the first `GetCloudFormationTemplate`); and `ListApplicationDependencies` (nested-application dependencies parsed from a template's `AWS::Serverless::Application` resources, paginated). Model-derived required/label validation with each op's declared `BadRequestException`/`NotFoundException`/`ConflictException`; account-partitioned and persisted. Honest gaps: `CreateCloudFormationChangeSet` mints well-formed `changeSetId`/`stackId` identifiers but does not drive the CloudFormation service to materialise a real stack (no clean in-process seam), and the returned `templateUrl` is a well-formed host-relative URL that fakecloud does not yet serve raw template bytes at. | | [API Gateway v1](@/docs/services/apigateway.md) | 124 | REST-JSON | Full | Full | Authorizer enforcement (TOKEN/REQUEST/COGNITO_USER_POOLS), request validators, VTL templates (MOCK and HTTP integrations), AWS direct service integrations, VPC_LINK integrations, and custom domain name + base path mapping routing are all implemented in the HTTP data plane. | | [API Gateway v2](@/docs/services/apigatewayv2.md) | 103 | JSON 1.1 | Full | Full | WebSocket support (`$connect`/`$disconnect`/`$default`), JWT and Lambda authorizer enforcement, AWS service integrations, access log delivery to CloudWatch Logs, stage variables, and custom domain routing are all implemented in the HTTP data plane. | @@ -104,7 +104,7 @@ fakecloud implements **105 AWS services** with **7,491 operations**. **248,557/2 | [Firehose](@/docs/services/firehose.md) | 12 | JSON 1.1 | Full | Full | Real S3 destination delivery with buffering hints honored. Other destinations (Redshift, OpenSearch, Splunk, HTTP endpoint) round-trip configuration. Server-side encryption (`Start`/`StopDeliveryStreamEncryption`) persists and surfaces in `DescribeDeliveryStream`. | | [Glue](@/docs/services/glue.md) | 299 | JSON 1.1 | Full | Partial | Full control plane: Data Catalog (databases, tables, partitions with `GetPartitions` `Expression` pruning), jobs, crawlers, classifiers, connections, triggers, workflows, blueprints, dev endpoints, schema registry, interactive sessions, ML transforms, data quality, user-defined functions, usage profiles, column statistics, and tagging. Status transitions are real (crawler `READY`↔`RUNNING`, trigger/workflow/run lifecycles). Job/crawler/Spark *execution* itself is synthesized — fakecloud is not a Spark engine. | | [Organizations](@/docs/services/organizations.md) | 63 | JSON 1.1 | Full | Full | Full org tree (roots, OUs, accounts), policies with SCP enforcement, handshakes, delegated administrators, service access, tagging, and a resource policy. Billing responsibility transfers ride handshake-backed records. `CreateAccount` transitions `IN_PROGRESS` -> `SUCCEEDED` after a short synthetic delay. | -| [EC2](@/docs/services/ec2.md) | 801 | ec2Query | Full | Partial | Full 786-op control plane: VPCs, subnets, security groups, route tables, gateways, ENIs, instances, EBS volumes/snapshots, AMIs (+ watermarks), network ACLs, VPC peering/endpoints, flow logs, launch templates, spot/fleet, capacity/reserved/dedicated hosts, transit gateways (+ multicast/peering/metering/policy-table entries), VPN + Client VPN, IPAM, Verified Access, Network Insights, Outpost/local-gateway/CoIP, and Instance Connect. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with the instance lifecycle mapped to the container lifecycle and `GetConsoleOutput` returning the container log; the control plane degrades to metadata-only when no container runtime is present. A few model ops absent from the vendored SDK are validated via raw ec2Query. | +| [EC2](@/docs/services/ec2.md) | 802 | ec2Query | Full | Partial | Full 786-op control plane: VPCs, subnets, security groups, route tables, gateways, ENIs, instances, EBS volumes/snapshots, AMIs (+ watermarks), network ACLs, VPC peering/endpoints, flow logs, launch templates, spot/fleet, capacity/reserved/dedicated hosts, transit gateways (+ multicast/peering/metering/policy-table entries), VPN + Client VPN, IPAM, Verified Access, Network Insights, Outpost/local-gateway/CoIP, and Instance Connect. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with the instance lifecycle mapped to the container lifecycle and `GetConsoleOutput` returning the container log; the control plane degrades to metadata-only when no container runtime is present. A few model ops absent from the vendored SDK are validated via raw ec2Query. | ## Reading the matrix diff --git a/website/content/docs/services/_index.md b/website/content/docs/services/_index.md index f05f4292b..26637833a 100644 --- a/website/content/docs/services/_index.md +++ b/website/content/docs/services/_index.md @@ -58,7 +58,7 @@ fakecloud implements 105 AWS services with 3,966 total operations. 248,557/248,5 | Verified Permissions | 34 | Complete Cedar authorization control plane: policy stores, schemas, static + template-linked policies, policy templates, identity sources, aliases, tagging; `IsAuthorized`/`*WithToken`/`Batch*` compute real Cedar decisions via the `cedar-policy` engine; persisted (all 34 ops) | | Step Functions | 37 | Full ASL interpreter, Lambda/SQS/SNS/EventBridge/DynamoDB tasks | | Amazon SWF | 39 | **Full 39-op control plane + decider/worker state machine.** Domains (`Register`/`Deprecate`/`Undeprecate`/`Describe`/`ListDomains`, `REGISTERED`/`DEPRECATED`, `/domain/` ARNs), versioned activity + workflow types (`Register`/`Deprecate`/`Undeprecate`/`Delete`/`Describe`/`List`, `default*` config echoed back, delete-requires-deprecate), and workflow executions with a real state machine: `StartWorkflowExecution` mints a `runId` and seeds `WorkflowExecutionStarted` + `DecisionTaskScheduled`; `PollForDecisionTask` returns the decision task with full history; `RespondDecisionTaskCompleted` applies `ScheduleActivityTask` / `CompleteWorkflowExecution` / fail / cancel / timer / marker / signal / child / lambda decisions into history events; `PollForActivityTask` + `RespondActivityTask{Completed,Failed,Canceled}` + `RecordActivityTaskHeartbeat` drive the worker side; `Describe`/`GetHistory`/`List`+`Count` open/closed executions and pending tasks; `Signal`/`RequestCancel`/`Terminate`; ARN-keyed tagging. Model-driven validation with SWF's declared faults (`UnknownResourceFault`/`DomainAlreadyExistsFault`/`TypeAlreadyExistsFault`/`DefaultUndefinedFault`/`WorkflowExecutionAlreadyStartedFault`). JSON 1.0 protocol. Account-partitioned and persisted. Honest gap: no autonomous clock fires timer/task/execution timeouts — those transitions are decider/worker-driven | -| AWS Support | 16 | **Full 16-op control plane.** Support-case API (`CreateCase` mints an AWS-shaped `case-{account}-{year}-{hex}` id + numeric `displayId`, opens `opened`, seeds the communication thread; `DescribeCases` filters by case-id list / `displayId` / time window / `includeResolvedCases` / `includeCommunications` / language with a round-tripping `nextToken`; `AddCommunicationToCase` -> `result: true`; `DescribeCommunications` pages the thread; `ResolveCase` -> `initialCaseStatus` + `finalCaseStatus`), attachment sets (`AddAttachmentsToSet` mints/extends an `attachmentSetId` + `expiryTime`; `DescribeAttachment`), severity levels + service/category catalogues (`DescribeSeverityLevels` / `DescribeServices` / `DescribeCreateCaseOptions` / `DescribeSupportedLanguages`), and the Trusted Advisor API (`DescribeTrustedAdvisorChecks` returns the vendored check catalogue; `DescribeTrustedAdvisorCheckResult` / `DescribeTrustedAdvisorCheckSummaries` return all-clear results; `RefreshTrustedAdvisorCheck` + `DescribeTrustedAdvisorCheckRefreshStatuses` drive a real `none` -> `enqueued` -> `processing` -> `success` refresh state machine). Model-driven validation with Support's declared exceptions (`CaseIdNotFound`/`AttachmentIdNotFound`/`AttachmentSetIdNotFound`). JSON 1.1 protocol. Account-partitioned and persisted. Honest gap: no Trusted Advisor analysis engine and no live support agent, so results report zero flagged resources and cases receive no automated agent reply | +| AWS Support | 20 | **Full 20-op control plane.** Support-case API (`CreateCase` mints an AWS-shaped `case-{account}-{year}-{hex}` id + numeric `displayId`, opens `opened`, seeds the communication thread; `DescribeCases` filters by case-id list / `displayId` / time window / `includeResolvedCases` / `includeCommunications` / language with a round-tripping `nextToken`; `AddCommunicationToCase` -> `result: true`; `DescribeCommunications` pages the thread; `ResolveCase` -> `initialCaseStatus` + `finalCaseStatus`), attachment sets (`AddAttachmentsToSet` mints/extends an `attachmentSetId` + `expiryTime`; `DescribeAttachment`), presigned attachment uploads (`GetAttachmentUploadLinks` / `CompleteAttachmentUpload` / `DescribeAttachmentUploadStatus` / `GetAttachmentDownloadLink`, with links that point back at fakecloud and really transfer bytes), severity levels + service/category catalogues (`DescribeSeverityLevels` / `DescribeServices` / `DescribeCreateCaseOptions` / `DescribeSupportedLanguages`), and the Trusted Advisor API (`DescribeTrustedAdvisorChecks` returns the vendored check catalogue; `DescribeTrustedAdvisorCheckResult` / `DescribeTrustedAdvisorCheckSummaries` return all-clear results; `RefreshTrustedAdvisorCheck` + `DescribeTrustedAdvisorCheckRefreshStatuses` drive a real `none` -> `enqueued` -> `processing` -> `success` refresh state machine). Model-driven validation with Support's declared exceptions (`CaseIdNotFound`/`AttachmentIdNotFound`/`AttachmentSetIdNotFound`/`UploadIdNotFound`/`DryRunOperationException`). JSON 1.1 protocol. Account-partitioned and persisted. Honest gap: no Trusted Advisor analysis engine and no live support agent, so results report zero flagged resources and cases receive no automated agent reply | | AWS Serverless Application Repository | 14 | Complete AWS Serverless Application Repository control plane: applications (`CreateApplication` mints the `arn:aws:serverlessrepo:...:applications/` ARN that is the `applicationId`, stores author/description/labels/license/readme/spdxLicenseId/sourceCodeUrl, seeds an initial version from a supplied `semanticVersion` + template; `GetApplication` returns the app plus a `Version` block with `parameterDefinitions` parsed from the SAM/CloudFormation template, `requiredCapabilities`, and `resourcesSupported`; `ListApplications` paginates; `UpdateApplication`; `DeleteApplication`), versions (`CreateApplicationVersion` PUTs a semantic version, parsing the template; `ListApplicationVersions`), sharing policy (`Put`/`GetApplicationPolicy` over principals/actions/`principalOrgIDs`; `UnshareApplication`), CloudFormation templates (`CreateCloudFormationTemplate` mints a `templateId` + `templateUrl`, status `PREPARING` -> `ACTIVE` on read; `GetCloudFormationTemplate`), and `ListApplicationDependencies` (nested `AWS::Serverless::Application` dependencies). Model-derived required/label validation (`BadRequestException`/`NotFoundException`/`ConflictException`), account-partitioned, persisted. Honest gaps: `CreateCloudFormationChangeSet` mints well-formed `changeSetId`/`stackId` identifiers without materialising a real CloudFormation stack, and the `templateUrl` is well-formed but fakecloud does not serve raw template bytes at it (all 14 ops) | | API Gateway v1 | 124 | REST APIs, resources, methods, integrations (`MOCK`/`HTTP`/`HTTP_PROXY`/`AWS_PROXY` Lambda), deployments, stages, API keys, usage plans, authorizers, models, request validators, VPC links, domain names, base path mappings, client certs, gateway responses, docs, tags | | API Gateway v2 | 103 | HTTP APIs, routes, integrations, stages, deployments, authorizers, domains, models, VPC links, routing rules, developer portals, CORS, tags | diff --git a/website/content/docs/services/ec2.md b/website/content/docs/services/ec2.md index b26ce6dc0..d8a87bd4d 100644 --- a/website/content/docs/services/ec2.md +++ b/website/content/docs/services/ec2.md @@ -1,14 +1,14 @@ +++ title = "EC2" -description = "Amazon EC2 — the full 801-operation control plane. VPCs, subnets, security groups, instances, EBS, AMIs, transit gateways, VPN, IPAM, Verified Access, and the entire networking long tail at 100% Smithy conformance." +description = "Amazon EC2 — the full 802-operation control plane. VPCs, subnets, security groups, instances, EBS, AMIs, transit gateways, VPN, IPAM, Verified Access, and the entire networking long tail at 100% Smithy conformance." weight = 41 +++ -fakecloud implements **801 of 801** AWS EC2 operations at 100% Smithy conformance — the complete control plane for the largest service surface in AWS. Request/response shapes, flattened `ec2Query` XML lists, field names, enum validation, and integer/length bounds are checked against AWS's own Smithy model on every commit. +fakecloud implements **802 of 802** AWS EC2 operations at 100% Smithy conformance — the complete control plane for the largest service surface in AWS. Request/response shapes, flattened `ec2Query` XML lists, field names, enum validation, and integer/length bounds are checked against AWS's own Smithy model on every commit. ## Supported features -- **Core networking** — VPCs (+ secondary CIDRs, tenancy), DHCP option sets, subnets (+ CIDR reservations), security groups (rules, references, VPC associations), route tables, internet / egress-only / NAT gateways, and elastic IPs with transfer/move flows. +- **Core networking** — VPCs (+ secondary CIDRs, tenancy), DHCP option sets, subnets (+ CIDR reservations), security groups (rules, references, VPC associations, per-interface quota validation), route tables, internet / egress-only / NAT gateways, and elastic IPs with transfer/move flows. - **Compute** — `RunInstances` and the full instance lifecycle (start/stop/reboot/terminate/monitor), instance attributes, credit specifications, metadata + maintenance options, instance types, and topology. Key pairs and placement groups. **Instances are backed by real Docker/Podman containers** — `RunInstances` boots a container per instance, runs your user-data at boot, and maps start/stop/reboot/terminate onto the container lifecycle (falling back to a metadata-only control plane when no container runtime is present). - **Storage** — EBS volumes (+ modifications, recycle bin), snapshots (+ copy, tier, lock, fast restores, block-public-access), AMIs (register/copy/deprecate/deregistration-protection), and EBS encryption defaults. - **Seeded public AMI catalogue** — every account starts with a small catalogue of public Amazon-owned (Amazon Linux 2, Amazon Linux 2023 x86_64/arm64, Windows Server) and Canonical-owned (Ubuntu 22.04/24.04) AMIs, just like a real account. `DescribeImages` honours the `Owner` param (`amazon` / `aws-marketplace` / `self` / account id) and `name` filters with `*` wildcards, so the standard Terraform `data "aws_ami"` pattern (`most_recent = true`, `owners = ["amazon"]`, a `name` glob) resolves to a real image instead of returning empty — and anything that chains off it (an `aws_instance`, an ELBv2 target-group attachment) can be planned. diff --git a/website/content/docs/services/kinesis.md b/website/content/docs/services/kinesis.md index be32fe66f..86c020bf3 100644 --- a/website/content/docs/services/kinesis.md +++ b/website/content/docs/services/kinesis.md @@ -4,7 +4,7 @@ description = "Data Streams, records, shard iterators, retention, tagging." weight = 16 +++ -fakecloud implements **39 of 39** Kinesis operations at 100% Smithy conformance. +fakecloud implements **44 of 44** Kinesis operations at 100% Smithy conformance. ## Supported features @@ -18,6 +18,7 @@ fakecloud implements **39 of 39** Kinesis operations at 100% Smithy conformance. - **Consumers** — EnableEnhancedMonitoring, DisableEnhancedMonitoring - **Cross-stream** — MergeShards, SplitShard - **Resource policies** — PutResourcePolicy, GetResourcePolicy, DeleteResourcePolicy +- **Delivery channels** — CreateChannel, DescribeChannel, ListChannels, UpdateChannel, DeleteChannel: a channel fans records from one or more source streams into an S3 bucket or into Apache Iceberg tables on S3 Tables, with the destination, freshness, encryption, and CloudWatch logging configuration persisted and round-tripped. A stream cannot be deleted while a channel still draws from it. ## Protocol diff --git a/website/content/docs/services/ses.md b/website/content/docs/services/ses.md index fb0b0704c..7afd0e3e4 100644 --- a/website/content/docs/services/ses.md +++ b/website/content/docs/services/ses.md @@ -4,7 +4,7 @@ description = "Sending, templates, DKIM, suppression, and real inbound receipt r weight = 14 +++ -fakecloud implements **110 of 110** SES v2 operations at 100% Smithy conformance, plus SES v1 inbound receipt rule operations. +fakecloud implements **116 of 116** SES v2 operations at 100% Smithy conformance, plus SES v1 inbound receipt rule operations. ## Supported features @@ -13,7 +13,8 @@ fakecloud implements **110 of 110** SES v2 operations at 100% Smithy conformance - **SendEmail, SendBulkEmail** — recorded at `/_fakecloud/ses/emails`, including the stamped `DKIM-Signature` header when signing is enabled - **SendBounce (v1)** — synthesise an inbound bounce message; the bounce record lands in `/_fakecloud/ses/emails` and triggers configured event destinations - **Identities** — email identity and domain identity CRUD, real DKIM RSA-SHA256 signing with relaxed/relaxed canonicalization (public key served at `/_fakecloud/ses/identities/{name}/dkim-public-key`), mail-from, feedback attributes, signing attributes -- **Configuration sets** — CRUD, event destinations, reputation options, sending options, tracking, suppression, VDM, archiving +- **Configuration sets** — CRUD (including `UpdateConfigurationSet` for message security options), event destinations, reputation options, sending options, tracking, suppression, VDM, archiving +- **Identity certificates** — AssociateEmailIdentityCertificate, DisassociateEmailIdentityCertificate, ListEmailIdentityCertificates: an ACM certificate ARN is associated with a verified identity (per from-address for domain identities), stored, and listed back with its provisioning status - **Templates** — email templates, custom verification templates, and `TestRenderEmailTemplate` which produces a full RFC 5322 / MIME message (Subject, From, To, CC, BCC, Reply-To, Date, Message-ID, multipart bodies, attachments) from the stored template + JSON template data - **Contact lists** — CRUD, contacts, subscription topics - **Dedicated IPs** — pools, warmup, scaling diff --git a/website/content/docs/services/support.md b/website/content/docs/services/support.md index 82463b9d4..8ce0b8b95 100644 --- a/website/content/docs/services/support.md +++ b/website/content/docs/services/support.md @@ -1,11 +1,11 @@ +++ title = "AWS Support" -description = "AWS Support on fakecloud: the full 16-operation surface -- support cases, communications, attachment sets, severity levels, the service/category catalogue, and the Trusted Advisor check catalogue + refresh state machine -- with account-partitioned persistence. The Trusted Advisor analysis engine and the live support agent are documented gaps." +description = "AWS Support on fakecloud: the full 20-operation surface -- support cases, communications, attachment sets, presigned attachment uploads and downloads, severity levels, the service/category catalogue, and the Trusted Advisor check catalogue + refresh state machine -- with account-partitioned persistence. The Trusted Advisor analysis engine and the live support agent are documented gaps." weight = 77 +++ fakecloud implements **AWS Support** (`support`), the programmatic interface to -AWS Support cases and AWS Trusted Advisor. All **16 operations** from the AWS +AWS Support cases and AWS Trusted Advisor. All **20 operations** from the AWS Smithy model ship now, backed by account-partitioned state that persists across restarts in persistent mode. The wire protocol is awsJson1.1 (x-amz-target `AWSSupport_20130415.`), signing as `support`. @@ -15,8 +15,11 @@ Trusted Advisor refresh status is real, validated, persisted state -- no stubbed success responses. Requests are validated against the model's `required` / `length` / `range` constraints before any handler runs, and an operation that dereferences a case that does not exist returns Support's `CaseIdNotFound`; an -unknown attachment id returns `AttachmentIdNotFound`, and an unknown attachment -set id returns `AttachmentSetIdNotFound`. +unknown attachment id returns `AttachmentIdNotFound`, an unknown attachment +set id returns `AttachmentSetIdNotFound`, and an unknown, expired, or +already-completed upload id returns `UploadIdNotFound`. Every operation that +models `DryRunOperationException` honours `dryRun`: the request is validated +and then refused without touching state. ## Supported features @@ -49,6 +52,38 @@ set id returns `AttachmentSetIdNotFound`. - **`DescribeAttachment`** returns a stored attachment (`fileName` + base64 `data`) by id. +### Presigned attachment uploads + +Large attachments do not travel in the API request. AWS hands out presigned +links and the client transfers the bytes itself; fakecloud does the same, with +the links pointing back at fakecloud, which serves them. + +- **`GetAttachmentUploadLinks`** records a new upload (or resumes one named by + `uploadId`, optionally narrowed to an `uploadRange`) and returns an + `uploadId`, the `partSizeBytes` (5 MiB), the `totalParts` derived from + `fileSizeBytes`, the `nextIndex` still outstanding, and one presigned `PUT` + `url` per part with its own `expiryDate`. Each link carries its own + signature, recorded in state; a link that was never issued, was tampered + with, or has expired is refused. +- The links are real. `PUT` the part's bytes to the URL and fakecloud stores + them and returns the part's `ETag`, exactly as an S3 part upload does. +- **`CompleteAttachmentUpload`** takes the `uploadId` and the + `completedUploads` list of `{partIndex, eTag}`. Every part must have been + uploaded and every `ETag` must match what the `PUT` returned; the parts are + then concatenated into a real attachment, retrievable with + `DescribeAttachment`. Completing twice, or completing an upload whose links + expired, returns `UploadIdNotFound`. +- **`DescribeAttachmentUploadStatus`** reports the recorded `uploadStatus` + (`attachment-not-ready` / `attachment-ready` / `failed`), the `fileName`, and + the `uploadProgress` (`totalParts` + `completedPartsCount`). +- **`GetAttachmentDownloadLink`** mints a presigned `GET` link for a stored + attachment and returns it with the `fileName` and an `expiryDate`. Following + the link serves the attachment's bytes; an unknown attachment id returns + `AttachmentIdNotFound`. +- A completed upload attaches to a case through `uploadIds` on `CreateCase` or + `AddCommunicationToCase`; the resulting communication lists it under + `attachments`. + ### Severity levels + case-creation reference data - **`DescribeSeverityLevels`** returns the five real severity levels (`low`, @@ -102,12 +137,14 @@ fall outside their Smithy error contract.) The declared exceptions `CaseIdNotFound`, `AttachmentIdNotFound`, `AttachmentSetIdNotFound`, `AttachmentSetExpired`, `AttachmentSetSizeLimitExceeded`, `AttachmentLimitExceeded`, `DescribeAttachmentLimitExceeded`, -`CaseCreationLimitExceeded`, and `InternalServerError` model the service's error -surface. +`CaseCreationLimitExceeded`, `UploadIdNotFound`, `DryRunOperationException`, +and `InternalServerError` model the service's error surface. ## Persistence Support state is account-partitioned and, in persistent mode, snapshotted to disk and restored on restart. Cases, communication threads, attachment sets, -individual attachments, and Trusted Advisor refresh statuses all survive a -restart. +individual attachments, in-flight attachment uploads (including the bytes +already uploaded to their presigned links), issued download grants, and Trusted +Advisor refresh statuses all survive a restart. An upload whose links expired +while the server was down is swept to `failed` on load rather than resurrected. diff --git a/website/content/fake-aws-server.md b/website/content/fake-aws-server.md index 583c20e13..3d4d1ffed 100644 --- a/website/content/fake-aws-server.md +++ b/website/content/fake-aws-server.md @@ -16,7 +16,7 @@ Listens on `http://localhost:4566`. Any AWS SDK in any language points at it and ## What "fake AWS server" means here - **Real HTTP server**, not an in-process mock. Your Go / Java / Kotlin / Node / Rust / PHP / Python code uses the regular AWS SDK with `endpoint_url` set to `http://localhost:4566`. -- **Speaks the AWS wire protocol** at true 100% conformance across every implemented service. 105 services, 7,491 operations, 248,557/248,557 Smithy-model-generated test variants pass on every commit. +- **Speaks the AWS wire protocol** at true 100% conformance across every implemented service. 105 services, 7,505 operations, 248,557/248,557 Smithy-model-generated test variants pass on every commit. - **Real execution** for stateful services: Lambda runs your function code in Docker containers across 23 runtimes, RDS runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2, ElastiCache runs real Redis/Valkey/Memcached. - **Real cross-service wiring**: S3 -> Lambda, SQS -> Lambda, SNS fan-out, EventBridge -> Step Functions, and 15+ more integrations execute end-to-end, not as stubs. - **Free, open-source, AGPL-3.0.** No account, no auth token, no paid tier. diff --git a/website/content/faq.md b/website/content/faq.md index d6097ff78..9f4922df2 100644 --- a/website/content/faq.md +++ b/website/content/faq.md @@ -20,7 +20,7 @@ Yes. LocalStack replaced its open-source Community Edition with a proprietary im ### How many AWS services does fakecloud support? -105 services and 7,491 API operations. 248,557/248,557 generated Smithy conformance variants pass on every commit — true 100% across every implemented service, with more services on the roadmap. The explicit goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups. +105 services and 7,505 API operations. 248,557/248,557 generated Smithy conformance variants pass on every commit — true 100% across every implemented service, with more services on the roadmap. The explicit goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups. ### Which AWS services are supported? @@ -109,7 +109,7 @@ GitHub issues: [github.com/faiscadev/fakecloud/issues](https://github.com/faisca {"@type": "Question", "name": "What is fakecloud?", "acceptedAnswer": {"@type": "Answer", "text": "fakecloud is a free, open-source local AWS cloud emulator for integration testing and local development. It runs on a single port (4566), requires no account or auth token, and aims for 100% behavioral conformance with real AWS on every service it implements. AGPL-3.0 licensed."}}, {"@type": "Question", "name": "Is fakecloud free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. AGPL-3.0, free for commercial use. Using fakecloud as a dev/test dependency has zero AGPL implications for your application."}}, {"@type": "Question", "name": "Is fakecloud a LocalStack alternative?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. LocalStack replaced its open-source Community Edition with a proprietary image in March 2026 that requires an account and auth token. fakecloud is a free, open-source replacement."}}, - {"@type": "Question", "name": "How many AWS services does fakecloud support?", "acceptedAnswer": {"@type": "Answer", "text": "105 services and 7,491 API operations. 248,557/248,557 generated Smithy conformance variants pass on every commit, true 100% across every implemented service, with more on the roadmap. The goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations."}}, + {"@type": "Question", "name": "How many AWS services does fakecloud support?", "acceptedAnswer": {"@type": "Answer", "text": "105 services and 7,505 API operations. 248,557/248,557 generated Smithy conformance variants pass on every commit, true 100% across every implemented service, with more on the roadmap. The goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations."}}, {"@type": "Question", "name": "Which AWS services are supported?", "acceptedAnswer": {"@type": "Answer", "text": "S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue."}}, {"@type": "Question", "name": "Does fakecloud execute Lambda code for real?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. fakecloud pulls real AWS Lambda runtime containers and executes your handler against them. 27 official runtimes including Node.js 16/18/20/22/24, Python 3.8 through 3.14, Java 11/17/21/25, .NET 6/8/10, Ruby 3.2, Go (go1.x), and custom provided/provided.al2/provided.al2023."}}, {"@type": "Question", "name": "Does fakecloud run real databases for RDS?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. RDS emulation pulls real PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, and Db2 Docker images and runs them as the DB instance."}}, diff --git a/website/content/localstack-alternative.md b/website/content/localstack-alternative.md index 122c36d2d..d074f0f7d 100644 --- a/website/content/localstack-alternative.md +++ b/website/content/localstack-alternative.md @@ -1,6 +1,6 @@ +++ title = "Free, open-source LocalStack alternative" -description = "fakecloud is a free, open-source local AWS emulator: 105 services, 7,491 operations, 248,557/248,557 Smithy variants pass (true 100% conformance), 6 test-assertion SDKs. No account, no token, no paid tier. Drop-in replacement for LocalStack Community." +description = "fakecloud is a free, open-source local AWS emulator: 105 services, 7,505 operations, 248,557/248,557 Smithy variants pass (true 100% conformance), 6 test-assertion SDKs. No account, no token, no paid tier. Drop-in replacement for LocalStack Community." template = "page.html" aliases = [ "/alternative/localstack/", @@ -31,7 +31,7 @@ This is why fakecloud runs real Lambda code in real runtime containers, runs rea ## What fakecloud gives you - **105 AWS services.** S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue. -- **7,491 API operations. True 100% conformance** across every implemented service — 248,557/248,557 Smithy-model-generated test variants pass on every commit. +- **7,505 API operations. True 100% conformance** across every implemented service — 248,557/248,557 Smithy-model-generated test variants pass on every commit. - **Tested against upstream Terraform acceptance tests.** CI runs `hashicorp/terraform-provider-aws` `TestAcc*` suites against fakecloud, catching waiter and field-presence drift that pure SDK tests miss. - **Real Lambda execution.** 23 runtimes in Docker containers. Not a mock, not a stub. Node, Python, Java, Go, .NET, Ruby, custom runtimes. - **Real stateful services.** RDS runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2. ElastiCache runs real Redis/Valkey/Memcached. Your Lambda talking to RDS is talking to a real Postgres (or Oracle, or SQL Server). diff --git a/website/content/supported-services.md b/website/content/supported-services.md index da5096df3..61ea031c8 100644 --- a/website/content/supported-services.md +++ b/website/content/supported-services.md @@ -1,21 +1,21 @@ +++ title = "AWS Service Coverage & API Conformance" -description = "fakecloud provides 100% API conformance across 7,491 operations. Explore our supported AWS services for local development." +description = "fakecloud provides 100% API conformance across 7,505 operations. Explore our supported AWS services for local development." template = "page.html" +++ -fakecloud provides 100% API conformance across 7,491 operations. Unlike mocks, fakecloud is built against official AWS Smithy models to ensure wire-protocol compatibility and deterministic behavior for local development. +fakecloud provides 100% API conformance across 7,505 operations. Unlike mocks, fakecloud is built against official AWS Smithy models to ensure wire-protocol compatibility and deterministic behavior for local development. ## Coverage Summary - **Total Services**: 105 -- **Total Operations**: 7,491 +- **Total Operations**: 7,505 - **Conformance Engine**: 248,557 Smithy-based test variants - **Startup Time**: ~300ms ## Supported Services ### Compute & Containers -- **EC2**: 801 operations. The complete EC2 control plane — VPCs, subnets, security groups, route tables, gateways, instances, EBS, AMIs, the full 74-op Transit Gateway surface, Site-to-Site + Client VPN, IPAM, Verified Access, Network Insights, and Outpost / local-gateway networking. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with start/stop/reboot/terminate mapped to the container lifecycle and `GetConsoleOutput` returning the container log; degrades to metadata-only when no container runtime is present. +- **EC2**: 802 operations. The complete EC2 control plane — VPCs, subnets, security groups, route tables, gateways, instances, EBS, AMIs, the full 74-op Transit Gateway surface, Site-to-Site + Client VPN, IPAM, Verified Access, Network Insights, and Outpost / local-gateway networking. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with start/stop/reboot/terminate mapped to the container lifecycle and `GetConsoleOutput` returning the container log; degrades to metadata-only when no container runtime is present. - **Lambda**: 73 operations. Full execution environment in real Docker containers across 23 runtimes, cross-service triggers (S3, SNS, SQS, EventBridge). - **ECR**: 58 operations. Full OCI v2 Distribution protocol support for `docker push` and `docker pull`. - **ECS**: 77 operations. Real Fargate-style task execution via Docker, services with rolling deployments, ECS Exec. diff --git a/website/static/llms-full.txt b/website/static/llms-full.txt index 24b61497e..8f98b026e 100644 --- a/website/static/llms-full.txt +++ b/website/static/llms-full.txt @@ -6,7 +6,7 @@ fakecloud emulates AWS locally for integration testing and development. It is a single Rust binary (~19 MB, ~300ms startup, ~10 MiB idle memory) — no Docker required to run fakecloud itself, no signup. Point any AWS SDK or the AWS CLI at `http://localhost:4566` with dummy credentials. -**Coverage goal:** 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Approach is depth-first — a service lands when it passes the full Smithy-model test variants and the cross-service wire-ups that matter for it, not when the API surface looks filled in. 105 services (7,491 operations) are shipped today, all at true 100% conformance — 248,557/248,557 generated Smithy variants pass on every commit; more land progressively as they hit the bar. +**Coverage goal:** 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Approach is depth-first — a service lands when it passes the full Smithy-model test variants and the cross-service wire-ups that matter for it, not when the API surface looks filled in. 105 services (7,505 operations) are shipped today, all at true 100% conformance — 248,557/248,557 generated Smithy variants pass on every commit; more land progressively as they hit the bar. Key design principles: - **Depth-first coverage**: every implemented service targets 100% conformance with real AWS, validated on every commit against AWS's own Smithy models — 248,557/248,557 generated test variants pass on every commit, true 100% across every implemented service. CI also runs upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites against fakecloud. diff --git a/website/static/llms.txt b/website/static/llms.txt index 1b90d219f..6e495ea7f 100644 --- a/website/static/llms.txt +++ b/website/static/llms.txt @@ -4,7 +4,7 @@ - **Goal:** 100% of AWS services, each at 100% conformance, with 100% of cross-service integrations. Approach is depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. - Single static binary (~19 MB), ~300ms startup, ~10 MiB idle memory, no Docker required to run fakecloud itself -- **105 AWS services shipped today, 7,491 operations, true 100% Smithy conformance — 248,557/248,557 generated test variants pass on every commit**, no flake margin and no skipped services. More services land as they hit the conformance bar; roadmap is driven by real-project demand. +- **105 AWS services shipped today, 7,505 operations, true 100% Smithy conformance — 248,557/248,557 generated test variants pass on every commit**, no flake margin and no skipped services. More services land as they hit the conformance bar; roadmap is driven by real-project demand. - Services: S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, AWS Backup, AWS AppConfig, Cloud Map, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, CloudTrail, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue - 30+ cross-service integrations: S3 notifications, SNS fan-out, EventBridge rules, DynamoDB Streams, CloudWatch Logs subscriptions, Cognito triggers, API Gateway -> Lambda, Step Functions task integrations, SES inbound -> S3/SNS/Lambda, and more - Real Lambda execution via Docker across 23 runtimes (Node.js 16/18/20/22/24, Python 3.8/3.9/3.10/3.11/3.12/3.13/3.14, Java 11/17/21/25, Go 1.x, Ruby 3.3/3.4, .NET 8/10, custom `provided.al2` / `provided.al2023`) @@ -49,9 +49,9 @@ - **CloudFormation** (90 ops): stacks, resource provisioning, template parsing, intrinsic functions, custom resources, drift detection, change sets - **Cloud Control API** (8 ops): uniform create/read/update/delete/list over CloudFormation resource types, real provisioners (container-backed included), RFC 6902 JSON Patch updates, ClientToken idempotency, request tracking - **Resource Groups** (23 ops): groups defined by tag/CloudFormation-stack resource queries, explicit membership (GroupResources/UngroupResources/ListGroupResources), group configuration, tagging, account settings, grouping statuses, tag-sync tasks; account-partitioned and persisted -- **SES** (112 ops): v2 API (send email, templates, configuration sets, event destinations, DKIM, suppression list) + v1 inbound (receipt rules with real S3/SNS/Lambda action execution, receipt filters) +- **SES** (116 ops): v2 API (send email, templates, configuration sets, event destinations, DKIM, suppression list) + v1 inbound (receipt rules with real S3/SNS/Lambda action execution, receipt filters) - **Cognito User Pools** (132 ops): user pools, app clients, users, groups, MFA (SMS, TOTP, WebAuthn), identity providers (Google, Facebook, SAML, OIDC), resource servers, domains, devices, full authentication flows (USER_PASSWORD_AUTH, USER_SRP_AUTH, REFRESH_TOKEN_AUTH, CUSTOM_AUTH), triggers -- **Kinesis** (39 ops): data streams, records, shard iterators, retention changes, enhanced fan-out consumers, tagging +- **Kinesis** (44 ops): data streams, records, shard iterators, retention changes, enhanced fan-out consumers, tagging - **RDS** (163 ops): DB instances with real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2 engines via Docker, snapshots, read replicas, parameter groups, subnet groups, engine/version discovery, tagging - **ElastiCache** (75 ops): cache clusters with real Redis/Valkey/Memcached via Docker, replication groups, global replication groups, serverless caches and snapshots, subnet groups, users/user groups, failover, tagging - **MemoryDB** (45 ops): full control plane for Redis/Valkey clusters, shards, ACLs, users, parameter/subnet groups, snapshots, multi-region clusters, reserved nodes; persisted. Data-plane container backing is a follow-up @@ -81,7 +81,7 @@ - **Organizations** (63 ops): org tree (roots/OUs/accounts), `CreateAccount` async `IN_PROGRESS -> SUCCEEDED`, policies (SCP/TAG/BACKUP/AISERVICES_OPT_OUT) with **real SCP enforcement** as a permission ceiling under `FAKECLOUD_IAM=strict`, handshakes, delegated administrators, AWS service access, billing responsibility transfers, resource policy, tagging - **Bedrock Agent** (72 ops): agents, agent versions/aliases, action groups, knowledge bases, data sources, flows, prompts, agent collaborators, tagging - **Bedrock Agent Runtime** (31 ops): `InvokeAgent`, `InvokeFlow`, `Retrieve`, `RetrieveAndGenerate`, session management, memory, with configurable + streaming responses -- **EC2** (801 ops): the complete EC2 control plane — VPCs, subnets, security groups, route tables, gateways, instances, EBS, AMIs, the full 74-op Transit Gateway surface, Site-to-Site + Client VPN, IPAM, Verified Access, Network Insights, and Outpost / local-gateway networking. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with start/stop/reboot/terminate mapped to the container lifecycle and `GetConsoleOutput` returning the container log; degrades to metadata-only when no container runtime is present. Real network isolation: a default VPC ships per account+region; each subnet gets its own daemon bridge (cross-VPC instances can't route to each other), and security-group/NACL rules are enforced via host nftables (opt-in `FAKECLOUD_EC2_SG_ENFORCEMENT`, needs CAP_NET_ADMIN) on Docker/Podman or via NetworkPolicy on Kubernetes, degrading to tracked-only without the capability. `ec2Query` protocol with flattened-XML lists +- **EC2** (802 ops): the complete EC2 control plane — VPCs, subnets, security groups, route tables, gateways, instances, EBS, AMIs, the full 74-op Transit Gateway surface, Site-to-Site + Client VPN, IPAM, Verified Access, Network Insights, and Outpost / local-gateway networking. Instances run as real containers — Docker/Podman by default or native Kubernetes Pods (`FAKECLOUD_EC2_BACKEND=k8s`) — running user-data at boot, with start/stop/reboot/terminate mapped to the container lifecycle and `GetConsoleOutput` returning the container log; degrades to metadata-only when no container runtime is present. Real network isolation: a default VPC ships per account+region; each subnet gets its own daemon bridge (cross-VPC instances can't route to each other), and security-group/NACL rules are enforced via host nftables (opt-in `FAKECLOUD_EC2_SG_ENFORCEMENT`, needs CAP_NET_ADMIN) on Docker/Podman or via NetworkPolicy on Kubernetes, degrading to tracked-only without the capability. `ec2Query` protocol with flattened-XML lists ## Introspection endpoints (for tests) diff --git a/website/templates/index.html b/website/templates/index.html index 855bd2a84..f5fbd8370 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -12,7 +12,7 @@ "applicationCategory": "DeveloperApplication", "applicationSubCategory": "CloudTestingTool", "operatingSystem": "Linux, macOS, Windows", - "description": "Free, open-source local AWS cloud emulator. 105 services, 7,491 operations, 248,557/248,557 Smithy variants pass — true 100% conformance. Single binary, no account, no auth token.", + "description": "Free, open-source local AWS cloud emulator. 105 services, 7,505 operations, 248,557/248,557 Smithy variants pass — true 100% conformance. Single binary, no account, no auth token.", "url": "https://fakecloud.dev", "downloadUrl": "https://github.com/faiscadev/fakecloud/releases", "codeRepository": "https://github.com/faiscadev/fakecloud", @@ -56,7 +56,7 @@

fakecloud

Local AWS cloud emulator for integration tests. Run your app with normal AWS clients, stay fully local, and use fakecloud SDKs when your tests need deeper visibility.

-

105 services. 7,491 operations. 248,557/248,557 Smithy variants pass — true 100% conformance.

+

105 services. 7,505 operations. 248,557/248,557 Smithy variants pass — true 100% conformance.

Get Started Why fakecloud From 36f4a1e91974e72e7920c0ff5edcbde4603fe48f Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sun, 13 Sep 2026 15:01:28 -0300 Subject: [PATCH 3/3] fix(models): address the review findings on the new refresh operations Support attachment uploads: - the presigned part PUT took axum's `Bytes` extractor, so its 2 MB default body limit rejected every part the multipart flow exists for; buffer the body with the same 1 GiB cap the dispatcher uses - `nextIndex` reported the next un-uploaded part instead of the next part to request links for, so a client following the documented paging contract looped forever; it is now the exclusive end of the range just issued, null once the last part has been handed out - re-issuing links rotated a part's signature (invalidating a URL the caller was still using) and renewed the upload's own deadline indefinitely; a live link is now returned as-is and the upload deadline never moves - `uploadRange.endIndex` is exclusive per the model, and a call returns at most ten links; a wider range is rejected - `CompleteAttachmentUpload` demanded every part in one call; the model allows one part per call, so completion is recorded per part and the attachment is assembled once every part is in - part sizes are enforced against `partSizeBytes` / `fileSizeBytes` instead of accepting a 4-byte stand-in for a 20 MB file - the assembled attachment no longer keeps a second copy of every part's bytes in memory and in every snapshot - expired download grants are pruned when a new one is minted rather than accumulating as live credentials until restart - `Content-Disposition` escapes the caller-supplied file name, so a name with a quote can no longer inject a second filename parameter and one with CR/LF no longer 500s Kinesis channels: - channel tags were write-only: the model's `ResourceARN` covers channel ARNs, so TagResource / UntagResource / ListTagsForResource now resolve them - `GSRSchemaARN` and `ServiceExecutionRoleARN` were validated at 2048; both are 512 in the model, and `ResourceARN` was not length-checked at all - `ListChannels` stream filters and the DeleteStream in-use guard compared raw ARNs while creation resolves them region-tolerantly, so a client in another region filtered to nothing and could delete a stream with a channel attached EC2 `Validate*` is read-only, so it no longer triggers a snapshot write. --- crates/fakecloud-conformance/tests/kinesis.rs | 200 ++++- crates/fakecloud-conformance/tests/support.rs | 174 +++- crates/fakecloud-kinesis/src/service.rs | 36 +- .../fakecloud-kinesis/src/service_helpers.rs | 82 +- crates/fakecloud-kinesis/src/service_tests.rs | 269 +++++++ crates/fakecloud-kinesis/src/state.rs | 69 +- .../src/support_attachments.rs | 226 +++++- crates/fakecloud-support/src/dataplane.rs | 102 +++ crates/fakecloud-support/src/lib.rs | 10 +- crates/fakecloud-support/src/persistence.rs | 1 + crates/fakecloud-support/src/service.rs | 759 +++++++++++++++--- crates/fakecloud-support/src/shared.rs | 6 + crates/fakecloud-support/src/state.rs | 102 ++- website/content/docs/services/support.md | 25 +- 14 files changed, 1855 insertions(+), 206 deletions(-) diff --git a/crates/fakecloud-conformance/tests/kinesis.rs b/crates/fakecloud-conformance/tests/kinesis.rs index 42df3bdd1..e687890da 100644 --- a/crates/fakecloud-conformance/tests/kinesis.rs +++ b/crates/fakecloud-conformance/tests/kinesis.rs @@ -800,16 +800,32 @@ async fn kinesis_subscribe_to_shard_requires_registered_consumer() { // awsJson1_1 HTTP with the `X-Amz-Target` header, the same way the acm-pca // suite drives a service with no SDK at all. -const CHANNEL_AUTH: &str = - "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/kinesis/aws4_request, SignedHeaders=host, Signature=0"; +/// A credential scope naming `region`, so the server resolves the caller's +/// region the way a real SDK client configured for it would. +fn channel_auth(region: &str) -> String { + format!( + "AWS4-HMAC-SHA256 Credential=test/20240101/{region}/kinesis/aws4_request, \ + SignedHeaders=host, Signature=0" + ) +} /// POST an awsJson1_1 Kinesis action, returning `(status, parsed_body)`. async fn channel_op(server: &TestServer, op: &str, body: Value) -> (u16, Value) { + channel_op_in_region(server, "us-east-1", op, body).await +} + +/// `channel_op` under a credential scope naming `region`. +async fn channel_op_in_region( + server: &TestServer, + region: &str, + op: &str, + body: Value, +) -> (u16, Value) { let resp = reqwest::Client::new() .post(format!("{}/", server.endpoint())) .header("content-type", "application/x-amz-json-1.1") .header("x-amz-target", format!("Kinesis_20131202.{op}")) - .header("authorization", CHANNEL_AUTH) + .header("authorization", channel_auth(region)) .body(body.to_string()) .send() .await @@ -939,3 +955,181 @@ async fn kinesis_channel_lifecycle() { assert_eq!(status, 400, "describe deleted channel: {missing}"); assert_eq!(missing["__type"], "ResourceNotFoundException", "{missing}"); } + +#[test_action("kinesis", "TagResource", checksum = "58941b22")] +#[test_action("kinesis", "ListTagsForResource", checksum = "0eb8b1e3")] +#[test_action("kinesis", "UntagResource", checksum = "3c1bbd62")] +#[tokio::test] +async fn kinesis_channel_tags() { + let server = TestServer::start().await; + let client = server.kinesis_client().await; + + client + .create_stream() + .stream_name("channel-tag-stream") + .shard_count(1) + .send() + .await + .unwrap(); + let source_arn = stream_arn(&client, "channel-tag-stream").await; + + let (status, created) = channel_op( + &server, + "CreateChannel", + json!({ + "ChannelName": "tagged-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::000000000000:role/tagged-channel", + "StreamConfigurationList": [{ + "StreamARN": source_arn, + "RecordConfiguration": { "RecordFormatType": "JSON" }, + }], + "S3DestinationConfiguration": { + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::conf-channel-bucket", + "ExpectedBucketOwner": "000000000000", + "CompressionType": "ZSTD", + } + }, + "Tags": { "team": "data" }, + }), + ) + .await; + assert_eq!(status, 200, "create channel: {created}"); + let channel_arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap() + .to_string(); + + // The tags CreateChannel accepted are readable through Tags v2, whose + // ResourceARN covers channels as well as streams. + let listed = client + .list_tags_for_resource() + .resource_arn(&channel_arn) + .send() + .await + .unwrap(); + assert!(listed.tags().iter().any(|t| t.key() == "team")); + + client + .tag_resource() + .resource_arn(&channel_arn) + .tags("env", "test") + .send() + .await + .unwrap(); + client + .untag_resource() + .resource_arn(&channel_arn) + .tag_keys("team") + .send() + .await + .unwrap(); + + let after = client + .list_tags_for_resource() + .resource_arn(&channel_arn) + .send() + .await + .unwrap(); + assert_eq!(after.tags().len(), 1, "{:?}", after.tags()); + assert_eq!(after.tags()[0].key(), "env"); + + // The source stream's own tags were never touched. + let stream_tags = client + .list_tags_for_resource() + .resource_arn(&source_arn) + .send() + .await + .unwrap(); + assert!(stream_tags.tags().is_empty(), "{:?}", stream_tags.tags()); +} + +#[test_action("kinesis", "ListChannels", checksum = "1fbca5f2")] +#[test_action("kinesis", "DeleteStream", checksum = "51c62afa")] +#[tokio::test] +async fn kinesis_channel_resolution_is_region_tolerant() { + let server = TestServer::start().await; + let client = server.kinesis_client().await; + + client + .create_stream() + .stream_name("xregion-stream") + .shard_count(1) + .send() + .await + .unwrap(); + // The stream is stored with a us-east-1 ARN; the channel is created by a + // caller whose credential scope is eu-west-1, naming the same stream by + // its own regional ARN. + let source_arn = stream_arn(&client, "xregion-stream").await; + let foreign_arn = source_arn.replace(":us-east-1:", ":eu-west-1:"); + assert_ne!(foreign_arn, source_arn, "{source_arn}"); + + let (status, created) = channel_op_in_region( + &server, + "eu-west-1", + "CreateChannel", + json!({ + "ChannelName": "xregion-channel", + "ServiceExecutionRoleARN": "arn:aws:iam::000000000000:role/xregion-channel", + "StreamConfigurationList": [{ + "StreamARN": foreign_arn, + "RecordConfiguration": { "RecordFormatType": "JSON" }, + }], + "S3DestinationConfiguration": { + "StorageConfiguration": { + "BucketARN": "arn:aws:s3:::conf-channel-bucket", + "ExpectedBucketOwner": "000000000000", + "CompressionType": "ZSTD", + } + }, + }), + ) + .await; + assert_eq!(status, 200, "create channel: {created}"); + let channel_arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap() + .to_string(); + + // The creating client filters by the only stream ARN it knows: its own. + let (status, listed) = channel_op_in_region( + &server, + "eu-west-1", + "ListChannels", + json!({ "StreamFilter": [{ "StreamARN": foreign_arn }] }), + ) + .await; + assert_eq!(status, 200, "list channels: {listed}"); + let summaries = listed["ChannelSummaries"].as_array().unwrap(); + assert_eq!(summaries.len(), 1, "{listed}"); + assert_eq!(summaries[0]["ChannelName"], "xregion-channel"); + + // And the in-use guard holds for that same client. + let (status, in_use) = channel_op_in_region( + &server, + "eu-west-1", + "DeleteStream", + json!({ "StreamARN": foreign_arn }), + ) + .await; + assert_eq!(status, 400, "delete attached stream: {in_use}"); + assert_eq!(in_use["__type"], "ResourceInUseException", "{in_use}"); + + let (status, deleted) = channel_op_in_region( + &server, + "eu-west-1", + "DeleteChannel", + json!({ "ChannelARN": channel_arn }), + ) + .await; + assert_eq!(status, 200, "delete channel: {deleted}"); + let (status, dropped) = channel_op_in_region( + &server, + "eu-west-1", + "DeleteStream", + json!({ "StreamARN": foreign_arn }), + ) + .await; + assert_eq!(status, 200, "delete detached stream: {dropped}"); +} diff --git a/crates/fakecloud-conformance/tests/support.rs b/crates/fakecloud-conformance/tests/support.rs index 676812c4b..ad1537937 100644 --- a/crates/fakecloud-conformance/tests/support.rs +++ b/crates/fakecloud-conformance/tests/support.rs @@ -57,8 +57,9 @@ async fn support_attachment_upload_round_trip() { let upload_id = links["uploadId"].as_str().unwrap().to_string(); assert!(!upload_id.is_empty()); assert_eq!(links["totalParts"], 1, "{links}"); - // Nothing has been uploaded yet, so part 1 is still outstanding. - assert_eq!(links["nextIndex"], 1, "{links}"); + // The only part's URL has been returned, so there is no next page of links + // to ask for. + assert!(links["nextIndex"].is_null(), "{links}"); assert!(links["partSizeBytes"].as_i64().unwrap() > 0, "{links}"); let part = &links["uploadUrls"][0]; assert_eq!(part["partIndex"], 1, "{links}"); @@ -190,6 +191,175 @@ async fn support_attachment_upload_round_trip() { assert_eq!(err["__type"], "UploadIdNotFound", "{err}"); } +/// A real multipart upload: a part far larger than any default request-body +/// limit, the model's half-open `uploadRange`, `nextIndex` paging, and one part +/// per `CompleteAttachmentUpload` call. +#[tokio::test] +async fn support_multipart_attachment_upload_is_incremental() { + let server = TestServer::start().await; + let client = reqwest::Client::new(); + // 5 MiB + 1 byte is two parts, the first of them well past the 2 MB body + // limit a default extractor would impose on the upload route. + let part_size = 5 * 1024 * 1024; + let first = vec![b'a'; part_size]; + let last = vec![b'z'; 1]; + + let (status, links) = support( + &server, + "GetAttachmentUploadLinks", + json!({ + "fileName": "big.bin", + "fileSizeBytes": part_size + 1, + "uploadRange": { "startIndex": 1, "endIndex": 2 }, + }), + ) + .await; + assert_eq!(status, 200, "{links}"); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + assert_eq!(links["totalParts"], 2, "{links}"); + // endIndex is exclusive, so 1..2 is part 1 alone and part 2 is next. + assert_eq!(links["uploadUrls"].as_array().unwrap().len(), 1, "{links}"); + assert_eq!(links["uploadUrls"][0]["partIndex"], 1, "{links}"); + assert_eq!(links["nextIndex"], 2, "{links}"); + + let resp = client + .put(links["uploadUrls"][0]["url"].as_str().unwrap()) + .body(first.clone()) + .send() + .await + .unwrap(); + assert!( + resp.status().is_success(), + "5 MiB part upload failed: {}", + resp.status() + ); + let first_etag = resp + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + // One part per call is allowed: the upload stays pending until every part + // has been reported. + let (status, pending) = support( + &server, + "CompleteAttachmentUpload", + json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": first_etag }], + }), + ) + .await; + assert_eq!(status, 200, "{pending}"); + assert_eq!(pending["uploadStatus"], "attachment-not-ready", "{pending}"); + + // Page to the rest of the links with the returned nextIndex. + let (status, more) = support( + &server, + "GetAttachmentUploadLinks", + json!({ + "fileName": "big.bin", + "uploadId": upload_id, + "uploadRange": { "startIndex": 2 }, + }), + ) + .await; + assert_eq!(status, 200, "{more}"); + assert_eq!(more["uploadUrls"][0]["partIndex"], 2, "{more}"); + assert!(more["nextIndex"].is_null(), "{more}"); + let last_url = more["uploadUrls"][0]["url"].as_str().unwrap().to_string(); + + // The last part carries the declared remainder and nothing else. + let resp = client + .put(&last_url) + .body(vec![b'z'; 64]) + .send() + .await + .unwrap(); + assert_eq!(resp.status().as_u16(), 400, "an oversized part is refused"); + let resp = client + .put(&last_url) + .body(last.clone()) + .send() + .await + .unwrap(); + assert!(resp.status().is_success(), "{}", resp.status()); + let last_etag = resp + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + let (status, completed) = support( + &server, + "CompleteAttachmentUpload", + json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 2, "eTag": last_etag }], + }), + ) + .await; + assert_eq!(status, 200, "{completed}"); + assert_eq!(completed["uploadStatus"], "attachment-ready", "{completed}"); + + // A range wider than the ten URLs a call may return is refused. + let (status, err) = support( + &server, + "GetAttachmentUploadLinks", + json!({ + "fileName": "big.bin", + "fileSizeBytes": 121 * 1024 * 1024, + "uploadRange": { "startIndex": 1, "endIndex": 13 }, + }), + ) + .await; + assert_eq!(status, 400, "{err}"); + assert_eq!(err["__type"], "ValidationException", "{err}"); + + // The assembled attachment is the two parts, in order. + let (status, created) = support( + &server, + "CreateCase", + json!({ + "subject": "conformance multipart upload", + "communicationBody": "big file attached", + "uploadIds": [upload_id], + }), + ) + .await; + assert_eq!(status, 200, "{created}"); + let (_, comms) = support( + &server, + "DescribeCommunications", + json!({ "caseId": created["caseId"].as_str().unwrap() }), + ) + .await; + let attachment_id = comms["communications"][0]["attachments"][0]["attachmentId"] + .as_str() + .unwrap() + .to_string(); + let (_, link) = support( + &server, + "GetAttachmentDownloadLink", + json!({ "attachmentId": attachment_id }), + ) + .await; + let resp = client + .get(link["downloadUrl"]["url"].as_str().unwrap()) + .send() + .await + .unwrap(); + assert!(resp.status().is_success(), "{}", resp.status()); + let body = resp.bytes().await.unwrap(); + assert_eq!(body.len(), first.len() + last.len()); + assert_eq!(body[0], b'a'); + assert_eq!(body[body.len() - 1], b'z'); +} + #[tokio::test] async fn support_attachment_upload_errors() { let server = TestServer::start().await; diff --git a/crates/fakecloud-kinesis/src/service.rs b/crates/fakecloud-kinesis/src/service.rs index 9b01e012a..069131c30 100644 --- a/crates/fakecloud-kinesis/src/service.rs +++ b/crates/fakecloud-kinesis/src/service.rs @@ -503,9 +503,10 @@ impl KinesisService { let state = accounts.get_or_create(&request.account_id); let stream_name = resolve_stream_name(state, &body)?; // A stream cannot be deleted while a channel still draws from it; AWS - // requires the attached channels to be deleted first. - let stream_arn = state.stream_arn(request.region.as_str(), &stream_name); - let attached_channels = state.channels_for_stream(&stream_arn); + // requires the attached channels to be deleted first. The guard keys + // off the stream name so a caller whose credential scope names a + // different region than the channel's source ARNs still trips it. + let attached_channels = state.channels_for_stream(&stream_name); if !attached_channels.is_empty() { return Err(AwsServiceError::aws_error( StatusCode::BAD_REQUEST, @@ -521,6 +522,7 @@ impl KinesisService { if stream.is_none() { return Err(stream_not_found(&state.account_id, &stream_name)); } + let stream_arn = state.stream_arn(request.region.as_str(), &stream_name); state.consumers.retain(|_, c| c.stream_arn != stream_arn); Ok(AwsResponse::ok_json(json!({}))) @@ -965,13 +967,10 @@ impl KinesisService { let mut accounts = self.state.write(); let state = accounts.get_or_create(&request.account_id); - let stream_name = state - .stream_name_from_arn(resource_arn) - .ok_or_else(|| resource_not_found_arn(resource_arn))?; - let stream = state.streams.get_mut(&stream_name).unwrap(); + let stored = resource_tags_mut(state, resource_arn)?; for (key, value) in tags { if let Some(value) = value.as_str() { - stream.tags.insert(key.clone(), value.to_string()); + stored.insert(key.clone(), value.to_string()); } } Ok(AwsResponse::ok_json(json!({}))) @@ -987,12 +986,9 @@ impl KinesisService { let mut accounts = self.state.write(); let state = accounts.get_or_create(&request.account_id); - let stream_name = state - .stream_name_from_arn(resource_arn) - .ok_or_else(|| resource_not_found_arn(resource_arn))?; - let stream = state.streams.get_mut(&stream_name).unwrap(); + let stored = resource_tags_mut(state, resource_arn)?; for key in tag_keys.iter().filter_map(|v| v.as_str()) { - stream.tags.remove(key); + stored.remove(key); } Ok(AwsResponse::ok_json(json!({}))) } @@ -1005,12 +1001,7 @@ impl KinesisService { let accounts = self.state.read(); let empty = KinesisState::new(&request.account_id, &request.region); let state = accounts.get(&request.account_id).unwrap_or(&empty); - let stream_name = state - .stream_name_from_arn(resource_arn) - .ok_or_else(|| resource_not_found_arn(resource_arn))?; - let stream = state.streams.get(&stream_name).unwrap(); - let tags: Vec = stream - .tags + let tags: Vec = resource_tags(state, resource_arn)? .iter() .map(|(key, value)| json!({ "Key": key, "Value": value })) .collect(); @@ -2133,8 +2124,9 @@ impl KinesisService { fn create_channel(&self, request: &AwsRequest) -> Result { let body = request.json_body(); let channel_name = require_channel_name(&body)?; + // `ServiceExecutionRoleARN` is a `RoleARN`, capped at 512. let service_execution_role_arn = - require_channel_member(&body, "ServiceExecutionRoleARN", 2048)?; + require_channel_member(&body, "ServiceExecutionRoleARN", 512)?; let channel_id = uuid::Uuid::new_v4().to_string(); let destination = parse_channel_destination(&body, channel_name, &channel_id)?; let encryption = parse_channel_encryption(&body["EncryptionConfiguration"])?; @@ -2219,7 +2211,6 @@ impl KinesisService { .as_i64() .unwrap_or(MAX_LIST_CHANNELS_PAGE as i64) .min(MAX_LIST_CHANNELS_PAGE as i64) as usize; - let filters = parse_channel_stream_filters(&body["StreamFilter"])?; let resume_after = match body["NextToken"].as_str() { Some(token) => Some(decode_list_channels_token(token)?), None => None, @@ -2228,6 +2219,9 @@ impl KinesisService { let accounts = self.state.read(); let empty = KinesisState::new(&request.account_id, &request.region); let state = accounts.get(&request.account_id).unwrap_or(&empty); + // Filters resolve source ARNs against the account's streams, so they + // are parsed once the state is in hand rather than off the raw body. + let filters = parse_channel_stream_filters(state, &body["StreamFilter"])?; // `channels` is keyed by name, so BTreeMap iteration is already the // name order the cursor resumes against. diff --git a/crates/fakecloud-kinesis/src/service_helpers.rs b/crates/fakecloud-kinesis/src/service_helpers.rs index e7831555b..04b9a29a3 100644 --- a/crates/fakecloud-kinesis/src/service_helpers.rs +++ b/crates/fakecloud-kinesis/src/service_helpers.rs @@ -226,10 +226,66 @@ pub(crate) fn require_shard_id(body: &Value) -> Result<&str, AwsServiceError> { } pub(crate) fn require_resource_arn(body: &Value) -> Result<&str, AwsServiceError> { - body["ResourceARN"] + let arn = body["ResourceARN"] .as_str() .filter(|value| !value.is_empty()) - .ok_or_else(|| invalid_argument("ResourceARN is required")) + .ok_or_else(|| invalid_argument("ResourceARN is required"))?; + validate_string_length("ResourceARN", arn, 1, 2048)?; + Ok(arn) +} + +/// The resource a Tags v2 `ResourceARN` names. The model constrains it to +/// `^arn:aws.*:kinesis:.*:\d{12}:.*(stream|channel)/\S+$`, so a delivery +/// channel is as valid a tag target as a stream. +pub(crate) enum TaggedResource { + Stream(String), + Channel(String), +} + +/// Resolve a Tags v2 `ResourceARN` to the resource it names. Both arms key off +/// the ARN's resource segment, so a caller whose credential scope names a +/// different region than the stored ARN still resolves the resource. +pub(crate) fn resolve_tagged_resource( + state: &crate::state::KinesisState, + resource_arn: &str, +) -> Result { + if resource_arn.contains(":channel/") { + return state + .channel_name_from_arn(resource_arn) + .map(TaggedResource::Channel) + .ok_or_else(|| resource_not_found_arn(resource_arn)); + } + state + .stream_name_from_arn(resource_arn) + .map(TaggedResource::Stream) + .ok_or_else(|| resource_not_found_arn(resource_arn)) +} + +/// The tag map `resource_arn` names, for the two mutating tag operations. +pub(crate) fn resource_tags_mut<'a>( + state: &'a mut crate::state::KinesisState, + resource_arn: &str, +) -> Result<&'a mut std::collections::BTreeMap, AwsServiceError> { + // Resolved before the mutable borrows below, and infallible from here: + // `resolve_tagged_resource` only names a resource it found in these very + // maps. + let resource = resolve_tagged_resource(state, resource_arn)?; + match resource { + TaggedResource::Stream(name) => Ok(&mut state.streams.get_mut(&name).unwrap().tags), + TaggedResource::Channel(name) => Ok(&mut state.channels.get_mut(&name).unwrap().tags), + } +} + +/// The tag map `resource_arn` names, for `ListTagsForResource`. +pub(crate) fn resource_tags<'a>( + state: &'a crate::state::KinesisState, + resource_arn: &str, +) -> Result<&'a std::collections::BTreeMap, AwsServiceError> { + let resource = resolve_tagged_resource(state, resource_arn)?; + match resource { + TaggedResource::Stream(name) => Ok(&state.streams[&name].tags), + TaggedResource::Channel(name) => Ok(&state.channels[&name].tags), + } } pub(crate) fn decode_record_data(value: &Value) -> Result, AwsServiceError> { @@ -746,7 +802,9 @@ pub(crate) fn parse_channel_streams( .filter(|value| !value.is_empty()) { Some(arn) => { - validate_string_length("GSRSchemaARN", arn, 1, 2048)?; + // `GSRSchemaARN` is capped at 512, not the 2048 the other ARN + // members share. + validate_string_length("GSRSchemaARN", arn, 1, 512)?; Some(arn.to_string()) } None => None, @@ -1160,7 +1218,15 @@ pub(crate) struct ChannelStreamFilter { /// Parse the `StreamFilter` list. Parsing up front (rather than per candidate /// channel) means a malformed filter is rejected even when the account holds /// no channels to evaluate it against. +/// +/// Each `StreamARN` is resolved against `state` the same region-tolerant way +/// `CreateChannel` resolves a source ARN, then stored in the stream's own +/// canonical form, which is what a channel holds. Without that, a caller +/// whose credential scope names a different region than the stream's stored +/// ARN filters against a string no channel can ever carry. An ARN that names +/// no existing stream is kept verbatim: it simply matches nothing. pub(crate) fn parse_channel_stream_filters( + state: &crate::state::KinesisState, value: &Value, ) -> Result, AwsServiceError> { if value.is_null() { @@ -1187,8 +1253,16 @@ pub(crate) fn parse_channel_stream_filters( Some((seconds * 1000.0).round() as i64) } }; + let stream_arn = require_channel_member(entry, "StreamARN", 2048)?; + let stream_arn = state + .stream_name_from_arn(stream_arn) + .and_then(|name| state.streams.get(&name)) + .map_or_else( + || stream_arn.to_string(), + |stream| stream.stream_arn.clone(), + ); Ok(ChannelStreamFilter { - stream_arn: require_channel_member(entry, "StreamARN", 2048)?.to_string(), + stream_arn, creation_timestamp_millis, }) }) diff --git a/crates/fakecloud-kinesis/src/service_tests.rs b/crates/fakecloud-kinesis/src/service_tests.rs index bc5f570c1..37d2a0042 100644 --- a/crates/fakecloud-kinesis/src/service_tests.rs +++ b/crates/fakecloud-kinesis/src/service_tests.rs @@ -27,6 +27,15 @@ fn request(action: &str, body: Value) -> AwsRequest { } } +/// The same request with a different credential-scope region, for the paths +/// that must resolve ARNs region-tolerantly. +fn request_in_region(action: &str, region: &str, body: Value) -> AwsRequest { + AwsRequest { + region: region.to_string(), + ..request(action, body) + } +} + fn test_stream(name: &str) -> KinesisStream { KinesisStream { stream_name: name.to_string(), @@ -3080,3 +3089,263 @@ fn channel_actions_are_supported_and_mutating() { assert!(!is_mutating_action("DescribeChannel")); assert!(!is_mutating_action("ListChannels")); } + +// ── Tags v2 against streams and channels ── + +#[test] +fn tag_resource_round_trips_stream_tags() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let arn = stream_arn_for("orders"); + + svc.tag_resource(&request( + "TagResource", + json!({ "ResourceARN": arn, "Tags": { "env": "test" } }), + )) + .unwrap(); + + let listed = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": arn }), + )) + .unwrap(), + ); + assert_eq!(listed["Tags"], json!([{ "Key": "env", "Value": "test" }])); + + svc.untag_resource(&request( + "UntagResource", + json!({ "ResourceARN": arn, "TagKeys": ["env"] }), + )) + .unwrap(); + let after = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": arn }), + )) + .unwrap(), + ); + assert_eq!(after["Tags"], json!([])); +} + +#[test] +fn tag_operations_reach_channels_by_arn() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let mut body = s3_channel_body("deliveries", "orders"); + body["Tags"] = json!({ "team": "data" }); + let created = json_response(svc.create_channel(&request("CreateChannel", body)).unwrap()); + let channel_arn = created["ChannelDescription"]["ChannelARN"] + .as_str() + .unwrap() + .to_string(); + + // The tags supplied to CreateChannel are readable, not write-only. + let listed = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": channel_arn }), + )) + .unwrap(), + ); + assert_eq!(listed["Tags"], json!([{ "Key": "team", "Value": "data" }])); + + svc.tag_resource(&request( + "TagResource", + json!({ "ResourceARN": channel_arn, "Tags": { "env": "test" } }), + )) + .unwrap(); + svc.untag_resource(&request( + "UntagResource", + json!({ "ResourceARN": channel_arn, "TagKeys": ["team"] }), + )) + .unwrap(); + + let after = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": channel_arn }), + )) + .unwrap(), + ); + assert_eq!(after["Tags"], json!([{ "Key": "env", "Value": "test" }])); + + // Tagging the channel left the source stream's own tags alone. + let stream_tags = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": stream_arn_for("orders") }), + )) + .unwrap(), + ); + assert_eq!(stream_tags["Tags"], json!([])); +} + +#[test] +fn tag_operations_resolve_channels_from_another_region() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + let created = create_channel_action(&svc, "deliveries", "orders"); + let channel_id = created["ChannelDescription"]["ChannelId"].as_str().unwrap(); + let foreign_arn = format!("arn:aws:kinesis:eu-west-1:123456789012:channel/{channel_id}"); + + svc.tag_resource(&request_in_region( + "TagResource", + "eu-west-1", + json!({ "ResourceARN": foreign_arn, "Tags": { "env": "test" } }), + )) + .unwrap(); + + let listed = json_response( + svc.list_tags_for_resource(&request( + "ListTagsForResource", + json!({ "ResourceARN": created["ChannelDescription"]["ChannelARN"] }), + )) + .unwrap(), + ); + assert_eq!(listed["Tags"], json!([{ "Key": "env", "Value": "test" }])); +} + +#[test] +fn tag_operations_reject_unknown_resources() { + let (svc, _) = make_service(); + for arn in [ + "arn:aws:kinesis:us-east-1:123456789012:channel/ghost", + "arn:aws:kinesis:us-east-1:123456789012:stream/ghost", + ] { + let body = json!({ + "ResourceARN": arn, + "Tags": { "env": "test" }, + "TagKeys": ["env"], + }); + assert_code_kinesis( + svc.tag_resource(&request("TagResource", body.clone())), + "ResourceNotFoundException", + ); + assert_code_kinesis( + svc.untag_resource(&request("UntagResource", body.clone())), + "ResourceNotFoundException", + ); + assert_code_kinesis( + svc.list_tags_for_resource(&request("ListTagsForResource", body)), + "ResourceNotFoundException", + ); + } +} + +// ── channel model bounds and region-tolerant resolution ── + +#[test] +fn create_channel_enforces_the_models_arn_lengths() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + + // An ARN of exactly `len` bytes, padded in the resource segment. + let padded = |prefix: &str, len: usize| format!("{prefix}{}", "a".repeat(len - prefix.len())); + let role = |len: usize| padded("arn:aws:iam::123456789012:role/", len); + let schema = |len: usize| padded("arn:aws:glue:us-east-1:123456789012:schema/registry/", len); + + // ServiceExecutionRoleARN is a RoleARN: 512, not the 2048 other ARN + // members share. + let mut long_role = s3_channel_body("deliveries", "orders"); + long_role["ServiceExecutionRoleARN"] = json!(role(513)); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", long_role)), + "ValidationException", + ); + let mut max_role = s3_channel_body("deliveries", "orders"); + max_role["ServiceExecutionRoleARN"] = json!(role(512)); + svc.create_channel(&request("CreateChannel", max_role)) + .unwrap(); + + // GSRSchemaARN is capped at 512 too. + let mut long_schema = s3_channel_body("schemas", "orders"); + long_schema["StreamConfigurationList"][0]["RecordConfiguration"]["GSRSchemaARN"] = + json!(schema(513)); + assert_code_kinesis( + svc.create_channel(&request("CreateChannel", long_schema)), + "ValidationException", + ); + let mut max_schema = s3_channel_body("schemas", "orders"); + max_schema["StreamConfigurationList"][0]["RecordConfiguration"]["GSRSchemaARN"] = + json!(schema(512)); + svc.create_channel(&request("CreateChannel", max_schema)) + .unwrap(); +} + +/// A CreateChannel body whose source ARN carries `region` rather than the +/// region the stream was created in. +fn s3_channel_body_in_region(name: &str, stream_name: &str, region: &str) -> Value { + let mut body = s3_channel_body(name, stream_name); + body["StreamConfigurationList"][0]["StreamARN"] = json!(format!( + "arn:aws:kinesis:{region}:123456789012:stream/{stream_name}" + )); + body +} + +#[test] +fn list_channels_filter_matches_a_cross_region_stream_arn() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + // The channel is created by a eu-west-1-scoped caller naming the stream by + // its own regional ARN; the stored source ARN is the stream's canonical + // us-east-1 one. + svc.create_channel(&request_in_region( + "CreateChannel", + "eu-west-1", + s3_channel_body_in_region("deliveries", "orders", "eu-west-1"), + )) + .unwrap(); + + let filtered = json_response( + svc.list_channels(&request_in_region( + "ListChannels", + "eu-west-1", + json!({ + "StreamFilter": [{ + "StreamARN": "arn:aws:kinesis:eu-west-1:123456789012:stream/orders", + }] + }), + )) + .unwrap(), + ); + let summaries = filtered["ChannelSummaries"].as_array().unwrap(); + assert_eq!(summaries.len(), 1, "{filtered}"); + assert_eq!(summaries[0]["ChannelName"], "deliveries"); + + // A filter naming a stream that does not exist still matches nothing. + let unmatched = json_response( + svc.list_channels(&request_in_region( + "ListChannels", + "eu-west-1", + json!({ + "StreamFilter": [{ + "StreamARN": "arn:aws:kinesis:eu-west-1:123456789012:stream/ghost", + }] + }), + )) + .unwrap(), + ); + assert!(unmatched["ChannelSummaries"].as_array().unwrap().is_empty()); +} + +#[test] +fn delete_stream_guard_holds_for_a_cross_region_caller() { + let (svc, _) = make_service(); + create_stream_action(&svc, "orders", 1); + svc.create_channel(&request_in_region( + "CreateChannel", + "eu-west-1", + s3_channel_body_in_region("deliveries", "orders", "eu-west-1"), + )) + .unwrap(); + + assert_code_kinesis( + svc.delete_stream(&request_in_region( + "DeleteStream", + "eu-west-1", + json!({ "StreamARN": "arn:aws:kinesis:eu-west-1:123456789012:stream/orders" }), + )), + "ResourceInUseException", + ); +} diff --git a/crates/fakecloud-kinesis/src/state.rs b/crates/fakecloud-kinesis/src/state.rs index a16eaf751..7cf23bf21 100644 --- a/crates/fakecloud-kinesis/src/state.rs +++ b/crates/fakecloud-kinesis/src/state.rs @@ -295,16 +295,22 @@ impl KinesisState { .map(|channel| channel.channel_name.clone()) } - /// Names of the channels that draw from `stream_arn`. A stream cannot be - /// deleted while any channel is attached to it. - pub fn channels_for_stream(&self, stream_arn: &str) -> Vec { + /// Names of the channels that draw from the stream named `stream_name`. A + /// stream cannot be deleted while any channel is attached to it. + /// + /// A channel stores its sources' canonical ARNs, which carry the region + /// of whichever credential scope created the channel. Resolving each one + /// through [`KinesisState::stream_name_from_arn`], the same way + /// `CreateChannel` resolved the caller's ARN, keeps the attachment + /// visible to a caller scoped to a different region, which a raw ARN + /// comparison would miss. + pub fn channels_for_stream(&self, stream_name: &str) -> Vec { self.channels .values() .filter(|channel| { - channel - .streams - .iter() - .any(|source| source.stream_arn == stream_arn) + channel.streams.iter().any(|source| { + self.stream_name_from_arn(&source.stream_arn).as_deref() == Some(stream_name) + }) }) .map(|channel| channel.channel_name.clone()) .collect() @@ -551,19 +557,60 @@ mod tests { assert!(restored.channels.is_empty()); } + /// A stream carrying only the members the ARN-resolution paths read. + fn insert_test_stream(state: &mut KinesisState, name: &str) { + let stream = KinesisStream { + stream_name: name.to_string(), + stream_arn: state.stream_arn(&state.region, name), + stream_status: "ACTIVE".to_string(), + stream_creation_timestamp: Utc::now(), + retention_period_hours: 24, + stream_mode: "PROVISIONED".to_string(), + encryption_type: "NONE".to_string(), + key_id: None, + shard_count: 0, + open_shard_count: 0, + tags: BTreeMap::new(), + shards: Vec::new(), + next_shard_index: 0, + enhanced_metrics: Vec::new(), + warm_throughput_mibps: None, + max_record_size_kib: None, + }; + state.streams.insert(name.to_string(), stream); + } + #[test] fn channels_for_stream_lists_attached_channels() { let mut state = KinesisState::new("123456789012", "us-east-1"); + insert_test_stream(&mut state, "orders"); + insert_test_stream(&mut state, "other"); let channel = test_channel(&state, "deliveries"); state.channels.insert("deliveries".to_string(), channel); assert_eq!( - state.channels_for_stream(&state.stream_arn(&state.region, "orders")), + state.channels_for_stream("orders"), + vec!["deliveries".to_string()] + ); + assert!(state.channels_for_stream("other").is_empty()); + } + + #[test] + fn channels_for_stream_ignores_the_source_arns_region() { + // The channel was created by a caller scoped to us-east-1, so it holds + // a us-east-1 source ARN; a caller scoped elsewhere still resolves to + // the same stream name and must see the attachment. + let mut state = KinesisState::new("123456789012", "eu-west-1"); + insert_test_stream(&mut state, "orders"); + let mut channel = test_channel(&state, "deliveries"); + channel.streams[0].stream_arn = + "arn:aws:kinesis:us-east-1:123456789012:stream/orders".to_string(); + state.channels.insert("deliveries".to_string(), channel); + + assert_eq!( + state.channels_for_stream("orders"), vec!["deliveries".to_string()] ); - assert!(state - .channels_for_stream(&state.stream_arn(&state.region, "other")) - .is_empty()); } #[test] diff --git a/crates/fakecloud-server/src/support_attachments.rs b/crates/fakecloud-server/src/support_attachments.rs index 185635729..78d0f31c7 100644 --- a/crates/fakecloud-server/src/support_attachments.rs +++ b/crates/fakecloud-server/src/support_attachments.rs @@ -9,11 +9,13 @@ //! `X-Amz-Signature` query parameter: `fakecloud_support::dataplane` checks it //! against the value recorded when the link was issued, along with the link's //! expiry. All the state handling lives there; this module is the transport -//! shim. +//! shim: it buffers the `PUT` body under the server-wide request cap (a part +//! can be up to 100 MB, far past axum's 2 MB extractor default) and renders a +//! download's caller-supplied file name into a header value safely. use std::collections::HashMap; -use axum::body::Bytes; +use axum::body::Body; use axum::extract::{Path, Query, State}; use axum::http::{header, StatusCode}; use axum::response::IntoResponse; @@ -50,12 +52,54 @@ fn signature(params: &HashMap) -> String { params.get("X-Amz-Signature").cloned().unwrap_or_default() } +/// The `Content-Disposition` header for a downloaded attachment. +/// +/// The file name is whatever the caller passed to `GetAttachmentUploadLinks` / +/// `AddAttachmentsToSet`, so it is not safe to interpolate: a quote would close +/// the quoted string and let the rest of the name inject header parameters of +/// its own, and a CR/LF (or any other control character) makes a `HeaderValue` +/// that cannot be built at all, turning a download into a bare 500. Quotes and +/// backslashes are escaped the way RFC 6266's quoted-string does, anything +/// outside printable ASCII is dropped, and a name with nothing usable left +/// falls back to a generic one. +fn content_disposition(file_name: &str) -> String { + let mut quoted = String::with_capacity(file_name.len()); + for ch in file_name.chars() { + match ch { + '"' | '\\' => { + quoted.push('\\'); + quoted.push(ch); + } + c if c == ' ' || c.is_ascii_graphic() => quoted.push(c), + // Control characters (CR/LF included) and non-ASCII are dropped: + // neither can appear verbatim in a header value. + _ => {} + } + } + if quoted.trim().is_empty() { + quoted = "attachment".to_string(); + } + format!("attachment; filename=\"{quoted}\"") +} + async fn put_part( Path((account_id, upload_id, part_index)): Path<(String, String, i64)>, Query(params): Query>, State(ctx): State, - body: Bytes, + body: Body, ) -> impl IntoResponse { + // Buffer the body by hand under the server-wide cap, exactly as the AWS + // dispatcher does. Extracting `Bytes` instead would apply axum's default + // 2 MB body limit, which every real part upload (up to 100 MB per the + // model) exceeds. + let body = match axum::body::to_bytes(body, fakecloud_core::dispatch::max_request_body_bytes()) + .await + { + Ok(bytes) => bytes, + Err(_) => { + return (StatusCode::PAYLOAD_TOO_LARGE, "part body too large").into_response(); + } + }; let outcome = fakecloud_support::dataplane::put_upload_part( &ctx.support_state, &account_id, @@ -85,6 +129,11 @@ async fn put_part( "the attachment upload is already complete", ) .into_response(), + PutPartOutcome::InvalidSize(expected, actual) => ( + StatusCode::BAD_REQUEST, + format!("this part must be exactly {expected} bytes, got {actual}"), + ) + .into_response(), } } @@ -104,10 +153,7 @@ async fn download_attachment( StatusCode::OK, [ (header::CONTENT_TYPE, "application/octet-stream".to_string()), - ( - header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{file_name}\""), - ), + (header::CONTENT_DISPOSITION, content_disposition(&file_name)), ], bytes, ) @@ -125,15 +171,38 @@ async fn download_attachment( #[cfg(test)] mod tests { use super::*; - use axum::body::Body; use axum::http::Request; use fakecloud_core::multi_account::MultiAccountState; - use fakecloud_support::state::{AttachmentUpload, UploadPart, UPLOAD_NOT_READY}; + use fakecloud_support::state::{AttachmentUpload, DownloadGrant, UploadPart, UPLOAD_NOT_READY}; use parking_lot::RwLock; + use serde_json::json; use std::sync::Arc; use tower::ServiceExt; const ACCOUNT: &str = "000000000000"; + /// Comfortably past axum's 2 MB `DefaultBodyLimit`, still under one part. + const BIG_PART_LEN: usize = 3 * 1024 * 1024; + + fn upload(file_name: &str, file_size_bytes: i64) -> AttachmentUpload { + AttachmentUpload { + upload_id: "unused".to_string(), + file_name: file_name.to_string(), + file_size_bytes, + part_size_bytes: 5 * 1024 * 1024, + total_parts: 1, + status: UPLOAD_NOT_READY.to_string(), + expiry: "2999-01-01T00:00:00.000Z".to_string(), + parts: vec![UploadPart { + part_index: 1, + signature: "sig-1".to_string(), + expiry: "2999-01-01T00:00:00.000Z".to_string(), + etag: None, + data: None, + completed: false, + }], + attachment_id: None, + } + } fn context() -> SupportAttachmentRoutesContext { let state: SharedSupportState = Arc::new(RwLock::new(MultiAccountState::new( @@ -144,24 +213,27 @@ mod tests { { let mut guard = state.write(); let data = guard.get_or_create(ACCOUNT); - data.attachment_uploads.insert( - "upload-1".to_string(), - AttachmentUpload { - upload_id: "upload-1".to_string(), - file_name: "log.txt".to_string(), - file_size_bytes: 5, - part_size_bytes: 5 * 1024 * 1024, - total_parts: 1, - status: UPLOAD_NOT_READY.to_string(), + let mut small = upload("log.txt", 5); + small.upload_id = "upload-1".to_string(); + data.attachment_uploads + .insert("upload-1".to_string(), small); + // A part big enough to prove the route is not capped at axum's + // 2 MB extractor default. + let mut big = upload("big.bin", BIG_PART_LEN as i64); + big.upload_id = "upload-big".to_string(); + data.attachment_uploads + .insert("upload-big".to_string(), big); + + // A stored attachment whose file name is hostile to a header. + data.attachments.insert( + "attachment-1".to_string(), + json!({ "fileName": "re\"port\r\nX-Injected: yes.txt", "data": "aGVsbG8=" }), + ); + data.attachment_downloads.insert( + "dl-sig".to_string(), + DownloadGrant { + attachment_id: "attachment-1".to_string(), expiry: "2999-01-01T00:00:00.000Z".to_string(), - parts: vec![UploadPart { - part_index: 1, - signature: "sig-1".to_string(), - expiry: "2999-01-01T00:00:00.000Z".to_string(), - etag: None, - data: None, - }], - attachment_id: None, }, ); } @@ -213,6 +285,108 @@ mod tests { assert_eq!(response.status(), StatusCode::FORBIDDEN); } + #[tokio::test] + async fn put_accepts_a_part_larger_than_axums_default_body_limit() { + let ctx = context(); + let state = ctx.support_state.clone(); + let part = vec![b'z'; BIG_PART_LEN]; + let response = router(ctx) + .oneshot( + Request::builder() + .method("PUT") + .uri("/_fakecloud/support/attachments/uploads/000000000000/upload-big/1?X-Amz-Signature=sig-1") + .body(Body::from(part)) + .unwrap(), + ) + .await + .unwrap(); + // Extracting `Bytes` would have made this a 413 at 2 MB. + assert_eq!(response.status(), StatusCode::OK); + let guard = state.read(); + let stored = guard.get(ACCOUNT).unwrap().attachment_uploads["upload-big"] + .part(1) + .unwrap() + .data + .clone() + .unwrap(); + // Base64 of n bytes is 4 * ceil(n / 3) characters. + assert_eq!(stored.len(), 4 * BIG_PART_LEN.div_ceil(3)); + } + + #[tokio::test] + async fn put_of_a_wrongly_sized_part_is_rejected() { + let response = router(context()) + .oneshot( + Request::builder() + .method("PUT") + .uri("/_fakecloud/support/attachments/uploads/000000000000/upload-1/1?X-Amz-Signature=sig-1") + .body(Body::from("hi")) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn download_escapes_a_hostile_file_name() { + let response = router(context()) + .oneshot( + Request::builder() + .uri("/_fakecloud/support/attachments/downloads/000000000000/attachment-1?X-Amz-Signature=dl-sig") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + // The CR/LF in the name would otherwise make an unbuildable header + // value and turn the download into a bare 500. + assert_eq!(response.status(), StatusCode::OK); + let disposition = response + .headers() + .get(header::CONTENT_DISPOSITION) + .unwrap() + .to_str() + .unwrap(); + assert_eq!( + disposition, + "attachment; filename=\"re\\\"portX-Injected: yes.txt\"" + ); + assert!(response.headers().get("x-injected").is_none()); + } + + #[test] + fn content_disposition_escapes_quotes_and_drops_control_characters() { + assert_eq!( + content_disposition("log.txt"), + "attachment; filename=\"log.txt\"" + ); + // A quote is escaped rather than closing the quoted string, so it + // cannot start a parameter of its own. + assert_eq!( + content_disposition("a\"; filename*=UTF-8''evil"), + "attachment; filename=\"a\\\"; filename*=UTF-8''evil\"" + ); + // A backslash cannot escape the closing quote either. + assert_eq!(content_disposition("a\\"), "attachment; filename=\"a\\\\\""); + // CR/LF and other control characters are dropped entirely. + assert_eq!( + content_disposition("a\r\nX-Injected: yes\tb"), + "attachment; filename=\"aX-Injected: yesb\"" + ); + // Nothing usable left falls back to a generic name. + assert_eq!( + content_disposition("\r\n\u{1}"), + "attachment; filename=\"attachment\"" + ); + // Non-ASCII cannot appear verbatim in a header value, so it is dropped + // rather than failing the whole download. + assert_eq!( + content_disposition("relat\u{f3}rio.txt"), + "attachment; filename=\"relatrio.txt\"" + ); + } + #[tokio::test] async fn download_without_a_grant_is_forbidden() { let response = router(context()) diff --git a/crates/fakecloud-support/src/dataplane.rs b/crates/fakecloud-support/src/dataplane.rs index 8ae7d36d1..49dfe93e1 100644 --- a/crates/fakecloud-support/src/dataplane.rs +++ b/crates/fakecloud-support/src/dataplane.rs @@ -12,6 +12,12 @@ //! constant time, plus the link's own expiry. A link that was never issued, was //! tampered with, or has expired is refused. //! +//! An authorised `PUT` still has to carry the right number of bytes: the +//! declared `fileSizeBytes` and the upload's `partSizeBytes` fix every part's +//! length exactly (parts 1..n-1 are one part size, part n is the remainder), so +//! a part of any other length is refused rather than assembled into a file of +//! the wrong size. +//! //! The server crate mounts these as routes; everything that touches Support //! state lives here so the transport layer stays a thin shim. @@ -34,6 +40,10 @@ pub enum PutPartOutcome { Expired, /// The upload was already completed; its links no longer accept bytes. AlreadyCompleted, + /// The body is not the size this part must have. Every part but the last + /// is exactly `partSizeBytes` and the last carries the remainder of the + /// declared `fileSizeBytes`. Carries `(expected, actual)`. + InvalidSize(i64, i64), } /// Result of a presigned attachment download. @@ -85,6 +95,12 @@ pub fn put_upload_part( if upload.expiry <= now { return PutPartOutcome::Expired; } + // The part's size is fixed by the declared file size and the part size the + // upload was split into, so a part that is not exactly that long would + // assemble into a file of the wrong length. Checked before the bytes are + // stored, and only after the link itself is authorised so a forged link + // learns nothing about the upload. + let expected_len = upload.expected_part_len(part_index); let Some(part) = upload.part_mut(part_index) else { return PutPartOutcome::NotFound; }; @@ -94,6 +110,11 @@ pub fn put_upload_part( if part.expiry <= now { return PutPartOutcome::Expired; } + if let Some(expected) = expected_len { + if body.len() as i64 != expected { + return PutPartOutcome::InvalidSize(expected, body.len() as i64); + } + } let etag = part_etag(body); part.data = Some(base64::engine::general_purpose::STANDARD.encode(body)); part.etag = Some(etag.clone()); @@ -181,12 +202,43 @@ mod tests { expiry: expiry.into(), etag: None, data: None, + completed: false, }], attachment_id: None, }, ); } + /// A two-part upload of a file one byte longer than a single part. + fn seed_multipart(state: &SharedSupportState, part_size: i64) { + let expiry = iso_in(LINK_TTL_SECONDS); + let mut guard = state.write(); + let data: &mut SupportData = guard.get_or_create(ACCOUNT); + data.attachment_uploads.insert( + "upload-2".into(), + AttachmentUpload { + upload_id: "upload-2".into(), + file_name: "big.bin".into(), + file_size_bytes: part_size + 1, + part_size_bytes: part_size, + total_parts: 2, + status: UPLOAD_NOT_READY.into(), + expiry: expiry.clone(), + parts: (1..=2) + .map(|part_index| UploadPart { + part_index, + signature: format!("sig-{part_index}"), + expiry: expiry.clone(), + etag: None, + data: None, + completed: false, + }) + .collect(), + attachment_id: None, + }, + ); + } + #[test] fn put_stores_bytes_and_returns_an_etag() { let state = state(); @@ -257,6 +309,56 @@ mod tests { ); } + #[test] + fn put_rejects_a_part_of_the_wrong_size() { + let state = state(); + // The seeded upload declares a 5-byte file in a single part. + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 1, "sig-1", b"hi"), + PutPartOutcome::InvalidSize(5, 2) + ); + // Nothing was stored, so the part is still outstanding. + assert!( + state.read().get(ACCOUNT).unwrap().attachment_uploads["upload-1"] + .part(1) + .unwrap() + .data + .is_none() + ); + + // A non-final part must be exactly one part size; the last part + // carries the remainder and nothing more. + seed_multipart(&state, 8); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-2", 1, "sig-1", b"short"), + PutPartOutcome::InvalidSize(8, 5) + ); + assert!(matches!( + put_upload_part(&state, ACCOUNT, "upload-2", 1, "sig-1", b"12345678"), + PutPartOutcome::Stored(_) + )); + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-2", 2, "sig-2", b"toolong"), + PutPartOutcome::InvalidSize(1, 7) + ); + assert!(matches!( + put_upload_part(&state, ACCOUNT, "upload-2", 2, "sig-2", b"9"), + PutPartOutcome::Stored(_) + )); + } + + #[test] + fn put_authorises_before_it_measures() { + let state = state(); + seed_upload(&state, &iso_in(LINK_TTL_SECONDS)); + // A forged link is refused without revealing the expected part size. + assert_eq!( + put_upload_part(&state, ACCOUNT, "upload-1", 1, "forged", b"hi"), + PutPartOutcome::Forbidden + ); + } + fn seed_attachment(state: &SharedSupportState, expiry: &str) { let mut guard = state.write(); let data: &mut SupportData = guard.get_or_create(ACCOUNT); diff --git a/crates/fakecloud-support/src/lib.rs b/crates/fakecloud-support/src/lib.rs index b43e703dc..6dcbb665a 100644 --- a/crates/fakecloud-support/src/lib.rs +++ b/crates/fakecloud-support/src/lib.rs @@ -30,10 +30,12 @@ //! (`AddAttachmentsToSet` mints/extends an `attachmentSetId` with an //! `expiryTime`; `DescribeAttachment` returns a stored attachment by id). //! Attachment uploads are real end to end: `GetAttachmentUploadLinks` records -//! an upload and issues one presigned `PUT` link per 5 MiB part pointing back -//! at this server, the [`dataplane`] routes the server mounts store the bytes -//! and return an `ETag` per part, `CompleteAttachmentUpload` verifies every -//! part and its `ETag` before assembling the attachment, and +//! an upload and issues presigned `PUT` links, at most ten per call, for one +//! half-open range of its 5 MiB parts, pointing back at this server; the +//! [`dataplane`] routes the server mounts store each correctly sized part and +//! return its `ETag`; `CompleteAttachmentUpload` verifies the parts named in it +//! against those `ETag`s and assembles the attachment once every part has been +//! reported (it may be called one part at a time); and //! `GetAttachmentDownloadLink` issues a presigned `GET` link that serves it //! back. A completed upload attaches to a case or communication through //! `uploadIds`. diff --git a/crates/fakecloud-support/src/persistence.rs b/crates/fakecloud-support/src/persistence.rs index 59a8ba50b..2e2c83e02 100644 --- a/crates/fakecloud-support/src/persistence.rs +++ b/crates/fakecloud-support/src/persistence.rs @@ -133,6 +133,7 @@ mod tests { expiry: "2999-01-01T00:00:00.000Z".into(), etag: Some("\"abc\"".into()), data: Some("aGVsbG8=".into()), + completed: false, }], attachment_id: None, }; diff --git a/crates/fakecloud-support/src/service.rs b/crates/fakecloud-support/src/service.rs index 201459354..2e7c1d31a 100644 --- a/crates/fakecloud-support/src/service.rs +++ b/crates/fakecloud-support/src/service.rs @@ -9,9 +9,11 @@ //! `AttachmentSetIdNotFound`. //! //! Attachment uploads are the presigned flow: `GetAttachmentUploadLinks` -//! records an upload and hands out one presigned `PUT` link per part, -//! `CompleteAttachmentUpload` verifies the parts and their `ETag`s and -//! assembles the attachment, `DescribeAttachmentUploadStatus` reports the +//! records an upload and hands out presigned `PUT` links, at most ten per call, +//! for one half-open range of parts; `CompleteAttachmentUpload` verifies the +//! parts named in it against their uploaded `ETag`s and assembles the +//! attachment once every part has been reported (it may be called one part at +//! a time); `DescribeAttachmentUploadStatus` reports the //! recorded progress, and `GetAttachmentDownloadLink` mints a presigned `GET` //! link for a stored attachment. The links point back at this fakecloud and //! are served by [`crate::dataplane`], so they really do transfer bytes. @@ -40,6 +42,7 @@ use crate::shared::{ attachment_download_path, current_year, display_id, iso_in, iso_now, new_attachment_id, new_attachment_set_id, new_case_id, new_signature, new_upload_id, presigned_url, str_member, upload_part_path, DEFAULT_PART_SIZE_BYTES, EXAMPLE_ACCESS_KEY_ID, LINK_TTL_SECONDS, + MAX_UPLOAD_URLS_PER_CALL, }; use crate::state::{ AttachmentUpload, DownloadGrant, SharedSupportState, SupportData, UploadPart, UPLOAD_FAILED, @@ -586,10 +589,16 @@ impl SupportService { // ---- Presigned attachment uploads / downloads ------------------------- - /// Start (or resume) a multipart attachment upload and hand out one - /// presigned `PUT` link per part. Every link is recorded in state with its - /// own signature and expiry, so the data-plane route can authorise the + /// Start (or resume) a multipart attachment upload and hand out presigned + /// `PUT` links for one range of parts. Every link is recorded in state with + /// its own signature and expiry, so the data-plane route can authorise the /// `PUT` that follows. + /// + /// `uploadRange` is half-open: `startIndex` is inclusive, `endIndex` is + /// exclusive, and the range may cover at most [`MAX_UPLOAD_URLS_PER_CALL`] + /// parts, which is also the cap on a call that names no range. A caller + /// pages through a large file by feeding the returned `nextIndex` back as + /// the next `startIndex`. fn get_attachment_upload_links( &self, req: &AwsRequest, @@ -619,7 +628,9 @@ impl SupportService { let endpoint = link_endpoint(req, d); let region = link_region(req, d); - let upload_id = match &requested_upload { + // A new upload is built but not stored until the requested range + // has been validated, so a rejected call leaves no orphan behind. + let (upload_id, new_upload) = match &requested_upload { Some(id) => { let existing = d .attachment_uploads @@ -631,7 +642,7 @@ impl SupportService { if existing.status == UPLOAD_FAILED || existing.expiry <= now { return Err(upload_expired(id)); } - id.clone() + (id.clone(), None) } None => { let id = new_upload_id(); @@ -642,10 +653,10 @@ impl SupportService { } else { (file_size as u64).div_ceil(DEFAULT_PART_SIZE_BYTES as u64) as i64 }; - d.attachment_uploads.insert( + ( id.clone(), - AttachmentUpload { - upload_id: id.clone(), + Some(AttachmentUpload { + upload_id: id, file_name: file_name.clone(), file_size_bytes: file_size.max(0), part_size_bytes: DEFAULT_PART_SIZE_BYTES, @@ -654,27 +665,71 @@ impl SupportService { expiry: expiry.clone(), parts: Vec::new(), attachment_id: None, - }, - ); - id + }), + ) + } + }; + + // Without an explicit start, resume at the first part whose bytes + // have not arrived; `endIndex` is exclusive and defaults to a full + // page of links, capped by the part count. + let (total_parts, default_start) = match &new_upload { + Some(upload) => (upload.total_parts, 1), + None => { + let upload = &d.attachment_uploads[&upload_id]; + ( + upload.total_parts, + upload.next_unuploaded_index().unwrap_or(1), + ) } }; + let start = range_start.unwrap_or(default_start); + let end = range_end.unwrap_or(start + MAX_UPLOAD_URLS_PER_CALL); + let end = validate_upload_range(&upload_id, start, end, total_parts)?; + if let Some(upload) = new_upload { + d.attachment_uploads.insert(upload_id.clone(), upload); + } let upload = d .attachment_uploads .get_mut(&upload_id) .expect("upload was just inserted or looked up"); - let total_parts = upload.total_parts; - // Without an explicit range, hand out links for everything that is - // still outstanding. - let start = range_start - .unwrap_or_else(|| upload.next_index().max(1)) - .clamp(1, total_parts); - let end = range_end.unwrap_or(total_parts).clamp(start, total_parts); + // A part link never outlives the upload it belongs to, so an + // upload's deadline cannot be pushed back by asking for links + // again. + let link_expiry = expiry.min(upload.expiry.clone()); let mut upload_urls = Vec::new(); - for part_index in start..=end { - let signature = new_signature(); + for part_index in start..end { + // A part that already has a live link keeps it: the caller may + // still be uploading to the URL it was handed, and rotating the + // signature would break it mid-flight. Only a part with no link + // yet, or one whose link has expired, gets a fresh signature. + let reusable = upload + .part(part_index) + .filter(|p| p.expiry > now) + .map(|p| (p.signature.clone(), p.expiry.clone())); + let (signature, part_expiry) = match reusable { + Some(existing) => existing, + None => { + let signature = new_signature(); + match upload.part_mut(part_index) { + Some(part) => { + part.signature = signature.clone(); + part.expiry = link_expiry.clone(); + } + None => upload.parts.push(UploadPart { + part_index, + signature: signature.clone(), + expiry: link_expiry.clone(), + etag: None, + data: None, + completed: false, + }), + } + (signature, link_expiry.clone()) + } + }; let url = presigned_url( &endpoint, &upload_part_path(&account, &upload_id, part_index), @@ -683,46 +738,40 @@ impl SupportService { &signature, LINK_TTL_SECONDS, ); - // Re-issuing a link for a part that already has one replaces - // its signature: the old link stops working, the uploaded - // bytes (if any) are kept so a resume does not lose them. - if upload.part(part_index).is_some() { - let part = upload.part_mut(part_index).expect("checked above"); - part.signature = signature; - part.expiry = expiry.clone(); - } else { - upload.parts.push(UploadPart { - part_index, - signature, - expiry: expiry.clone(), - etag: None, - data: None, - }); - } upload_urls.push(json!({ "url": url, "partIndex": part_index, - "expiryDate": expiry, + "expiryDate": part_expiry, })); } upload.parts.sort_by_key(|p| p.part_index); - upload.expiry = expiry.clone(); + // `nextIndex` is where the caller should ask for the next page of + // links: the part after the last one this call covered, or null + // once links for every part have been returned. + let next_index = if end > total_parts { + Value::Null + } else { + json!(end) + }; Ok(ok(json!({ "uploadId": upload_id, "partSizeBytes": upload.part_size_bytes, "totalParts": total_parts, - "nextIndex": upload.next_index(), + "nextIndex": next_index, "uploadUrls": upload_urls, }))) }) } - /// Finalise an upload: every part must have been `PUT` to its link and the - /// client must echo back the `ETag` each `PUT` returned. On success the - /// parts are concatenated into a real attachment, retrievable with - /// `DescribeAttachment` / `GetAttachmentDownloadLink` and attachable to a - /// case through `uploadIds`. + /// Report one or more parts of an upload as complete. Each named part must + /// have been `PUT` to its link and the client must echo back the `ETag` + /// that `PUT` returned; the model allows one part per call or several, so + /// only the parts named here are validated and recorded. Once every part + /// has been reported the parts are concatenated into a real attachment, + /// retrievable with `DescribeAttachment` / `GetAttachmentDownloadLink` and + /// attachable to a case through `uploadIds`; until then the upload stays + /// `attachment-not-ready`. fn complete_attachment_upload( &self, req: &AwsRequest, @@ -749,12 +798,9 @@ impl SupportService { let now = iso_now(); self.with_account_mut(req, |d| { - // Clone the record so the checks below can run while the - // attachment map is mutated afterwards. let upload = d .attachment_uploads - .get(&upload_id) - .cloned() + .get_mut(&upload_id) .ok_or_else(|| upload_id_not_found(&upload_id))?; if upload.status == UPLOAD_READY { return Err(upload_already_completed(&upload_id)); @@ -762,56 +808,73 @@ impl SupportService { if upload.status == UPLOAD_FAILED || upload.expiry <= now { return Err(upload_expired(&upload_id)); } - - for (part_index, _) in &claimed { - if upload.part(*part_index).is_none() { - return Err(validation_error(format!( - "Upload {upload_id} has no part {part_index}." - ))); - } + // A call that names no part would report nothing as complete; + // `completedUploads` is required precisely so it says which parts + // this call finishes. + if claimed.is_empty() { + return Err(validation_error(format!( + "completedUploads must name at least one part of upload {upload_id}." + ))); } - let mut bytes: Vec = Vec::new(); - for part_index in 1..=upload.total_parts { - let part = upload.part(part_index).ok_or_else(|| { - validation_error(format!( - "No upload link was issued for part {part_index} of upload {upload_id}." - )) + // Validate every part named in this call before recording any of + // them, so a call that names a bad part changes nothing. + for (part_index, claimed_etag) in &claimed { + let part = upload.part(*part_index).ok_or_else(|| { + validation_error(format!("Upload {upload_id} has no part {part_index}.")) })?; - let (Some(stored_etag), Some(data)) = (&part.etag, &part.data) else { + let (Some(stored_etag), Some(_)) = (&part.etag, &part.data) else { return Err(validation_error(format!( "Part {part_index} of upload {upload_id} was never uploaded." ))); }; - let claimed_etag = claimed - .iter() - .find(|(i, _)| *i == part_index) - .map(|(_, tag)| tag.as_str()) - .ok_or_else(|| { - validation_error(format!( - "completedUploads is missing part {part_index} of upload {upload_id}." - )) - })?; if !etags_match(stored_etag, claimed_etag) { return Err(validation_error(format!( "The eTag given for part {part_index} of upload {upload_id} does not match the uploaded part." ))); } - let mut decoded = base64::engine::general_purpose::STANDARD - .decode(data) - .map_err(|err| { - internal_server_error(format!( - "part {part_index} of upload {upload_id} could not be decoded: {err}" - )) - })?; - bytes.append(&mut decoded); + } + for (part_index, _) in &claimed { + if let Some(part) = upload.part_mut(*part_index) { + part.completed = true; + } + } + + // The service assembles the file only after every part has been + // reported; until then the upload keeps accepting further calls. + if !upload.all_parts_completed() { + return Ok(ok(json!({ "uploadStatus": UPLOAD_NOT_READY }))); + } + + // Take the parts out rather than cloning them: the payloads are + // the whole file, and once it is assembled they are dropped so the + // bytes are not kept twice in memory and in every snapshot. + let mut parts = std::mem::take(&mut upload.parts); + let file_name = upload.file_name.clone(); + let file_size_bytes = upload.file_size_bytes; + let total_parts = upload.total_parts; + + let bytes = match assemble_parts(&upload_id, &parts, total_parts, file_size_bytes) { + Ok(bytes) => bytes, + Err(err) => { + // Put the parts back so the uploaded bytes are not lost and + // the caller can retry. + d.attachment_uploads + .get_mut(&upload_id) + .expect("looked up above") + .parts = parts; + return Err(err); + } + }; + for part in &mut parts { + part.data = None; } let attachment_id = new_attachment_id(); d.attachments.insert( attachment_id.clone(), json!({ - "fileName": upload.file_name, + "fileName": file_name, "data": base64::engine::general_purpose::STANDARD.encode(&bytes), }), ); @@ -819,6 +882,7 @@ impl SupportService { .attachment_uploads .get_mut(&upload_id) .expect("looked up above"); + stored.parts = parts; stored.status = UPLOAD_READY.to_string(); stored.attachment_id = Some(attachment_id); @@ -839,8 +903,8 @@ impl SupportService { .get(&upload_id) .ok_or_else(|| upload_id_not_found(&upload_id))?; // An upload whose links expired before it was completed can never - // be completed, so report it as failed even though the sweep that - // records that only runs on load. + // be completed, so report it as failed even when the sweep that + // records that has not run since it expired. let status = if upload.status == UPLOAD_NOT_READY && upload.expiry <= now { UPLOAD_FAILED } else { @@ -892,6 +956,11 @@ impl SupportService { &signature, LINK_TTL_SECONDS, ); + // Each link mints a grant keyed by its own signature, so a + // long-running process would otherwise accumulate dead grants + // until the next snapshot load swept them. Settle the expired ones + // (grants and uploads alike) before adding another. + d.reconcile(); d.attachment_downloads.insert( signature, DownloadGrant { @@ -1125,6 +1194,82 @@ fn link_region(req: &AwsRequest, d: &SupportData) -> String { } } +/// Resolve the half-open `uploadRange` a `GetAttachmentUploadLinks` call asked +/// for into the exclusive end index to hand links out up to. `startIndex` is +/// inclusive and `endIndex` exclusive, the range covers at most +/// [`MAX_UPLOAD_URLS_PER_CALL`] parts, and an end past the last part is capped +/// there rather than refused (the file simply has fewer parts left). +fn validate_upload_range( + upload_id: &str, + start: i64, + end: i64, + total_parts: i64, +) -> Result { + if start < 1 { + return Err(validation_error( + "uploadRange.startIndex must be at least 1; part indexes start at 1.", + )); + } + if start > total_parts { + return Err(validation_error(format!( + "uploadRange.startIndex {start} is past the last part of upload {upload_id}, which has {total_parts} parts." + ))); + } + if end <= start { + return Err(validation_error(format!( + "uploadRange.endIndex {end} must be greater than startIndex {start}; endIndex is exclusive." + ))); + } + if end - start > MAX_UPLOAD_URLS_PER_CALL { + return Err(validation_error(format!( + "uploadRange size (endIndex - startIndex) must not exceed {MAX_UPLOAD_URLS_PER_CALL}." + ))); + } + Ok(end.min(total_parts + 1)) +} + +/// Concatenate an upload's parts into the file they make up. Every part must +/// still hold its bytes, and the assembled length must be the `fileSizeBytes` +/// the upload declared (an upload that declared none has nothing to check). +fn assemble_parts( + upload_id: &str, + parts: &[UploadPart], + total_parts: i64, + file_size_bytes: i64, +) -> Result, AwsServiceError> { + let mut bytes: Vec = Vec::new(); + for part_index in 1..=total_parts { + let part = parts + .iter() + .find(|p| p.part_index == part_index) + .ok_or_else(|| { + validation_error(format!( + "No upload link was issued for part {part_index} of upload {upload_id}." + )) + })?; + let Some(data) = &part.data else { + return Err(validation_error(format!( + "Part {part_index} of upload {upload_id} was never uploaded." + ))); + }; + let mut decoded = base64::engine::general_purpose::STANDARD + .decode(data) + .map_err(|err| { + internal_server_error(format!( + "part {part_index} of upload {upload_id} could not be decoded: {err}" + )) + })?; + bytes.append(&mut decoded); + } + if file_size_bytes > 0 && bytes.len() as i64 != file_size_bytes { + return Err(validation_error(format!( + "The parts of upload {upload_id} assemble to {} bytes, but the upload declared {file_size_bytes} bytes.", + bytes.len() + ))); + } + Ok(bytes) +} + /// Compare an `ETag` the client echoed back against the one the data plane /// issued. Clients quote it, strip the quotes, or (via XML-encoding layers in /// some SDKs and Terraform) send the quotes as numeric entities, so normalise @@ -1532,8 +1677,9 @@ mod tests { assert!(links["uploadId"].as_str().unwrap().starts_with("upload-")); assert_eq!(links["partSizeBytes"], 5 * 1024 * 1024); assert_eq!(links["totalParts"], 1); - // Nothing uploaded yet, so part 1 is still outstanding. - assert_eq!(links["nextIndex"], 1); + // The only part's URL was returned, so there is no next page to ask + // for. + assert!(links["nextIndex"].is_null(), "{links}"); let url = links["uploadUrls"][0]["url"].as_str().unwrap(); assert!(url.contains("/_fakecloud/support/attachments/uploads/000000000000/")); assert!(url.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256")); @@ -1633,6 +1779,16 @@ mod tests { ); } + /// The part indexes a `GetAttachmentUploadLinks` response handed out. + fn issued_parts(links: &Value) -> Vec { + links["uploadUrls"] + .as_array() + .unwrap() + .iter() + .map(|u| u["partIndex"].as_i64().unwrap()) + .collect() + } + #[test] fn multipart_upload_splits_into_five_mib_parts() { let svc = service(); @@ -1644,9 +1800,11 @@ mod tests { .unwrap(), ); assert_eq!(links["totalParts"], 3); - assert_eq!(links["uploadUrls"].as_array().unwrap().len(), 3); + assert_eq!(issued_parts(&links), vec![1, 2, 3]); + assert!(links["nextIndex"].is_null(), "{links}"); - // A resume asks for a sub-range of the same upload. + // A resume asks for a sub-range of the same upload. `endIndex` is + // exclusive, so 2..4 is parts 2 and 3. let upload_id = links["uploadId"].as_str().unwrap().to_string(); let resumed = body_of( &svc.get_attachment_upload_links( @@ -1654,19 +1812,205 @@ mod tests { &json!({ "fileName": "big.bin", "uploadId": upload_id, - "uploadRange": { "startIndex": 2, "endIndex": 3 }, + "uploadRange": { "startIndex": 2, "endIndex": 4 }, }), ) .unwrap(), ); assert_eq!(resumed["uploadId"], upload_id); - let parts: Vec = resumed["uploadUrls"] - .as_array() + assert_eq!(issued_parts(&resumed), vec![2, 3]); + assert!(resumed["nextIndex"].is_null(), "{resumed}"); + + // A one-part range is startIndex..startIndex + 1. + let single = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ + "fileName": "big.bin", + "uploadId": upload_id, + "uploadRange": { "startIndex": 2, "endIndex": 3 }, + }), + ) + .unwrap(), + ); + assert_eq!(issued_parts(&single), vec![2]); + // Part 3 still has no URL from this call, so that is where the caller + // picks up. + assert_eq!(single["nextIndex"], 3, "{single}"); + } + + #[test] + fn upload_links_are_paged_ten_at_a_time() { + let svc = service(); + // 25 parts: far more than one call may hand out. + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "huge.bin", "fileSizeBytes": 121 * 1024 * 1024 }), + ) + .unwrap(), + ); + assert_eq!(links["totalParts"], 25); + assert_eq!(issued_parts(&links), (1..=10).collect::>()); + assert_eq!(links["nextIndex"], 11, "{links}"); + + // Paging with the returned nextIndex walks the rest of the file. + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let mut next = links["nextIndex"].as_i64().unwrap(); + let mut seen: Vec = (1..=10).collect(); + while next != 0 { + let page = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ + "fileName": "huge.bin", + "uploadId": upload_id, + "uploadRange": { "startIndex": next }, + }), + ) + .unwrap(), + ); + seen.extend(issued_parts(&page)); + next = page["nextIndex"].as_i64().unwrap_or(0); + } + assert_eq!(seen, (1..=25).collect::>()); + } + + #[test] + fn upload_range_is_validated() { + let svc = service(); + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "huge.bin", "fileSizeBytes": 121 * 1024 * 1024 }), + ) + .unwrap(), + ); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let range = |start: i64, end: i64| { + json!({ + "fileName": "huge.bin", + "uploadId": upload_id, + "uploadRange": { "startIndex": start, "endIndex": end }, + }) + }; + + // More than ten URLs in one call. + let err = expect_err(svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &range(1, 12), + )); + assert!(format!("{err:?}").contains("must not exceed 10"), "{err:?}"); + + // An empty or inverted half-open range. + let err = expect_err(svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &range(3, 3), + )); + assert!( + format!("{err:?}").contains("must be greater than startIndex"), + "{err:?}" + ); + + // A start past the last part. + let err = expect_err(svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &range(26, 27), + )); + assert!(format!("{err:?}").contains("past the last part"), "{err:?}"); + + // Exactly ten is allowed, and an end past the last part is capped + // there rather than refused. + let ok_page = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &range(16, 26), + ) + .unwrap(), + ); + assert_eq!(issued_parts(&ok_page), (16..=25).collect::>()); + assert!(ok_page["nextIndex"].is_null(), "{ok_page}"); + let capped = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &range(24, 34), + ) + .unwrap(), + ); + assert_eq!(issued_parts(&capped), vec![24, 25]); + } + + #[test] + fn reissued_links_keep_the_signature_and_the_upload_deadline() { + let svc = service(); + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "big.bin", "fileSizeBytes": 6 * 1024 * 1024 }), + ) + .unwrap(), + ); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let first_signature = signature_of(links["uploadUrls"][0]["url"].as_str().unwrap()); + let deadline = svc + .state + .read() + .get("000000000000") .unwrap() - .iter() - .map(|u| u["partIndex"].as_i64().unwrap()) - .collect(); - assert_eq!(parts, vec![2, 3]); + .attachment_uploads[&upload_id] + .expiry + .clone(); + + let again = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "big.bin", "uploadId": upload_id }), + ) + .unwrap(), + ); + // The URL the caller may already be uploading to still works: the + // signature was reused rather than rotated. + assert_eq!( + signature_of(again["uploadUrls"][0]["url"].as_str().unwrap()), + first_signature + ); + // And asking again does not push the upload's own deadline back. + assert_eq!( + svc.state + .read() + .get("000000000000") + .unwrap() + .attachment_uploads[&upload_id] + .expiry, + deadline + ); + + // A part whose link expired does get a fresh signature, but never one + // that outlives the upload. + svc.state + .write() + .get_mut("000000000000") + .unwrap() + .attachment_uploads + .get_mut(&upload_id) + .unwrap() + .part_mut(1) + .unwrap() + .expiry = "2000-01-01T00:00:00.000Z".to_string(); + let rotated = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ "fileName": "big.bin", "uploadId": upload_id }), + ) + .unwrap(), + ); + assert_ne!( + signature_of(rotated["uploadUrls"][0]["url"].as_str().unwrap()), + first_signature + ); + let guard = svc.state.read(); + let upload = &guard.get("000000000000").unwrap().attachment_uploads[&upload_id]; + assert!(upload.part(1).unwrap().expiry <= upload.expiry); } #[test] @@ -1752,7 +2096,10 @@ mod tests { &req("CompleteAttachmentUpload", json!({})), &json!({ "uploadId": upload_id, "completedUploads": [] }), )); - assert!(format!("{err:?}").contains("missing part 1")); + assert!( + format!("{err:?}").contains("must name at least one part"), + "{err:?}" + ); // Part index that was never issued. let err = expect_err(svc.complete_attachment_upload( @@ -1793,8 +2140,106 @@ mod tests { assert_eq!(completed["uploadStatus"], "attachment-ready"); } + /// Upload part `part_index` of `upload_id` over its presigned link, + /// returning the `ETag` the data plane issued. + fn put_part(svc: &SupportService, links: &Value, part_index: i64, bytes: &[u8]) -> String { + let upload_id = links["uploadId"].as_str().unwrap(); + let url = links["uploadUrls"] + .as_array() + .unwrap() + .iter() + .find(|u| u["partIndex"].as_i64() == Some(part_index)) + .expect("a link was issued for this part")["url"] + .as_str() + .unwrap(); + match crate::dataplane::put_upload_part( + &svc.state, + "000000000000", + upload_id, + part_index, + &signature_of(url), + bytes, + ) { + crate::dataplane::PutPartOutcome::Stored(tag) => tag, + other => panic!("expected the part to be stored, got {other:?}"), + } + } + + #[test] + fn completing_reports_parts_one_call_at_a_time() { + let svc = service(); + // Two parts: a full 5 MiB one and a 1 MiB remainder. + let first = vec![b'a'; 5 * 1024 * 1024]; + let second = vec![b'b'; 1024 * 1024]; + let links = body_of( + &svc.get_attachment_upload_links( + &req("GetAttachmentUploadLinks", json!({})), + &json!({ + "fileName": "big.bin", + "fileSizeBytes": first.len() + second.len(), + }), + ) + .unwrap(), + ); + let upload_id = links["uploadId"].as_str().unwrap().to_string(); + let first_etag = put_part(&svc, &links, 1, &first); + + // The model allows completing one part per call: reporting part 1 + // alone succeeds and leaves the upload pending. + let pending = body_of( + &svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 1, "eTag": first_etag }], + }), + ) + .unwrap(), + ); + assert_eq!(pending["uploadStatus"], "attachment-not-ready"); + assert!(svc + .state + .read() + .get("000000000000") + .unwrap() + .attachment_uploads[&upload_id] + .attachment_id + .is_none()); + + // Part 2 is still uploadable, and reporting it finishes the upload. + let second_etag = put_part(&svc, &links, 2, &second); + let completed = body_of( + &svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ + "uploadId": upload_id, + "completedUploads": [{ "partIndex": 2, "eTag": second_etag }], + }), + ) + .unwrap(), + ); + assert_eq!(completed["uploadStatus"], "attachment-ready"); + + let guard = svc.state.read(); + let data = guard.get("000000000000").unwrap(); + let upload = &data.attachment_uploads[&upload_id]; + // The assembled file is both parts, in order. + let attachment_id = upload.attachment_id.clone().unwrap(); + let stored = base64::engine::general_purpose::STANDARD + .decode(data.attachments[&attachment_id]["data"].as_str().unwrap()) + .unwrap(); + assert_eq!(stored.len(), first.len() + second.len()); + assert_eq!(stored[0], b'a'); + assert_eq!(stored[stored.len() - 1], b'b'); + // The part payloads are released once the attachment holds the bytes, + // so the file is not kept twice in memory or in the snapshot. + assert!(upload.parts.iter().all(|p| p.data.is_none())); + // Progress still reads as complete even though the payloads are gone. + assert_eq!(upload.completed_parts(), 2); + } + #[test] - fn completing_before_every_part_is_uploaded_is_rejected() { + fn completing_a_part_that_was_never_uploaded_is_rejected() { let svc = service(); let links = body_of( &svc.get_attachment_upload_links( @@ -1804,26 +2249,116 @@ mod tests { .unwrap(), ); let upload_id = links["uploadId"].as_str().unwrap().to_string(); - let signature = signature_of(links["uploadUrls"][0]["url"].as_str().unwrap()); - let etag = match crate::dataplane::put_upload_part( - &svc.state, - "000000000000", - &upload_id, - 1, - &signature, - b"first", - ) { - crate::dataplane::PutPartOutcome::Stored(tag) => tag, - other => panic!("expected the part to be stored, got {other:?}"), - }; let err = expect_err(svc.complete_attachment_upload( &req("CompleteAttachmentUpload", json!({})), &json!({ "uploadId": upload_id, - "completedUploads": [{ "partIndex": 1, "eTag": etag }], + "completedUploads": [{ "partIndex": 2, "eTag": "\"x\"" }], }), )); - assert!(format!("{err:?}").contains("Part 2")); + assert!(format!("{err:?}").contains("was never uploaded"), "{err:?}"); + // The rejected call recorded nothing. + assert!( + !svc.state + .read() + .get("000000000000") + .unwrap() + .attachment_uploads[&upload_id] + .part(2) + .unwrap() + .completed + ); + } + + #[test] + fn assembling_a_corrupt_upload_keeps_the_parts() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + // Shorten the stored payload behind the data plane's back: the parts no + // longer add up to the declared file size. + svc.state + .write() + .get_mut("000000000000") + .unwrap() + .attachment_uploads + .get_mut(&upload_id) + .unwrap() + .part_mut(1) + .unwrap() + .data = Some(base64::engine::general_purpose::STANDARD.encode(b"hi")); + let err = expect_err(svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": upload_id, "completedUploads": completed_uploads }), + )); + assert!(format!("{err:?}").contains("declared 5 bytes"), "{err:?}"); + // The failed assembly did not drop the uploaded bytes. + let guard = svc.state.read(); + let upload = &guard.get("000000000000").unwrap().attachment_uploads[&upload_id]; + assert_eq!(upload.parts.len(), 1); + assert!(upload.part(1).unwrap().data.is_some()); + assert_eq!(upload.status, UPLOAD_NOT_READY); + } + + #[test] + fn download_links_prune_expired_grants() { + let svc = service(); + let (upload_id, completed_uploads) = upload_one_part(&svc, "log.txt", b"hello"); + svc.complete_attachment_upload( + &req("CompleteAttachmentUpload", json!({})), + &json!({ "uploadId": upload_id, "completedUploads": completed_uploads }), + ) + .unwrap(); + let attachment_id = svc + .state + .read() + .get("000000000000") + .unwrap() + .attachment_uploads[&upload_id] + .attachment_id + .clone() + .unwrap(); + + let link = |svc: &SupportService| { + svc.get_attachment_download_link( + &req("GetAttachmentDownloadLink", json!({})), + &json!({ "attachmentId": attachment_id }), + ) + .unwrap() + }; + link(&svc); + link(&svc); + assert_eq!( + svc.state + .read() + .get("000000000000") + .unwrap() + .attachment_downloads + .len(), + 2 + ); + + // Age both grants out; the next link sweeps them instead of letting + // them pile up for the lifetime of the process. + for grant in svc + .state + .write() + .get_mut("000000000000") + .unwrap() + .attachment_downloads + .values_mut() + { + grant.expiry = "2000-01-01T00:00:00.000Z".to_string(); + } + link(&svc); + assert_eq!( + svc.state + .read() + .get("000000000000") + .unwrap() + .attachment_downloads + .len(), + 1 + ); } #[test] diff --git a/crates/fakecloud-support/src/shared.rs b/crates/fakecloud-support/src/shared.rs index 3076f896f..e7451e36e 100644 --- a/crates/fakecloud-support/src/shared.rs +++ b/crates/fakecloud-support/src/shared.rs @@ -84,6 +84,12 @@ pub const LINK_TTL_SECONDS: i64 = 3600; /// part size and the value the Support console uses. pub const DEFAULT_PART_SIZE_BYTES: i64 = 5 * 1024 * 1024; +/// How many presigned upload URLs one `GetAttachmentUploadLinks` call returns +/// at most. The model caps both the response list ("The list contains at most +/// 10 URLs per call") and the requested range (`endIndex - startIndex` "must +/// not exceed 10") at the same number. +pub const MAX_UPLOAD_URLS_PER_CALL: i64 = 10; + /// The `X-Amz-Credential` access-key id used when the caller presented none /// (unsigned requests are accepted by default). Matches AWS's documented /// example key so the link keeps a realistic shape. diff --git a/crates/fakecloud-support/src/state.rs b/crates/fakecloud-support/src/state.rs index 55143c1c1..ad678f302 100644 --- a/crates/fakecloud-support/src/state.rs +++ b/crates/fakecloud-support/src/state.rs @@ -16,10 +16,11 @@ //! `PUT` each part -> `CompleteAttachmentUpload`) keeps its own bookkeeping in //! [`AttachmentUpload`]: one record per `uploadId` holding the issued part //! links (each with its own signature and expiry), the bytes and `ETag` -//! actually uploaded to each link, and the terminal upload status. Download -//! grants minted by `GetAttachmentDownloadLink` live in `attachment_downloads` -//! keyed by the URL signature, so the data-plane route can authorise a -//! download without re-deriving anything. +//! actually uploaded to each link, which parts `CompleteAttachmentUpload` has +//! been called for (it may be called one part at a time), and the terminal +//! upload status. Download grants minted by `GetAttachmentDownloadLink` live +//! in `attachment_downloads` keyed by the URL signature, so the data-plane +//! route can authorise a download without re-deriving anything. use std::collections::BTreeMap; use std::sync::Arc; @@ -56,9 +57,17 @@ pub struct UploadPart { /// uploaded. #[serde(default)] pub etag: Option, - /// The uploaded bytes, base64-encoded, `None` until the part is uploaded. + /// The uploaded bytes, base64-encoded, `None` until the part is uploaded + /// and again once `CompleteAttachmentUpload` assembled the attachment (the + /// bytes live in `attachments` from then on, so keeping a second copy here + /// would double the upload's cost in memory and in every snapshot). #[serde(default)] pub data: Option, + /// Whether `CompleteAttachmentUpload` has been called for this part. The + /// model allows completing one part per call, so completion is recorded + /// per part and the attachment is assembled only once every part is in. + #[serde(default)] + pub completed: bool, } /// A multipart attachment upload keyed by its `uploadId`. @@ -95,17 +104,46 @@ impl AttachmentUpload { self.parts.iter_mut().find(|p| p.part_index == part_index) } - /// How many parts have been uploaded so far. + /// How many parts have been uploaded so far. A part counts from the moment + /// its bytes arrive; the completion flag keeps it counted after the + /// assembled attachment released the payloads. pub fn completed_parts(&self) -> i64 { - self.parts.iter().filter(|p| p.data.is_some()).count() as i64 + self.parts + .iter() + .filter(|p| p.data.is_some() || p.completed) + .count() as i64 + } + + /// The lowest part index whose bytes have not arrived yet, or `None` when + /// every part is in. Used to pick the range to hand links out for when the + /// caller does not name one. + pub fn next_unuploaded_index(&self) -> Option { + (1..=self.total_parts).find(|i| { + self.part(*i) + .map(|p| p.data.is_none() && !p.completed) + .unwrap_or(true) + }) + } + + /// Whether `CompleteAttachmentUpload` has now been called for every part. + pub fn all_parts_completed(&self) -> bool { + self.total_parts > 0 + && (1..=self.total_parts).all(|i| self.part(i).map(|p| p.completed).unwrap_or(false)) } - /// The lowest part index that has not been uploaded yet, or `0` when every - /// part is in (matching the `nextIndex` sentinel the model defaults to). - pub fn next_index(&self) -> i64 { - (1..=self.total_parts) - .find(|i| self.part(*i).map(|p| p.data.is_none()).unwrap_or(true)) - .unwrap_or(0) + /// The exact size, in bytes, part `part_index` must have: every part but + /// the last is exactly `part_size_bytes` and the last carries the + /// remainder. `None` when the upload declared no file size, in which case + /// there is nothing to check a part against. + pub fn expected_part_len(&self, part_index: i64) -> Option { + if self.file_size_bytes <= 0 || self.part_size_bytes <= 0 { + return None; + } + if part_index < self.total_parts { + Some(self.part_size_bytes) + } else { + Some(self.file_size_bytes - self.part_size_bytes * (self.total_parts - 1)) + } } } @@ -271,6 +309,7 @@ mod tests { expiry: expiry.into(), etag: Some("\"abc\"".into()), data: Some("aGk=".into()), + completed: false, }, UploadPart { part_index: 2, @@ -278,6 +317,7 @@ mod tests { expiry: expiry.into(), etag: None, data: None, + completed: false, }, ], attachment_id: None, @@ -319,9 +359,10 @@ mod tests { let u = upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY); assert_eq!(u.completed_parts(), 1); // Part 1 is in, so part 2 is the next one the client should upload. - assert_eq!(u.next_index(), 2); + assert_eq!(u.next_unuploaded_index(), Some(2)); assert_eq!(u.part(1).unwrap().signature, "sig1"); assert!(u.part(3).is_none()); + assert!(!u.all_parts_completed()); } #[test] @@ -331,6 +372,37 @@ mod tests { part.data = Some("eA==".into()); part.etag = Some("\"def\"".into()); assert_eq!(u.completed_parts(), 2); - assert_eq!(u.next_index(), 0); + assert_eq!(u.next_unuploaded_index(), None); + } + + #[test] + fn completion_is_tracked_per_part() { + let mut u = upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY); + u.part_mut(1).unwrap().completed = true; + // Part 2 has not been reported yet, so the upload is not assembled. + assert!(!u.all_parts_completed()); + let part = u.part_mut(2).unwrap(); + part.data = Some("eA==".into()); + part.completed = true; + assert!(u.all_parts_completed()); + // Dropping the payloads after assembly does not lose the progress. + for part in &mut u.parts { + part.data = None; + } + assert_eq!(u.completed_parts(), 2); + } + + #[test] + fn expected_part_len_splits_the_declared_size() { + let mut u = upload("2999-01-01T00:00:00.000Z", UPLOAD_NOT_READY); + u.part_size_bytes = 5; + u.file_size_bytes = 8; + // Every part but the last is exactly one part size; the last carries + // the remainder. + assert_eq!(u.expected_part_len(1), Some(5)); + assert_eq!(u.expected_part_len(2), Some(3)); + // An upload that declared no size has nothing to check against. + u.file_size_bytes = 0; + assert_eq!(u.expected_part_len(1), None); } } diff --git a/website/content/docs/services/support.md b/website/content/docs/services/support.md index 8ce0b8b95..ddfb60bf5 100644 --- a/website/content/docs/services/support.md +++ b/website/content/docs/services/support.md @@ -61,16 +61,25 @@ the links pointing back at fakecloud, which serves them. - **`GetAttachmentUploadLinks`** records a new upload (or resumes one named by `uploadId`, optionally narrowed to an `uploadRange`) and returns an `uploadId`, the `partSizeBytes` (5 MiB), the `totalParts` derived from - `fileSizeBytes`, the `nextIndex` still outstanding, and one presigned `PUT` - `url` per part with its own `expiryDate`. Each link carries its own - signature, recorded in state; a link that was never issued, was tampered - with, or has expired is refused. + `fileSizeBytes`, the `nextIndex` to ask for next, and one presigned `PUT` + `url` per part with its own `expiryDate`. At most ten links come back per + call, `uploadRange.endIndex` is exclusive (`{1, 4}` is parts 1, 2 and 3) and + a wider range is rejected, and `nextIndex` is `null` once the last part has + been handed out. Re-asking for a part that already has a live link returns + that same link rather than rotating its signature, and re-asking never + extends the upload's own deadline. Each link carries its own signature, + recorded in state; a link that was never issued, was tampered with, or has + expired is refused. - The links are real. `PUT` the part's bytes to the URL and fakecloud stores - them and returns the part's `ETag`, exactly as an S3 part upload does. + them and returns the part's `ETag`, exactly as an S3 part upload does. Every + part but the last must be exactly `partSizeBytes`, and the last part the + remainder of `fileSizeBytes`; a wrongly sized part is rejected. - **`CompleteAttachmentUpload`** takes the `uploadId` and the - `completedUploads` list of `{partIndex, eTag}`. Every part must have been - uploaded and every `ETag` must match what the `PUT` returned; the parts are - then concatenated into a real attachment, retrievable with + `completedUploads` list of `{partIndex, eTag}`. It can be called one part at + a time or with several parts at once: each named part must have been uploaded + with an `ETag` matching what the `PUT` returned, and the upload stays + `attachment-not-ready` until every part has been completed, at which point + the parts are concatenated into a real attachment retrievable with `DescribeAttachment`. Completing twice, or completing an upload whose links expired, returns `UploadIdNotFound`. - **`DescribeAttachmentUploadStatus`** reports the recorded `uploadStatus`