Repository: cloudtools/troposphere
Pull request #1941 (2021-08-02 07:18:48)

Add serveless HttpApi class
Add support for AWS::Serverless::HttpApi

[sam-resource-httpapi](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-httpapi.html)


Patch:
diff --git a/README.rst b/README.rst
index 844ee448..4c9a13c1 100644
--- a/README.rst
+++ b/README.rst
@@ -279,6 +279,8 @@ Currently supported AWS resource types
 - `AWS::RoboMaker`_
 - `AWS::Route53`_
 - `AWS::Route53Resolver`_
+- `AWS::Route53RecoveryControl`_
+- `AWS::Route53RecoveryReadiness`_
 - `AWS::S3`_
 - `AWS::S3ObjectLambda`_
 - `AWS::S3Outposts`_
@@ -297,6 +299,7 @@ Currently supported AWS resource types
 - `AWS::ServiceCatalog`_
 - `AWS::ServiceCatalogAppRegistry`_
 - `AWS::ServiceDiscovery`_
+- `AWS::Signer`_
 - `AWS::StepFunctions`_
 - `AWS::Synthetics`_
 - `AWS::Timestream`_
@@ -558,6 +561,8 @@ See `LICENSE`_ for the troposphere full license text.
 .. _`AWS::RoboMaker`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_RoboMaker.html
 .. _`AWS::Route53`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Route53.html
 .. _`AWS::Route53Resolver`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Route53Resolver.html
+.. _`AWS::Route53RecoveryControl`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Route53RecoveryControl.html
+.. _`AWS::Route53RecoveryReadiness`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Route53RecoveryReadiness.html
 .. _`AWS::S3`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3.html
 .. _`AWS::S3ObjectLambda`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3ObjectLambda.html
 .. _`AWS::S3Outposts`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3Outposts.html
@@ -576,6 +581,7 @@ See `LICENSE`_ for the troposphere full license text.
 .. _`AWS::ServiceCatalog`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_ServiceCatalog.html
 .. _`AWS::ServiceCatalogAppRegistry`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_ServiceCatalogAppRegistry.html
 .. _`AWS::ServiceDiscovery`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_ServiceDiscovery.html
+.. _`AWS::Signer`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Signer.html
 .. _`AWS::StepFunctions`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_StepFunctions.html
 .. _`AWS::Synthetics`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Synthetics.html
 .. _`AWS::Timestream`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Timestream.html
diff --git a/examples/RedshiftClusterInVpc.py b/examples/RedshiftClusterInVpc.py
index 0ce2a8df..7b49e7bd 100644
--- a/examples/RedshiftClusterInVpc.py
+++ b/examples/RedshiftClusterInVpc.py
@@ -106,7 +106,7 @@ redshiftcluster = t.add_resource(
         MasterUsername=Ref("MasterUsername"),
         MasterUserPassword=Ref("MasterUserPassword"),
         ClusterParameterGroupName=Ref("RedshiftClusterParameterGroup"),
-        VpcSecurityGroupIds=Ref("SecurityGroup"),
+        VpcSecurityGroupIds=[Ref("SecurityGroup")],
         ClusterSubnetGroupName=Ref("RedshiftClusterSubnetGroup"),
     )
 )
@@ -130,7 +130,7 @@ redshiftclustersubnetgroup = t.add_resource(
     ClusterSubnetGroup(
         "RedshiftClusterSubnetGroup",
         Description="Cluster subnet group",
-        SubnetIds=Ref("Subnet"),
+        SubnetIds=[Ref("Subnet")],
     )
 )
 
diff --git a/troposphere/apigateway.py b/troposphere/apigateway.py
index 88b7b09f..a569e746 100644
--- a/troposphere/apigateway.py
+++ b/troposphere/apigateway.py
@@ -217,6 +217,7 @@ class DomainName(AWSObject):
         "DomainName": (str, True),
         "EndpointConfiguration": (EndpointConfiguration, False),
         "MutualTlsAuthentication": (MutualTlsAuthentication, False),
+        "OwnershipVerificationCertificateArn": (str, False),
         "RegionalCertificateArn": (str, False),
         "SecurityPolicy": (str, False),
         "Tags": (Tags, False),
diff --git a/troposphere/apigatewayv2.py b/troposphere/apigatewayv2.py
index 7d755de9..e85066e7 100644
--- a/troposphere/apigatewayv2.py
+++ b/troposphere/apigatewayv2.py
@@ -170,6 +170,8 @@ class DomainNameConfiguration(AWSProperty):
         "CertificateArn": (str, False),
         "CertificateName": (str, False),
         "EndpointType": (str, False),
+        "OwnershipVerificationCertificateArn": (str, False),
+        "SecurityPolicy": (str, False),
     }
 
 
diff --git a/troposphere/appsync.py b/troposphere/appsync.py
index 319bb7cd..6e65b90a 100644
--- a/troposphere/appsync.py
+++ b/troposphere/appsync.py
@@ -168,6 +168,14 @@ class AdditionalAuthenticationProvider(AWSProperty):
     }
 
 
+class LambdaAuthorizerConfig(AWSProperty):
+    props = {
+        "AuthorizerResultTtlInSeconds": (double, False),
+        "AuthorizerUri": (str, False),
+        "IdentityValidationExpression": (str, False),
+    }
+
+
 class LogConfig(AWSProperty):
     props = {
         "CloudWatchLogsRoleArn": (str, False),
@@ -194,6 +202,7 @@ class GraphQLApi(AWSObject):
             False,
         ),
         "AuthenticationType": (str, True),
+        "LambdaAuthorizerConfig": (LambdaAuthorizerConfig, False),
         "LogConfig": (LogConfig, False),
         "Name": (str, True),
         "OpenIDConnectConfig": (OpenIDConnectConfig, False),
diff --git a/troposphere/athena.py b/troposphere/athena.py
index e28398d2..f2cd58e9 100644
--- a/troposphere/athena.py
+++ b/troposphere/athena.py
@@ -59,6 +59,17 @@ class NamedQuery(AWSObject):
     }
 
 
+class PreparedStatement(AWSObject):
+    resource_type = "AWS::Athena::PreparedStatement"
+
+    props = {
+        "Description": (str, False),
+        "QueryStatement": (str, True),
+        "StatementName": (str, True),
+        "WorkGroup": (str, True),
+    }
+
+
 class EngineVersion(AWSProperty):
     props = {
         "EffectiveEngineVersion": (str, False),
diff --git a/troposphere/autoscaling.py b/troposphere/autoscaling.py
index a2ad4efc..433e6a92 100644
--- a/troposphere/autoscaling.py
+++ b/troposphere/autoscaling.py
@@ -4,7 +4,7 @@
 # See LICENSE file for full license.
 
 from . import AWSHelperFn, AWSObject, AWSProperty, FindInMap, If, Ref, cloudformation
-from .validators import boolean, exactly_one, integer, mutually_exclusive
+from .validators import boolean, double, exactly_one, integer, mutually_exclusive
 
 EC2_INSTANCE_LAUNCH = "autoscaling:EC2_INSTANCE_LAUNCH"
 EC2_INSTANCE_LAUNCH_ERROR = "autoscaling:EC2_INSTANCE_LAUNCH_ERROR"
@@ -148,6 +148,7 @@ class InstancesDistribution(AWSProperty):
 class LaunchTemplateOverrides(AWSProperty):
     props = {
         "InstanceType": (str, False),
+        "LaunchTemplateSpecification": (LaunchTemplateSpecification, False),
         "WeightedCapacity": (str, False),
     }
 
@@ -274,6 +275,55 @@ class LaunchConfiguration(AWSObject):
     }
 
 
+class PredictiveScalingPredefinedLoadMetric(AWSProperty):
+    props = {
+        "PredefinedMetricType": (str, True),
+        "ResourceLabel": (str, False),
+    }
+
+
+class PredictiveScalingPredefinedMetricPair(AWSProperty):
+    props = {
+        "PredefinedMetricType": (str, True),
+        "ResourceLabel": (str, False),
+    }
+
+
+class PredictiveScalingPredefinedScalingMetric(AWSProperty):
+    props = {
+        "PredefinedMetricType": (str, True),
+        "ResourceLabel": (str, False),
+    }
+
+
+class PredictiveScalingMetricSpecification(AWSProperty):
+    props = {
+        "PredefinedLoadMetricSpecification": (
+            PredictiveScalingPredefinedLoadMetric,
+            False,
+        ),
+        "PredefinedMetricPairSpecification": (
+            PredictiveScalingPredefinedMetricPair,
+            False,
+        ),
+        "PredefinedScalingMetricSpecification": (
+            PredictiveScalingPredefinedScalingMetric,
+            False,
+        ),
+        "TargetValue": (double, True),
+    }
+
+
+class PredictiveScalingConfiguration(AWSProperty):
+    props = {
+        "MaxCapacityBreachBehavior": (str, False),
+        "MaxCapacityBuffer": (integer, False),
+        "MetricSpecifications": ([PredictiveScalingMetricSpecification], True),
+        "Mode": (str, False),
+        "SchedulingBufferTime": (integer, False),
+    }
+
+
 class StepAdjustments(AWSProperty):
     props = {
         "MetricIntervalLowerBound": (integer, False),
@@ -326,6 +376,7 @@ class ScalingPolicy(AWSObject):
         "MetricAggregationType": (str, False),
         "MinAdjustmentMagnitude": (integer, False),
         "PolicyType": (str, False),
+        "PredictiveScalingConfiguration": (PredictiveScalingConfiguration, False),
         "ScalingAdjustment": (integer, False),
         "StepAdjustments": ([StepAdjustments], False),
         "TargetTrackingConfiguration": (TargetTrackingConfiguration, False),
@@ -343,6 +394,7 @@ class ScheduledAction(AWSObject):
         "MinSize": (integer, False),
         "Recurrence": (str, False),
         "StartTime": (str, False),
+        "TimeZone": (str, False),
     }
 
 
diff --git a/troposphere/codebuild.py b/troposphere/codebuild.py
index fe366f10..700d59e5 100644
--- a/troposphere/codebuild.py
+++ b/troposphere/codebuild.py
@@ -366,6 +366,7 @@ class Project(AWSObject):
         "BadgeEnabled": (boolean, False),
         "BuildBatchConfig": (ProjectBuildBatchConfig, False),
         "Cache": (ProjectCache, False),
+        "ConcurrentBuildLimit": (integer, False),
         "Description": (str, False),
         "EncryptionKey": (str, False),
         "Environment": (Environment, True),
@@ -373,6 +374,7 @@ class Project(AWSObject):
         "LogsConfig": (LogsConfig, False),
         "Name": (str, False),
         "QueuedTimeoutInMinutes": (integer, False),
+        "ResourceAccessRole": (str, False),
         "SecondaryArtifacts": ([Artifacts], False),
         "SecondarySourceVersions": ([ProjectSourceVersion], False),
         "SecondarySources": ([Source], False),
@@ -382,6 +384,7 @@ class Project(AWSObject):
         "Tags": (Tags, False),
         "TimeoutInMinutes": (integer, False),
         "Triggers": (ProjectTriggers, False),
+        "Visibility": (str, False),
         "VpcConfig": (VpcConfig, False),
     }
 
diff --git a/troposphere/databrew.py b/troposphere/databrew.py
index e00e4382..0d0091ae 100644
--- a/troposphere/databrew.py
+++ b/troposphere/databrew.py
@@ -4,7 +4,7 @@
 # See LICENSE file for full license.
 #
 # *** Do not modify - this file is autogenerated ***
-# Resource specification version: 39.7.0
+# Resource specification version: 39.8.0
 
 
 from troposphere import Tags
@@ -166,6 +166,14 @@ class DataCatalogOutput(AWSProperty):
     }
 
 
+class DatabaseOutput(AWSProperty):
+    props = {
+        "DatabaseOptions": (DatabaseTableOutputOptions, True),
+        "DatabaseOutputMode": (str, False),
+        "GlueConnectionName": (str, True),
+    }
+
+
 class JobSample(AWSProperty):
     props = {
         "Mode": (str, False),
@@ -203,11 +211,52 @@ class OutputLocation(AWSProperty):
     }
 
 
+class ColumnSelector(AWSProperty):
+    props = {
+        "Name": (str, False),
+        "Regex": (str, False),
+    }
+
+
+class ParameterMap(AWSProperty):
+    props = {}
+
+
+class StatisticOverride(AWSProperty):
+    props = {
+        "Parameters": (ParameterMap, True),
+        "Statistic": (str, True),
+    }
+
+
+class StatisticsConfiguration(AWSProperty):
+    props = {
+        "IncludedStatistics": ([str], False),
+        "Overrides": ([StatisticOverride], False),
+    }
+
+
+class ColumnStatisticsConfiguration(AWSProperty):
+    props = {
+        "Selectors": ([ColumnSelector], False),
+        "Statistics": (StatisticsConfiguration, True),
+    }
+
+
+class ProfileConfiguration(AWSProperty):
+    props = {
+        "ColumnStatisticsConfigurations": ([ColumnStatisticsConfiguration], False),
+        "DatasetStatisticsConfiguration": (StatisticsConfiguration, False),
+        "ProfileColumns": ([ColumnSelector], False),
+    }
+
+
 class Job(AWSObject):
     resource_type = "AWS::DataBrew::Job"
 
     props = {
         "DataCatalogOutputs": ([DataCatalogOutput], False),
+        "DatabaseOutputs": ([DatabaseOutput], False),
         "DatasetName": (str, False),
         "EncryptionKeyArn": (str, False),
         "EncryptionMode": (str, False),
@@ -218,6 +267,7 @@ class Job(AWSObject):
         "Name": (str, True),
         "OutputLocation": (OutputLocation, False),
         "Outputs": ([Output], False),
+        "ProfileConfiguration": (ProfileConfiguration, False),
         "ProjectName": (str, False),
         "Recipe": (dict, False),
         "RoleArn": (str, True),
diff --git a/troposphere/elasticsearch.py b/troposphere/elasticsearch.py
index de6effa0..51adaa3d 100644
--- a/troposphere/elasticsearch.py
+++ b/troposphere/elasticsearch.py
@@ -68,6 +68,12 @@ class EBSOptions(AWSProperty):
             raise ValueError("Must specify Iops if VolumeType is 'io1'.")
 
 
+class ColdStorageOptions(AWSProperty):
+    props = {
+        "Enabled": (boolean, False),
+    }
+
+
 class ZoneAwarenessConfig(AWSProperty):
     props = {
         "AvailabilityZoneCount": (integer, False),
@@ -76,6 +82,7 @@ class ZoneAwarenessConfig(AWSProperty):
 
 class ElasticsearchClusterConfig(AWSProperty):
     props = {
+        "ColdStorageOptions": (ColdStorageOptions, False),
         "DedicatedMasterCount": (integer, False),
         "DedicatedMasterEnabled": (boolean, False),
         "DedicatedMasterType": (str, False),
diff --git a/troposphere/fsx.py b/troposphere/fsx.py
index cb8ae3c6..42029f84 100644
--- a/troposphere/fsx.py
+++ b/troposphere/fsx.py
@@ -63,6 +63,14 @@ class LustreConfiguration(AWSProperty):
     }
 
 
+class AuditLogConfiguration(AWSProperty):
+    props = {
+        "AuditLogDestination": (str, False),
+        "FileAccessAuditLogLevel": (str, True),
+        "FileShareAccessAuditLogLevel": (str, True),
+    }
+
+
 class SelfManagedActiveDirectoryConfiguration(AWSProperty):
     props = {
         "DnsIps": ([str], False),
@@ -78,6 +86,7 @@ class WindowsConfiguration(AWSProperty):
     props = {
         "ActiveDirectoryId": (str, False),
         "Aliases": ([str], False),
+        "AuditLogConfiguration": (AuditLogConfiguration, False),
         "AutomaticBackupRetentionDays": (integer, False),
         "CopyTagsToBackups": (boolean, False),
         "DailyAutomaticBackupStartTime": (str, False),
diff --git a/troposphere/logs.py b/troposphere/logs.py
index 214368a7..306b9707 100644
--- a/troposphere/logs.py
+++ b/troposphere/logs.py
@@ -1,6 +1,36 @@
 from . import AWSObject, AWSProperty
+from .compat import policytypes
 from .constants import LOGS_ALLOWED_RETENTION_DAYS as RETENTION_DAYS
-from .validators import integer_list_item
+from .validators import integer_list_item, json_checker
+import json
+
+policytypes = policytypes + (str,)
+
+
+def validate_resource_policy(policy_document):
+    """validate policy_document. Between 1 to 5120"""
+
+    if not isinstance(policy_document, policytypes):
+        raise ValueError("PolicyDocument must be a valid policy document")
+
+    if isinstance(policy_document, str) and not json_checker(policy_document):
+        raise ValueError("PolicyDocument must be a valid JSON formated string")
+
+    if isinstance(policy_document, dict):
+        policy_document_text = json.dumps(policy_document)
+    elif isinstance(policy_document, str):
+        policy_document_text = policy_document
+    else:
+        policy_document_text = policy_document.to_json()
+
+    # NB: {} empty dict is 2 length
+    if len(policy_document_text) < 3:
+        raise ValueError("PolicyDocument must not be empty")
+
+    if len(policy_document_text) > 5120:
+        raise ValueError("PolicyDocument maximum length must not exceed 5120")
+
+    return policy_document
 
 
 class Destination(AWSObject):
@@ -77,3 +107,12 @@ class SubscriptionFilter(AWSObject):
         "LogGroupName": (str, True),
         "RoleArn": (str, False),
     }
+
+
+class LogResourcePolicy(AWSObject):
+    resource_type = "AWS::Logs::ResourcePolicy"
+
+    props = {
+        "PolicyDocument": (validate_resource_policy, True),
+        "PolicyName": (str, True),
+    }
diff --git a/troposphere/redshift.py b/troposphere/redshift.py
index 77e8ff9d..ecf8e06b 100644
--- a/troposphere/redshift.py
+++ b/troposphere/redshift.py
@@ -1,12 +1,25 @@
-# Copyright (c) 2014, Guillem Anguera <ganguera@gmail.com>
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
 # All rights reserved.
 #
 # See LICENSE file for full license.
+#
+# *** Do not modify - this file is autogenerated ***
+# Resource specification version: 40.0.0
+
+
+from troposphere import Tags
 
-from . import AWSObject, AWSProperty, Tags
+from . import AWSObject, AWSProperty
 from .validators import boolean, integer
 
 
+class Endpoint(AWSProperty):
+    props = {
+        "Address": (str, False),
+        "Port": (str, False),
+    }
+
+
 class LoggingProperties(AWSProperty):
     props = {
         "BucketName": (str, True),
@@ -19,34 +32,54 @@ class Cluster(AWSObject):
 
     props = {
         "AllowVersionUpgrade": (boolean, False),
+        "AquaConfigurationStatus": (str, False),
         "AutomatedSnapshotRetentionPeriod": (integer, False),
         "AvailabilityZone": (str, False),
+        "AvailabilityZoneRelocation": (boolean, False),
+        "AvailabilityZoneRelocationStatus": (str, False),
+        "Classic": (boolean, False),
         "ClusterIdentifier": (str, False),
         "ClusterParameterGroupName": (str, False),
-        "ClusterSecurityGroups": (list, False),
+        "ClusterSecurityGroups": ([str], False),
         "ClusterSubnetGroupName": (str, False),
         "ClusterType": (str, True),
         "ClusterVersion": (str, False),
         "DBName": (str, True),
+        "DeferMaintenance": (boolean, False),
+        "DeferMaintenanceDuration": (integer, False),
+        "DeferMaintenanceEndTime": (str, False),
+        "DeferMaintenanceIdentifier": (str, False),
+        "DeferMaintenanceStartTime": (str, False),
+        "DestinationRegion": (str, False),
         "ElasticIp": (str, False),
         "Encrypted": (boolean, False),
+        "Endpoint": (Endpoint, False),
+        "EnhancedVpcRouting": (boolean, False),
         "HsmClientCertificateIdentifier": (str, False),
         "HsmConfigurationIdentifier": (str, False),
         "IamRoles": ([str], False),
         "KmsKeyId": (str, False),
         "LoggingProperties": (LoggingProperties, False),
-        "MasterUsername": (str, True),
+        "MaintenanceTrackName": (str, False),
+        "ManualSnapshotRetentionPeriod": (integer, False),
         "MasterUserPassword": (str, True),
+        "MasterUsername": (str, True),
         "NodeType": (str, True),
-        "NumberOfNodes": (integer, False),  # Conditional
+        "NumberOfNodes": (integer, False),
         "OwnerAccount": (str, False),
         "Port": (integer, False),
         "PreferredMaintenanceWindow": (str, False),
         "PubliclyAccessible": (boolean, False),
+        "ResourceAction": (str, False),
+        "RevisionTarget": (str, False),
+        "RotateEncryptionKey": (boolean, False),
         "SnapshotClusterIdentifier": (str, False),
+        "SnapshotCopyGrantName": (str, False),
+        "SnapshotCopyManual": (boolean, False),
+        "SnapshotCopyRetentionPeriod": (integer, False),
         "SnapshotIdentifier": (str, False),
         "Tags": (Tags, False),
-        "VpcSecurityGroupIds": (list, False),
+        "VpcSecurityGroupIds": ([str], False),
     }
 
 
@@ -81,8 +114,8 @@ class ClusterSecurityGroupIngress(AWSObject):
     resource_type = "AWS::Redshift::ClusterSecurityGroupIngress"
 
     props = {
-        "ClusterSecurityGroupName": (str, True),
         "CIDRIP": (str, False),
+        "ClusterSecurityGroupName": (str, True),
         "EC2SecurityGroupName": (str, False),
         "EC2SecurityGroupOwnerId": (str, False),
     }
@@ -93,6 +126,6 @@ class ClusterSubnetGroup(AWSObject):
 
     props = {
         "Description": (str, True),
-        "SubnetIds": (list, True),
+        "SubnetIds": ([str], True),
         "Tags": (Tags, False),
     }
diff --git a/troposphere/route53recoverycontrol.py b/troposphere/route53recoverycontrol.py
new file mode 100644
index 00000000..54ea8b58
--- /dev/null
+++ b/troposphere/route53recoverycontrol.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
+# All rights reserved.
+#
+# See LICENSE file for full license.
+#
+# *** Do not modify - this file is autogenerated ***
+# Resource specification version: 39.8.0
+
+
+from . import AWSObject, AWSProperty
+from .validators import boolean, integer
+
+
+class Cluster(AWSObject):
+    resource_type = "AWS::Route53RecoveryControl::Cluster"
+
+    props = {
+        "Name": (str, False),
+    }
+
+
+class ControlPanel(AWSObject):
+    resource_type = "AWS::Route53RecoveryControl::ControlPanel"
+
+    props = {
+        "ClusterArn": (str, False),
+        "Name": (str, True),
+    }
+
+
+class RoutingControl(AWSObject):
+    resource_type = "AWS::Route53RecoveryControl::RoutingControl"
+
+    props = {
+        "ClusterArn": (str, False),
+        "ControlPanelArn": (str, False),
+        "Name": (str, True),
+    }
+
+
+class AssertionRule(AWSProperty):
+    props = {
+        "AssertedControls": ([str], True),
+        "WaitPeriodMs": (integer, True),
+    }
+
+
+class GatingRule(AWSProperty):
+    props = {
+        "GatingControls": ([str], True),
+        "TargetControls": ([str], True),
+        "WaitPeriodMs": (integer, True),
+    }
+
+
+class RuleConfig(AWSProperty):
+    props = {
+        "Inverted": (boolean, True),
+        "Threshold": (integer, True),
+        "Type": (str, True),
+    }
+
+
+class SafetyRule(AWSObject):
+    resource_type = "AWS::Route53RecoveryControl::SafetyRule"
+
+    props = {
+        "AssertionRule": (AssertionRule, False),
+        "ControlPanelArn": (str, True),
+        "GatingRule": (GatingRule, False),
+        "Name": (str, True),
+        "RuleConfig": (RuleConfig, True),
+    }
diff --git a/troposphere/route53recoveryreadiness.py b/troposphere/route53recoveryreadiness.py
new file mode 100644
index 00000000..7f75407e
--- /dev/null
+++ b/troposphere/route53recoveryreadiness.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
+# All rights reserved.
+#
+# See LICENSE file for full license.
+#
+# *** Do not modify - this file is autogenerated ***
+# Resource specification version: 39.8.0
+
+
+from troposphere import Tags
+
+from . import AWSObject, AWSProperty
+
+
+class Cell(AWSObject):
+    resource_type = "AWS::Route53RecoveryReadiness::Cell"
+
+    props = {
+        "CellName": (str, True),
+        "Cells": ([str], False),
+        "Tags": (Tags, False),
+    }
+
+
+class ReadinessCheck(AWSObject):
+    resource_type = "AWS::Route53RecoveryReadiness::ReadinessCheck"
+
+    props = {
+        "ReadinessCheckName": (str, True),
+        "ResourceSetName": (str, False),
+        "Tags": (Tags, False),
+    }
+
+
+class RecoveryGroup(AWSObject):
+    resource_type = "AWS::Route53RecoveryReadiness::RecoveryGroup"
+
+    props = {
+        "Cells": ([str], False),
+        "RecoveryGroupName": (str, True),
+        "Tags": (Tags, False),
+    }
+
+
+class NLBResource(AWSProperty):
+    props = {
+        "Arn": (str, False),
+    }
+
+
+class R53ResourceRecord(AWSProperty):
+    props = {
+        "DomainName": (str, False),
+        "RecordSetId": (str, False),
+    }
+
+
+class TargetResource(AWSProperty):
+    props = {
+        "NLBResource": (NLBResource, False),
+        "R53Resource": (R53ResourceRecord, False),
+    }
+
+
+class DNSTargetResource(AWSProperty):
+    props = {
+        "DomainName": (str, False),
+        "HostedZoneArn": (str, False),
+        "RecordSetId": (str, False),
+        "RecordType": (str, False),
+        "TargetResource": (TargetResource, False),
+    }
+
+
+class Resource(AWSProperty):
+    props = {
+        "ComponentId": (str, False),
+        "DnsTargetResource": (DNSTargetResource, False),
+        "ReadinessScopes": ([str], False),
+        "ResourceArn": (str, False),
+    }
+
+
+class ResourceSet(AWSObject):
+    resource_type = "AWS::Route53RecoveryReadiness::ResourceSet"
+
+    props = {
+        "ResourceSetName": (str, True),
+        "ResourceSetType": (str, True),
+        "Resources": ([Resource], True),
+        "Tags": (Tags, False),
+    }
diff --git a/troposphere/s3outposts.py b/troposphere/s3outposts.py
index f4003e34..2e27a559 100644
--- a/troposphere/s3outposts.py
+++ b/troposphere/s3outposts.py
@@ -4,7 +4,7 @@
 # See LICENSE file for full license.
 #
 # *** Do not modify - this file is autogenerated ***
-# Resource specification version: 31.0.0
+# Resource specification version: 39.8.0
 
 
 from troposphere import Tags
@@ -77,6 +77,8 @@ class Endpoint(AWSObject):
     resource_type = "AWS::S3Outposts::Endpoint"
 
     props = {
+        "AccessType": (str, False),
+        "CustomerOwnedIpv4Pool": (str, False),
         "OutpostId": (str, True),
         "SecurityGroupId": (str, True),
         "SubnetId": (str, True),
diff --git a/troposphere/sagemaker.py b/troposphere/sagemaker.py
index d63eac93..bddfb3a2 100644
--- a/troposphere/sagemaker.py
+++ b/troposphere/sagemaker.py
@@ -341,6 +341,34 @@ class Endpoint(AWSObject):
     }
 
 
+class AsyncInferenceClientConfig(AWSProperty):
+    props = {
+        "MaxConcurrentInvocationsPerInstance": (integer, False),
+    }
+
+
+class AsyncInferenceNotificationConfig(AWSProperty):
+    props = {
+        "ErrorTopic": (str, False),
+        "SuccessTopic": (str, False),
+    }
+
+
+class AsyncInferenceOutputConfig(AWSProperty):
+    props = {
+        "KmsKeyId": (str, False),
+        "NotificationConfig": (AsyncInferenceNotificationConfig, False),
+        "S3OutputPath": (str, True),
+    }
+
+
+class AsyncInferenceConfig(AWSProperty):
+    props = {
+        "ClientConfig": (AsyncInferenceClientConfig, False),
+        "OutputConfig": (AsyncInferenceOutputConfig, True),
+    }
+
+
 class CaptureContentTypeHeader(AWSProperty):
     props = {
         "CsvContentTypes": ([str], False),
@@ -379,6 +407,7 @@ class EndpointConfig(AWSObject):
     resource_type = "AWS::SageMaker::EndpointConfig"
 
     props = {
+        "AsyncInferenceConfig": (AsyncInferenceConfig, False),
         "DataCaptureConfig": (DataCaptureConfig, False),
         "EndpointConfigName": (str, False),
         "KmsKeyId": (str, False),
diff --git a/troposphere/serverless.py b/troposphere/serverless.py
index 497f944c..78047092 100644
--- a/troposphere/serverless.py
+++ b/troposphere/serverless.py
@@ -7,6 +7,7 @@ import types
 
 from . import AWSHelperFn, AWSObject, AWSProperty
 from .apigateway import AccessLogSetting, CanarySetting, MethodSetting
+from .apigatewayv2 import AccessLogSettings, RouteSettings
 from .awslambda import (
     DestinationConfig,
     Environment,
@@ -25,6 +26,7 @@ from .validators import (
     integer_range,
     mutually_exclusive,
     positive_integer,
+    boolean,
 )
 
 try:
@@ -335,6 +337,10 @@ class EndpointConfiguration(AWSProperty):
             )
 
 
+class ApiDefinition(AWSProperty):
+    props = {"Bucket": (str, True), "Key": (str, True), "Version": (str, False)}
+
+
 class Api(AWSObject):
     resource_type = "AWS::Serverless::Api"
 
@@ -347,7 +353,7 @@ class Api(AWSObject):
         "CanarySetting": (CanarySetting, False),
         "Cors": ((str, Cors), False),
         "DefinitionBody": (dict, False),
-        "DefinitionUri": (str, False),
+        "DefinitionUri": ((str, ApiDefinition), False),
         "Domain": (Domain, False),
         "EndpointConfiguration": (EndpointConfiguration, False),
         "MethodSettings": ([MethodSetting], False),
@@ -367,6 +373,88 @@ class Api(AWSObject):
         mutually_exclusive(self.__class__.__name__, self.properties, conds)
 
 
+class OAuth2Authorizer(AWSProperty):
+    props = {
+        "AuthorizationScopes": (list, False),
+        "IdentitySource": (str, False),
+        "JwtConfiguration": (dict, False),
+    }
+
+
+class LambdaAuthorizationIdentity(AWSProperty):
+    props = {
+        "Context": (list, False),
+        "Headers": (list, False),
+        "QueryStrings": (list, False),
+        "ReauthorizeEvery": (integer, False),
+        "StageVariables": (list, False),
+    }
+
+
+class LambdaAuthorizer(AWSProperty):
+    props = {
+        "AuthorizerPayloadFormatVersion": (str, True),
+        "EnableSimpleResponses": (boolean, False),
+        "FunctionArn": (str, True),
+        "FunctionInvokeRole": (str, False),
+        "Identity": (LambdaAuthorizationIdentity, False),
+    }
+
+
+class HttpApiAuth(AWSProperty):
+    props = {
+        "Authorizers": ((OAuth2Authorizer, LambdaAuthorizer), False),
+        "DefaultAuthorizer": (str, False),
+    }
+
+
+class HttpApiCorsConfiguration(AWSProperty):
+    props = {
+        "AllowCredentials": (boolean, False),
+        "AllowHeaders": (list, False),
+        "AllowMethods": (list, False),
+        "AllowOrigins": (list, False),
+        "ExposeHeaders": (list, False),
+        "MaxAge": (integer, False),
+    }
+
+
+class HttpApiDefinition(ApiDefinition):
+    pass
+
+
+class HttpApiDomainConfiguration(Domain):
+    pass
+
+
+class HttpApi(AWSObject):
+    resource_type = "AWS::Serverless::HttpApi"
+
+    props = {
+        "AccessLogSettings": (AccessLogSettings, False),
+        "Auth": (HttpApiAuth, False),
+        "CorsConfiguration": ((str, HttpApiCorsConfiguration), False),
+        "DefaultRouteSettings": (RouteSettings, False),
+        "DefinitionBody": (dict, False),
+        "DefinitionUri": ((str, HttpApiDefinition), False),
+        "Description": (str, False),
+        "DisableExecuteApiEndpoint": (boolean, False),
+        "Domain": (HttpApiDomainConfiguration, False),
+        "FailOnWarnings": (boolean, False),
+        "RouteSettings": (dict, False),
+        "StageName": (str, False),
+        "StageVariables": (dict, False),
+        "Tags": (dict, False),
+    }
+
+    def validate(self):
+        conds = [
+            "DefinitionBody",
+            "DefinitionUri",
+        ]
+        mutually_exclusive(self.__class__.__name__, self.properties, conds)
+
+
 class PrimaryKey(AWSProperty):
     props = {"Name": (str, False), "Type": (primary_key_type_validator, False)}
 
diff --git a/troposphere/signer.py b/troposphere/signer.py
new file mode 100644
index 00000000..1972aaec
--- /dev/null
+++ b/troposphere/signer.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2012-2021, Mark Peek <mark@peek.org>
+# All rights reserved.
+#
+# See LICENSE file for full license.
+#
+# *** Do not modify - this file is autogenerated ***
+# Resource specification version: 39.8.0
+
+
+from troposphere import Tags
+
+from . import AWSObject, AWSProperty
+from .validators import integer
+
+
+class ProfilePermission(AWSObject):
+    resource_type = "AWS::Signer::ProfilePermission"
+
+    props = {
+        "Action": (str, True),
+        "Principal": (str, True),
+        "ProfileName": (str, True),
+        "ProfileVersion": (str, False),
+        "StatementId": (str, True),
+    }
+
+
+class SignatureValidityPeriod(AWSProperty):
+    props = {
+        "Type": (str, False),
+        "Value": (integer, False),
+    }
+
+
+class SigningProfile(AWSObject):
+    resource_type = "AWS::Signer::SigningProfile"
+
+    props = {
+        "PlatformId": (str, True),
+        "SignatureValidityPeriod": (SignatureValidityPeriod, False),
+        "Tags": (Tags, False),
+    }
diff --git a/troposphere/wafv2.py b/troposphere/wafv2.py
index 50a40bc1..d42d384d 100644
--- a/troposphere/wafv2.py
+++ b/troposphere/wafv2.py
@@ -7,6 +7,7 @@ from . import AWSObject, AWSProperty, Tags
 from .validators import (
     boolean,
     integer,
+    json_checker,
     wafv2_custom_body_response_content,
     wafv2_custom_body_response_content_type,
 )
@@ -215,6 +216,7 @@ class ManagedRuleGroupStatement(AWSProperty):
         "ExcludedRules": ([ExcludedRule], False),
         "Name": (str, False),
         "VendorName": (str, False),
+        "Version": (str, False),
     }
 
 
@@ -395,6 +397,17 @@ class IPSet(AWSObject):
     }
 
 
+class LoggingConfiguration(AWSObject):
+    resource_type = "AWS::WAFv2::LoggingConfiguration"
+
+    props = {
+        "LogDestinationConfigs": ([str], True),
+        "LoggingFilter": (json_checker, False),
+        "RedactedFields": ([FieldToMatch], False),
+        "ResourceArn": (str, True),
+    }
+
+
 class RegexPatternSet(AWSObject):
     resource_type = "AWS::WAFv2::RegexPatternSet"
 


Test patch:
diff --git a/tests/examples_output/RedshiftClusterInVpc.template b/tests/examples_output/RedshiftClusterInVpc.template
index fde1181f..b327445e 100644
--- a/tests/examples_output/RedshiftClusterInVpc.template
+++ b/tests/examples_output/RedshiftClusterInVpc.template
@@ -122,9 +122,11 @@
                         }
                     ]
                 },
-                "VpcSecurityGroupIds": {
-                    "Ref": "SecurityGroup"
-                }
+                "VpcSecurityGroupIds": [
+                    {
+                        "Ref": "SecurityGroup"
+                    }
+                ]
             },
             "Type": "AWS::Redshift::Cluster"
         },
@@ -144,9 +146,11 @@
         "RedshiftClusterSubnetGroup": {
             "Properties": {
                 "Description": "Cluster subnet group",
-                "SubnetIds": {
-                    "Ref": "Subnet"
-                }
+                "SubnetIds": [
+                    {
+                        "Ref": "Subnet"
+                    }
+                ]
             },
             "Type": "AWS::Redshift::ClusterSubnetGroup"
         },
diff --git a/tests/test_logs.py b/tests/test_logs.py
index 5a865097..3fdce641 100644
--- a/tests/test_logs.py
+++ b/tests/test_logs.py
@@ -1,7 +1,7 @@
 import unittest
 
 from troposphere import Retain
-from troposphere.logs import Destination, LogGroup
+from troposphere.logs import Destination, LogGroup, validate_resource_policy, LogResourcePolicy
 
 
 class TestLogs(unittest.TestCase):
@@ -35,6 +35,22 @@ class TestLogs(unittest.TestCase):
         self.assertIn("Type", log_destination_json)
         self.assertIn("Properties", log_destination_json)
 
+    def test_validate_resource_policy(self):
+        for s in ["{ \"Version\": \"2012-10-17\", \"Statement\": [ { \"Sid\": \"Route53LogsToCloudWatchLogs\", \"Effect\": \"Allow\", \"Principal\": { \"Service\": [ \"route53.amazonaws.com\" ] }, \"Action\":\"logs:PutLogEvents\", \"Resource\": \"logArn\" } ] }", {'Version': '2012-10-17', 'Statement': [{'Sid': 'Route53LogsToCloudWatchLogs', 'Effect': 'Allow', 'Principal': {'Service': ['route53.amazonaws.com']}, 'Action': 'logs:PutLogEvents', 'Resource': 'logArn'}]}]:
+            validate_resource_policy(s)
+            log_policy = LogResourcePolicy(
+                "TestLogPolicy",
+                PolicyName='TestLogPolicy',
+                PolicyDocument=s
+            )
+            expected = log_policy.to_dict()
+            properties = expected['Properties']
+            self.assertEqual(properties.get('PolicyDocument'), s)
+
+        for s in ["", "H"*5121, "TEXT", {}]:
+            with self.assertRaises(ValueError):
+                validate_resource_policy(s)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_serverless.py b/tests/test_serverless.py
index 945b0bbc..ee3da48f 100644
--- a/tests/test_serverless.py
+++ b/tests/test_serverless.py
@@ -17,6 +17,10 @@ from troposphere.serverless import (
     S3Event,
     S3Location,
     SimpleTable,
+    HttpApi,
+    HttpApiAuth,
+    OAuth2Authorizer,
+    HttpApiDomainConfiguration,
 )
 
 
@@ -186,7 +190,7 @@ class TestServerless(unittest.TestCase):
         t.add_resource(serverless_api)
         t.to_json()
 
-    def test_api_with_endpoint_configuation(self):
+    def test_api_with_endpoint_configuration(self):
         serverless_api = Api(
             title="SomeApi",
             StageName="testStageName",
@@ -217,6 +221,80 @@ class TestServerless(unittest.TestCase):
         t.add_resource(serverless_api)
         t.to_json()
 
+    def test_http_api_definition_uri_defined(self):
+        serverless_http_api = HttpApi(
+            "SomeHttpApi",
+            StageName="testHttp",
+            DefinitionUri="s3://bucket/swagger.yml",
+        )
+        t = Template()
+        t.add_resource(serverless_http_api)
+        t.to_json()
+
+    def test_http_api_both_definition_uri_and_body_defined(self):
+        serverless_http_api = HttpApi(
+            "SomeHttpApi",
+            StageName="testHttp",
+            DefinitionUri="s3://bucket/swagger.yml",
+            DefinitionBody=self.swagger,
+        )
+        t = Template()
+        t.add_resource(serverless_http_api)
+        with self.assertRaises(ValueError):
+            t.to_json()
+
+    def test_http_api_definition_body(self):
+        serverless_http_api = HttpApi(
+            "SomeHttpApi",
+            StageName="testHttp",
+            DefinitionBody=self.swagger,
+        )
+        t = Template()
+        t.add_resource(serverless_http_api)
+        t.to_json()
+
+    def test_http_api_no_definition(self):
+        serverless_api = HttpApi(
+            "SomeHttpApi",
+            StageName="testHttp",
+        )
+        t = Template()
+        t.add_resource(serverless_api)
+        t.to_json()
+
+    def test_http_api_authorization_scopes(self):
+        serverless_api = HttpApi(
+            title="SomeHttpApi",
+            Auth=HttpApiAuth(
+                Authorizers=OAuth2Authorizer(AuthorizationScopes=["scope1", "scope2"])
+            ),
+            StageName="testHttpStageName",
+        )
+        t = Template()
+        t.add_resource(serverless_api)
+        t.to_json()
+
+    def test_http_api_with_domain(self):
+        certificate = Parameter("certificate", Type="String")
+        serverless_http_api = HttpApi(
+            "SomeHttpApi",
+            StageName="testHttp",
+            Domain=HttpApiDomainConfiguration(
+                BasePath=["/"],
+                CertificateArn=Ref(certificate),
+                DomainName=Sub("subdomain.${Zone}", Zone=ImportValue("MyZone")),
+                EndpointConfiguration="REGIONAL",
+                Route53=Route53(
+                    HostedZoneId=ImportValue("MyZone"),
+                    IpV6=True,
+                ),
+            ),
+        )
+        t = Template()
+        t.add_parameter(certificate)
+        t.add_resource(serverless_http_api)
+        t.to_json()
+
     def test_simple_table(self):
         serverless_table = SimpleTable("SomeTable")
         t = Template()