diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee88b6c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IDE configurations
+*.iml
+.vscode/
+
+# Build
+target/
+build
+
+# Generated Files
+.classpath
+.factorypath
+.project
+.settings/
+.gradle/
+
+# MacOS
+.DS_Store
+
+# IntelliJ
+*.iml
+.idea
+out.java
+out/
+.settings
+.project
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/.rpdk-config b/aws-sagemaker-dataqualityjobdefinition/.rpdk-config
new file mode 100644
index 0000000..b949f61
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/.rpdk-config
@@ -0,0 +1,16 @@
+{
+ "typeName": "AWS::SageMaker::DataQualityJobDefinition",
+ "language": "java",
+ "runtime": "java8",
+ "entrypoint": "software.amazon.sagemaker.dataqualityjobdefinition.HandlerWrapper::handleRequest",
+ "testEntrypoint": "software.amazon.sagemaker.dataqualityjobdefinition.HandlerWrapper::testEntrypoint",
+ "settings": {
+ "namespace": [
+ "software",
+ "amazon",
+ "sagemaker",
+ "dataqualityjobdefinition"
+ ],
+ "protocolVersion": "2.0.0"
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/README.md b/aws-sagemaker-dataqualityjobdefinition/README.md
new file mode 100644
index 0000000..aa955b7
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/README.md
@@ -0,0 +1,178 @@
+# AWS::SageMaker::DataQualityJobDefinition
+
+Resource Type definition for AWS::SageMaker::DataQualityJobDefinition
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::DataQualityJobDefinition",
+ "Properties" : {
+ "JobDefinitionName " : String ,
+ "DataQualityBaselineConfig " : DataQualityBaselineConfig ,
+ "DataQualityAppSpecification " : DataQualityAppSpecification ,
+ "DataQualityJobInput " : DataQualityJobInput ,
+ "DataQualityJobOutputConfig " : MonitoringOutputConfig ,
+ "JobResources " : MonitoringResources ,
+ "NetworkConfig " : NetworkConfig ,
+ "RoleArn " : String ,
+ "StoppingCondition " : StoppingCondition ,
+ "Tags " : [ Tag , ... ] ,
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::DataQualityJobDefinition
+Properties:
+ JobDefinitionName : String
+ DataQualityBaselineConfig : DataQualityBaselineConfig
+ DataQualityAppSpecification : DataQualityAppSpecification
+ DataQualityJobInput : DataQualityJobInput
+ DataQualityJobOutputConfig : MonitoringOutputConfig
+ JobResources : MonitoringResources
+ NetworkConfig : NetworkConfig
+ RoleArn : String
+ StoppingCondition : StoppingCondition
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### JobDefinitionName
+
+The name of the job definition.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+_Required_: No
+
+_Type_: DataQualityBaselineConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityAppSpecification
+
+Container image configuration object for the monitoring job.
+
+_Required_: Yes
+
+_Type_: DataQualityAppSpecification
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityJobInput
+
+The inputs for a monitoring job.
+
+_Required_: Yes
+
+_Type_: DataQualityJobInput
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityJobOutputConfig
+
+The output configuration for monitoring jobs.
+
+_Required_: Yes
+
+_Type_: MonitoringOutputConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### JobResources
+
+Identifies the resources to deploy for a monitoring job.
+
+_Required_: Yes
+
+_Type_: MonitoringResources
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+_Required_: No
+
+_Type_: NetworkConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+_Required_: No
+
+_Type_: StoppingCondition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pairs to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the JobDefinitionArn.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+The time at which the job definition was created.
+
+#### JobDefinitionArn
+
+The Amazon Resource Name (ARN) of job definition.
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/aws-sagemaker-dataqualityjobdefinition.json b/aws-sagemaker-dataqualityjobdefinition/aws-sagemaker-dataqualityjobdefinition.json
new file mode 100644
index 0000000..0c813b8
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/aws-sagemaker-dataqualityjobdefinition.json
@@ -0,0 +1,452 @@
+{
+ "typeName" : "AWS::SageMaker::DataQualityJobDefinition",
+ "description" : "Resource Type definition for AWS::SageMaker::DataQualityJobDefinition",
+ "additionalProperties" : false,
+ "properties" : {
+ "JobDefinitionArn" : {
+ "description": "The Amazon Resource Name (ARN) of job definition.",
+ "type" : "string",
+ "minLength": 1,
+ "maxLength": 256
+ },
+ "JobDefinitionName" : {
+ "$ref" : "#/definitions/JobDefinitionName"
+ },
+ "DataQualityBaselineConfig": {
+ "$ref": "#/definitions/DataQualityBaselineConfig"
+ },
+ "DataQualityAppSpecification": {
+ "$ref": "#/definitions/DataQualityAppSpecification"
+ },
+ "DataQualityJobInput": {
+ "$ref": "#/definitions/DataQualityJobInput"
+ },
+ "DataQualityJobOutputConfig": {
+ "$ref": "#/definitions/MonitoringOutputConfig"
+ },
+ "JobResources": {
+ "$ref": "#/definitions/MonitoringResources"
+ },
+ "NetworkConfig": {
+ "$ref": "#/definitions/NetworkConfig"
+ },
+ "RoleArn": {
+ "description": "The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.",
+ "type" : "string",
+ "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$",
+ "minLength": 20,
+ "maxLength": 2048
+ },
+ "StoppingCondition": {
+ "$ref": "#/definitions/StoppingCondition"
+ },
+ "Tags" : {
+ "type" : "array",
+ "maxItems" : 50,
+ "description" : "An array of key-value pairs to apply to this resource.",
+ "items" : {
+ "$ref" : "#/definitions/Tag"
+ }
+ },
+ "CreationTime": {
+ "description": "The time at which the job definition was created.",
+ "type": "string"
+ }
+ },
+ "definitions" : {
+ "DataQualityBaselineConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Baseline configuration used to validate that the data conforms to the specified constraints and statistics.",
+ "properties" : {
+ "BaseliningJobName": {
+ "$ref": "#/definitions/ProcessingJobName"
+ },
+ "ConstraintsResource": {
+ "$ref": "#/definitions/ConstraintsResource"
+ },
+ "StatisticsResource": {
+ "$ref": "#/definitions/StatisticsResource"
+ }
+ }
+ },
+ "ConstraintsResource" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The baseline constraints resource for a monitoring job.",
+ "properties" : {
+ "S3Uri": {
+ "description": "The Amazon S3 URI for baseline constraint file in Amazon S3 that the current monitoring job should validated against.",
+ "$ref": "#/definitions/S3Uri"
+ }
+ }
+ },
+ "StatisticsResource" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The baseline statistics resource for a monitoring job.",
+ "properties" : {
+ "S3Uri": {
+ "description": "The Amazon S3 URI for the baseline statistics file in Amazon S3 that the current monitoring job should be validated against.",
+ "$ref": "#/definitions/S3Uri"
+ }
+ }
+ },
+ "S3Uri": {
+ "type": "string",
+ "description": "The Amazon S3 URI.",
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$",
+ "maxLength": 1024
+ },
+ "Environment" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "Sets the environment variables in the Docker container",
+ "patternProperties" : {
+ "[a-zA-Z_][a-zA-Z0-9_]*": {
+ "type": "string",
+ "minLength" : 1,
+ "maxLength" : 256
+ },
+ "[\\S\\s]*": {
+ "type": "string",
+ "maxLength" : 256
+ }
+ }
+ },
+ "DataQualityAppSpecification" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Container image configuration object for the monitoring job.",
+ "properties" : {
+ "ContainerArguments": {
+ "type": "array",
+ "description": "An array of arguments for the container used to run the monitoring job.",
+ "maxItems" : 50,
+ "items" : {
+ "type" : "string",
+ "minLength" : 1,
+ "maxLength": 256
+ }
+ },
+ "ContainerEntrypoint": {
+ "type": "array",
+ "description": "Specifies the entrypoint for a container used to run the monitoring job.",
+ "maxItems" : 100,
+ "items" : {
+ "type" : "string",
+ "minLength" : 1,
+ "maxLength": 256
+ }
+ },
+ "ImageUri": {
+ "type" : "string",
+ "description" : "The container image to be run by the monitoring job.",
+ "pattern": ".*",
+ "maxLength" : 255
+ },
+ "PostAnalyticsProcessorSourceUri": {
+ "description" : "An Amazon S3 URI to a script that is called after analysis has been performed. Applicable only for the built-in (first party) containers.",
+ "$ref": "#/definitions/S3Uri"
+ },
+ "RecordPreprocessorSourceUri": {
+ "description" : "An Amazon S3 URI to a script that is called per row prior to running analysis. It can base64 decode the payload and convert it into a flatted json so that the built-in container can use the converted data. Applicable only for the built-in (first party) containers",
+ "$ref": "#/definitions/S3Uri"
+ },
+ "Environment": {
+ "$ref": "#/definitions/Environment"
+ }
+ },
+ "required" : [ "ImageUri" ]
+ },
+ "DataQualityJobInput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "The inputs for a monitoring job.",
+ "properties" : {
+ "EndpointInput": {
+ "$ref" : "#/definitions/EndpointInput"
+ }
+ },
+ "required": [ "EndpointInput" ]
+ },
+ "EndpointInput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The endpoint for a monitoring job.",
+ "properties" : {
+ "EndpointName": {
+ "$ref" : "#/definitions/EndpointName"
+ },
+ "LocalPath": {
+ "type" : "string",
+ "description" : "Path to the filesystem where the endpoint data is available to the container.",
+ "pattern": ".*",
+ "maxLength" : 256
+ },
+ "S3DataDistributionType": {
+ "type" : "string",
+ "description" : "Whether input data distributed in Amazon S3 is fully replicated or sharded by an S3 key. Defauts to FullyReplicated",
+ "enum":[
+ "FullyReplicated",
+ "ShardedByS3Key"
+ ]
+ },
+ "S3InputMode": {
+ "type" : "string",
+ "description" : "Whether the Pipe or File is used as the input mode for transfering data for the monitoring job. Pipe mode is recommended for large datasets. File mode is useful for small files that fit in memory. Defaults to File.",
+ "enum":[
+ "Pipe",
+ "File"
+ ]
+ }
+ },
+ "required" : [ "EndpointName", "LocalPath" ]
+ },
+ "MonitoringOutputConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The output configuration for monitoring jobs.",
+ "properties" : {
+ "KmsKeyId": {
+ "type" : "string",
+ "description" : "The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption.",
+ "pattern": ".*",
+ "maxLength" : 2048
+ },
+ "MonitoringOutputs" : {
+ "type" : "array",
+ "description" : "Monitoring outputs for monitoring jobs. This is where the output of the periodic monitoring jobs is uploaded.",
+ "minLength" : 1,
+ "maxLength" : 1,
+ "items" : {
+ "$ref" : "#/definitions/MonitoringOutput"
+ }
+ }
+ },
+ "required" : [ "MonitoringOutputs" ]
+ },
+ "MonitoringOutput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "The output object for a monitoring job.",
+ "properties" : {
+ "S3Output": {
+ "$ref" : "#/definitions/S3Output"
+ }
+ },
+ "required": [ "S3Output" ]
+ },
+ "S3Output" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Information about where and how to store the results of a monitoring job.",
+ "properties" : {
+ "LocalPath": {
+ "type" : "string",
+ "description" : "The local path to the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job. LocalPath is an absolute path for the output data.",
+ "pattern": ".*",
+ "maxLength" : 256
+ },
+ "S3UploadMode" : {
+ "type" : "string",
+ "description" : "Whether to upload the results of the monitoring job continuously or after the job completes.",
+ "enum":[
+ "Continuous",
+ "EndOfJob"
+ ]
+ },
+ "S3Uri" : {
+ "type" : "string",
+ "description" : "A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.",
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$",
+ "maxLength" : 512
+ }
+ },
+ "required" : [ "LocalPath", "S3Uri" ]
+ },
+ "MonitoringResources" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Identifies the resources to deploy for a monitoring job.",
+ "properties" : {
+ "ClusterConfig": {
+ "$ref" : "#/definitions/ClusterConfig"
+ }
+ },
+ "required" : [ "ClusterConfig" ]
+ },
+ "ClusterConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Configuration for the cluster used to run model monitoring jobs.",
+ "properties" : {
+ "InstanceCount": {
+ "description" : "The number of ML compute instances to use in the model monitoring job. For distributed processing jobs, specify a value greater than 1. The default value is 1.",
+ "type" : "integer",
+ "minimum" : 1,
+ "maximum" : 100
+ },
+ "InstanceType": {
+ "description" : "The ML compute instance type for the processing job.",
+ "type" : "string"
+ },
+ "VolumeKmsKeyId": {
+ "description" : "The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s) that run the model monitoring job.",
+ "type" : "string",
+ "minimum" : 1,
+ "maximum" : 2048
+ },
+ "VolumeSizeInGB": {
+ "description" : "The size of the ML storage volume, in gigabytes, that you want to provision. You must specify sufficient ML storage for your scenario.",
+ "type" : "integer",
+ "minimum" : 1,
+ "maximum" : 16384
+ }
+ },
+ "required" : [ "InstanceCount", "InstanceType", "VolumeSizeInGB" ]
+ },
+ "NetworkConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.",
+ "properties" : {
+ "EnableInterContainerTrafficEncryption": {
+ "description" : "Whether to encrypt all communications between distributed processing jobs. Choose True to encrypt communications. Encryption provides greater security for distributed processing jobs, but the processing might take longer.",
+ "type" : "boolean"
+ },
+ "EnableNetworkIsolation": {
+ "description" : "Whether to allow inbound and outbound network calls to and from the containers used for the processing job.",
+ "type" : "boolean"
+ },
+ "VpcConfig": {
+ "$ref" : "#/definitions/VpcConfig"
+ }
+ }
+ },
+ "VpcConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.",
+ "properties" : {
+ "SecurityGroupIds": {
+ "description" : "The VPC security group IDs, in the form sg-xxxxxxxx. Specify the security groups for the VPC that is specified in the Subnets field.",
+ "type" : "array",
+ "minItems" : 1,
+ "maxItems" : 5,
+ "items" : {
+ "type" : "string",
+ "maxLength": 32,
+ "pattern": "[-0-9a-zA-Z]+"
+ }
+ },
+ "Subnets": {
+ "description" : "The ID of the subnets in the VPC to which you want to connect to your monitoring jobs.",
+ "type" : "array",
+ "minItems" : 1,
+ "maxItems" : 16,
+ "items" : {
+ "type" : "string",
+ "maxLength": 32,
+ "pattern": "[-0-9a-zA-Z]+"
+ }
+ }
+ },
+ "required" : [ "SecurityGroupIds", "Subnets" ]
+ },
+ "StoppingCondition" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Specifies a time limit for how long the monitoring job is allowed to run.",
+ "properties" : {
+ "MaxRuntimeInSeconds": {
+ "description": "The maximum runtime allowed in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 86400
+ }
+ },
+ "required" : [ "MaxRuntimeInSeconds" ]
+ },
+ "Tag" : {
+ "description" : "A key-value pair to associate with a resource.",
+ "type" : "object",
+ "additionalProperties" : false,
+ "properties" : {
+ "Key" : {
+ "type" : "string",
+ "description" : "The key name of the tag. You can specify a value that is 1 to 127 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "minLength" : 1,
+ "maxLength" : 128,
+ "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$"
+ },
+ "Value" : {
+ "type" : "string",
+ "description" : "The value for the tag. You can specify a value that is 1 to 255 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "maxLength" : 256,
+ "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$"
+ }
+ },
+ "required" : [ "Key", "Value" ]
+ },
+ "EndpointName": {
+ "type" : "string",
+ "description" : "The name of the endpoint used to run the monitoring job.",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*",
+ "maxLength" : 63
+ },
+ "JobDefinitionName": {
+ "type" : "string",
+ "description" : "The name of the job definition.",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$",
+ "maxLength" : 63
+ },
+ "ProcessingJobName": {
+ "type" : "string",
+ "description" : "The name of a processing job",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$",
+ "minLength" : 1,
+ "maxLength" : 63
+ }
+ },
+ "required" : [ "DataQualityAppSpecification", "DataQualityJobInput", "DataQualityJobOutputConfig", "JobResources", "RoleArn"],
+ "primaryIdentifier" : [ "/properties/JobDefinitionArn" ],
+ "handlers": {
+ "create": {
+ "permissions": [
+ "sagemaker:CreateDataQualityJobDefinition",
+ "sagemaker:DescribeDataQualityJobDefinition",
+ "iam:PassRole"
+ ]
+ },
+ "delete": {
+ "permissions": [
+ "sagemaker:DeleteDataQualityJobDefinition"
+ ]
+ },
+ "read": {
+ "permissions": [
+ "sagemaker:DescribeDataQualityJobDefinition"
+ ]
+ },
+ "update": {
+ "permissions": []
+ }
+ },
+ "readOnlyProperties": [
+ "/properties/CreationTime",
+ "/properties/JobDefinitionArn"
+ ],
+ "createOnlyProperties": [
+ "/properties/JobDefinitionName",
+ "/properties/DataQualityAppSpecification",
+ "/properties/DataQualityBaselineConfig",
+ "/properties/DataQualityJobInput",
+ "/properties/DataQualityJobOutputConfig",
+ "/properties/JobResources",
+ "/properties/NetworkConfig",
+ "/properties/RoleArn",
+ "/properties/StoppingCondition",
+ "/properties/Tags"
+ ]
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/README.md b/aws-sagemaker-dataqualityjobdefinition/docs/README.md
new file mode 100644
index 0000000..aa955b7
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/README.md
@@ -0,0 +1,178 @@
+# AWS::SageMaker::DataQualityJobDefinition
+
+Resource Type definition for AWS::SageMaker::DataQualityJobDefinition
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::DataQualityJobDefinition",
+ "Properties" : {
+ "JobDefinitionName " : String ,
+ "DataQualityBaselineConfig " : DataQualityBaselineConfig ,
+ "DataQualityAppSpecification " : DataQualityAppSpecification ,
+ "DataQualityJobInput " : DataQualityJobInput ,
+ "DataQualityJobOutputConfig " : MonitoringOutputConfig ,
+ "JobResources " : MonitoringResources ,
+ "NetworkConfig " : NetworkConfig ,
+ "RoleArn " : String ,
+ "StoppingCondition " : StoppingCondition ,
+ "Tags " : [ Tag , ... ] ,
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::DataQualityJobDefinition
+Properties:
+ JobDefinitionName : String
+ DataQualityBaselineConfig : DataQualityBaselineConfig
+ DataQualityAppSpecification : DataQualityAppSpecification
+ DataQualityJobInput : DataQualityJobInput
+ DataQualityJobOutputConfig : MonitoringOutputConfig
+ JobResources : MonitoringResources
+ NetworkConfig : NetworkConfig
+ RoleArn : String
+ StoppingCondition : StoppingCondition
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### JobDefinitionName
+
+The name of the job definition.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+_Required_: No
+
+_Type_: DataQualityBaselineConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityAppSpecification
+
+Container image configuration object for the monitoring job.
+
+_Required_: Yes
+
+_Type_: DataQualityAppSpecification
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityJobInput
+
+The inputs for a monitoring job.
+
+_Required_: Yes
+
+_Type_: DataQualityJobInput
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### DataQualityJobOutputConfig
+
+The output configuration for monitoring jobs.
+
+_Required_: Yes
+
+_Type_: MonitoringOutputConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### JobResources
+
+Identifies the resources to deploy for a monitoring job.
+
+_Required_: Yes
+
+_Type_: MonitoringResources
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+_Required_: No
+
+_Type_: NetworkConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+_Required_: No
+
+_Type_: StoppingCondition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pairs to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the JobDefinitionArn.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+The time at which the job definition was created.
+
+#### JobDefinitionArn
+
+The Amazon Resource Name (ARN) of job definition.
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/clusterconfig.md b/aws-sagemaker-dataqualityjobdefinition/docs/clusterconfig.md
new file mode 100644
index 0000000..a8f250c
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/clusterconfig.md
@@ -0,0 +1,70 @@
+# AWS::SageMaker::DataQualityJobDefinition ClusterConfig
+
+Configuration for the cluster used to run model monitoring jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "InstanceCount " : Integer ,
+ "InstanceType " : String ,
+ "VolumeKmsKeyId " : String ,
+ "VolumeSizeInGB " : Integer
+}
+
+
+### YAML
+
+
+InstanceCount : Integer
+InstanceType : String
+VolumeKmsKeyId : String
+VolumeSizeInGB : Integer
+
+
+## Properties
+
+#### InstanceCount
+
+The number of ML compute instances to use in the model monitoring job. For distributed processing jobs, specify a value greater than 1. The default value is 1.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### InstanceType
+
+The ML compute instance type for the processing job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VolumeKmsKeyId
+
+The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s) that run the model monitoring job.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VolumeSizeInGB
+
+The size of the ML storage volume, in gigabytes, that you want to provision. You must specify sufficient ML storage for your scenario.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/constraintsresource.md b/aws-sagemaker-dataqualityjobdefinition/docs/constraintsresource.md
new file mode 100644
index 0000000..6d2b521
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/constraintsresource.md
@@ -0,0 +1,38 @@
+# AWS::SageMaker::DataQualityJobDefinition ConstraintsResource
+
+The baseline constraints resource for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+S3Uri : String
+
+
+## Properties
+
+#### S3Uri
+
+The Amazon S3 URI.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification-environment.md b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification-environment.md
new file mode 100644
index 0000000..796eff6
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification-environment.md
@@ -0,0 +1,48 @@
+# AWS::SageMaker::DataQualityJobDefinition DataQualityAppSpecification Environment
+
+Sets the environment variables in the Docker container
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "[a-zA-Z_][a-zA-Z0-9_]* " : String ,
+ "[\S\s]* " : String
+}
+
+
+### YAML
+
+
+[a-zA-Z_][a-zA-Z0-9_]* : String
+[\S\s]* : String
+
+
+## Properties
+
+#### \[a-zA-Z_][a-zA-Z0-9_]*
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### \[\S\s]*
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification.md b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification.md
new file mode 100644
index 0000000..a74aab1
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityappspecification.md
@@ -0,0 +1,108 @@
+# AWS::SageMaker::DataQualityJobDefinition DataQualityAppSpecification
+
+Container image configuration object for the monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "ContainerArguments " : [ String, ... ] ,
+ "ContainerEntrypoint " : [ String, ... ] ,
+ "ImageUri " : String ,
+ "PostAnalyticsProcessorSourceUri " : String ,
+ "RecordPreprocessorSourceUri " : String ,
+ "Environment " : Environment
+}
+
+
+### YAML
+
+
+ContainerArguments :
+ - String
+ContainerEntrypoint :
+ - String
+ImageUri : String
+PostAnalyticsProcessorSourceUri : String
+RecordPreprocessorSourceUri : String
+Environment : Environment
+
+
+## Properties
+
+#### ContainerArguments
+
+An array of arguments for the container used to run the monitoring job.
+
+_Required_: No
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ContainerEntrypoint
+
+Specifies the entrypoint for a container used to run the monitoring job.
+
+_Required_: No
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ImageUri
+
+The container image to be run by the monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 255
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### PostAnalyticsProcessorSourceUri
+
+The Amazon S3 URI.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### RecordPreprocessorSourceUri
+
+The Amazon S3 URI.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Environment
+
+Sets the environment variables in the Docker container
+
+_Required_: No
+
+_Type_: Environment
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/dataqualitybaselineconfig.md b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualitybaselineconfig.md
new file mode 100644
index 0000000..1d6ebfa
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualitybaselineconfig.md
@@ -0,0 +1,64 @@
+# AWS::SageMaker::DataQualityJobDefinition DataQualityBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "BaseliningJobName " : String ,
+ "ConstraintsResource " : ConstraintsResource ,
+ "StatisticsResource " : StatisticsResource
+}
+
+
+### YAML
+
+
+BaseliningJobName : String
+ConstraintsResource : ConstraintsResource
+StatisticsResource : StatisticsResource
+
+
+## Properties
+
+#### BaseliningJobName
+
+The name of a processing job
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ConstraintsResource
+
+The baseline constraints resource for a monitoring job.
+
+_Required_: No
+
+_Type_: ConstraintsResource
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### StatisticsResource
+
+The baseline statistics resource for a monitoring job.
+
+_Required_: No
+
+_Type_: StatisticsResource
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityjobinput.md b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityjobinput.md
new file mode 100644
index 0000000..0af45b6
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/dataqualityjobinput.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::DataQualityJobDefinition DataQualityJobInput
+
+The inputs for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EndpointInput " : EndpointInput
+}
+
+
+### YAML
+
+
+EndpointInput : EndpointInput
+
+
+## Properties
+
+#### EndpointInput
+
+The endpoint for a monitoring job.
+
+_Required_: Yes
+
+_Type_: EndpointInput
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/endpointinput.md b/aws-sagemaker-dataqualityjobdefinition/docs/endpointinput.md
new file mode 100644
index 0000000..37e80d7
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/endpointinput.md
@@ -0,0 +1,82 @@
+# AWS::SageMaker::DataQualityJobDefinition EndpointInput
+
+The endpoint for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EndpointName " : String ,
+ "LocalPath " : String ,
+ "S3DataDistributionType " : String ,
+ "S3InputMode " : String
+}
+
+
+### YAML
+
+
+EndpointName : String
+LocalPath : String
+S3DataDistributionType : String
+S3InputMode : String
+
+
+## Properties
+
+#### EndpointName
+
+The name of the endpoint used to run the monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### LocalPath
+
+Path to the filesystem where the endpoint data is available to the container.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3DataDistributionType
+
+Whether input data distributed in Amazon S3 is fully replicated or sharded by an S3 key. Defauts to FullyReplicated
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: FullyReplicated
| ShardedByS3Key
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3InputMode
+
+Whether the Pipe or File is used as the input mode for transfering data for the monitoring job. Pipe mode is recommended for large datasets. File mode is useful for small files that fit in memory. Defaults to File.
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: Pipe
| File
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutput.md b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutput.md
new file mode 100644
index 0000000..02274d6
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutput.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::DataQualityJobDefinition MonitoringOutput
+
+The output object for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Output " : S3Output
+}
+
+
+### YAML
+
+
+S3Output : S3Output
+
+
+## Properties
+
+#### S3Output
+
+Information about where and how to store the results of a monitoring job.
+
+_Required_: Yes
+
+_Type_: S3Output
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutputconfig.md b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutputconfig.md
new file mode 100644
index 0000000..9c8f27a
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringoutputconfig.md
@@ -0,0 +1,55 @@
+# AWS::SageMaker::DataQualityJobDefinition MonitoringOutputConfig
+
+The output configuration for monitoring jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "KmsKeyId " : String ,
+ "MonitoringOutputs " : [ MonitoringOutput , ... ]
+}
+
+
+### YAML
+
+
+KmsKeyId : String
+MonitoringOutputs :
+ - MonitoringOutput
+
+
+## Properties
+
+#### KmsKeyId
+
+The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 2048
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### MonitoringOutputs
+
+Monitoring outputs for monitoring jobs. This is where the output of the periodic monitoring jobs is uploaded.
+
+_Required_: Yes
+
+_Type_: List of MonitoringOutput
+
+_Minimum_: 1
+
+_Maximum_: 1
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/monitoringresources.md b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringresources.md
new file mode 100644
index 0000000..c0ea64c
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/monitoringresources.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::DataQualityJobDefinition MonitoringResources
+
+Identifies the resources to deploy for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "ClusterConfig " : ClusterConfig
+}
+
+
+### YAML
+
+
+ClusterConfig : ClusterConfig
+
+
+## Properties
+
+#### ClusterConfig
+
+Configuration for the cluster used to run model monitoring jobs.
+
+_Required_: Yes
+
+_Type_: ClusterConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/networkconfig.md b/aws-sagemaker-dataqualityjobdefinition/docs/networkconfig.md
new file mode 100644
index 0000000..6b10d4f
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/networkconfig.md
@@ -0,0 +1,58 @@
+# AWS::SageMaker::DataQualityJobDefinition NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EnableInterContainerTrafficEncryption " : Boolean ,
+ "EnableNetworkIsolation " : Boolean ,
+ "VpcConfig " : VpcConfig
+}
+
+
+### YAML
+
+
+EnableInterContainerTrafficEncryption : Boolean
+EnableNetworkIsolation : Boolean
+VpcConfig : VpcConfig
+
+
+## Properties
+
+#### EnableInterContainerTrafficEncryption
+
+Whether to encrypt all communications between distributed processing jobs. Choose True to encrypt communications. Encryption provides greater security for distributed processing jobs, but the processing might take longer.
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### EnableNetworkIsolation
+
+Whether to allow inbound and outbound network calls to and from the containers used for the processing job.
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VpcConfig
+
+Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.
+
+_Required_: No
+
+_Type_: VpcConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/s3output.md b/aws-sagemaker-dataqualityjobdefinition/docs/s3output.md
new file mode 100644
index 0000000..bdf45f2
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/s3output.md
@@ -0,0 +1,68 @@
+# AWS::SageMaker::DataQualityJobDefinition S3Output
+
+Information about where and how to store the results of a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "LocalPath " : String ,
+ "S3UploadMode " : String ,
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+LocalPath : String
+S3UploadMode : String
+S3Uri : String
+
+
+## Properties
+
+#### LocalPath
+
+The local path to the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job. LocalPath is an absolute path for the output data.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3UploadMode
+
+Whether to upload the results of the monitoring job continuously or after the job completes.
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: Continuous
| EndOfJob
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3Uri
+
+A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 512
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/statisticsresource.md b/aws-sagemaker-dataqualityjobdefinition/docs/statisticsresource.md
new file mode 100644
index 0000000..6ef1174
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/statisticsresource.md
@@ -0,0 +1,38 @@
+# AWS::SageMaker::DataQualityJobDefinition StatisticsResource
+
+The baseline statistics resource for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+S3Uri : String
+
+
+## Properties
+
+#### S3Uri
+
+The Amazon S3 URI.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/stoppingcondition.md b/aws-sagemaker-dataqualityjobdefinition/docs/stoppingcondition.md
new file mode 100644
index 0000000..c79dbb9
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/stoppingcondition.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::DataQualityJobDefinition StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "MaxRuntimeInSeconds " : Integer
+}
+
+
+### YAML
+
+
+MaxRuntimeInSeconds : Integer
+
+
+## Properties
+
+#### MaxRuntimeInSeconds
+
+The maximum runtime allowed in seconds.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/tag.md b/aws-sagemaker-dataqualityjobdefinition/docs/tag.md
new file mode 100644
index 0000000..42d3d48
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/tag.md
@@ -0,0 +1,56 @@
+# AWS::SageMaker::DataQualityJobDefinition Tag
+
+A key-value pair to associate with a resource.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Key " : String ,
+ "Value " : String
+}
+
+
+### YAML
+
+
+Key : String
+Value : String
+
+
+## Properties
+
+#### Key
+
+The key name of the tag. You can specify a value that is 1 to 127 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 128
+
+_Pattern_: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Value
+
+The value for the tag. You can specify a value that is 1 to 255 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/docs/vpcconfig.md b/aws-sagemaker-dataqualityjobdefinition/docs/vpcconfig.md
new file mode 100644
index 0000000..8ae3b3b
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/docs/vpcconfig.md
@@ -0,0 +1,48 @@
+# AWS::SageMaker::DataQualityJobDefinition VpcConfig
+
+Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "SecurityGroupIds " : [ String, ... ] ,
+ "Subnets " : [ String, ... ]
+}
+
+
+### YAML
+
+
+SecurityGroupIds :
+ - String
+Subnets :
+ - String
+
+
+## Properties
+
+#### SecurityGroupIds
+
+The VPC security group IDs, in the form sg-xxxxxxxx. Specify the security groups for the VPC that is specified in the Subnets field.
+
+_Required_: Yes
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Subnets
+
+The ID of the subnets in the VPC to which you want to connect to your monitoring jobs.
+
+_Required_: Yes
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-dataqualityjobdefinition/lombok.config b/aws-sagemaker-dataqualityjobdefinition/lombok.config
new file mode 100644
index 0000000..7a21e88
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/aws-sagemaker-dataqualityjobdefinition/pom.xml b/aws-sagemaker-dataqualityjobdefinition/pom.xml
new file mode 100644
index 0000000..40d20f9
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/pom.xml
@@ -0,0 +1,210 @@
+
+
+ 4.0.0
+
+ software.amazon.sagemaker.dataqualityjobdefinition
+ aws-sagemaker-dataqualityjobdefinition-handler
+ aws-sagemaker-dataqualityjobdefinition-handler
+ 1.0-SNAPSHOT
+ jar
+
+
+ 1.8
+ 1.8
+ UTF-8
+ UTF-8
+
+
+
+
+
+ software.amazon.awssdk
+ sagemaker
+ 2.15.50
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ [2.0.0, 3.0.0)
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.4
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ 3.12.2
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.5.0-M1
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 2.26.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 2.26.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ -Xlint:all,-options,-processing
+ -Werror
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.3
+
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+ generate
+ generate-sources
+
+ exec
+
+
+ cfn
+ generate
+ ${project.basedir}
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.basedir}/target/generated-sources/rpdk
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.4
+
+
+ maven-surefire-plugin
+ 3.0.0-M3
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+
+ **/BaseConfiguration*
+ **/BaseHandler*
+ **/HandlerWrapper*
+ **/ResourceModel*
+
+
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+ jacoco-check
+
+ check
+
+
+
+
+ PACKAGE
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.4
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.5
+
+
+
+
+
+
+
+
+
+
+
+ ${project.basedir}
+
+ aws-sagemaker-dataqualityjobdefinition.json
+
+
+
+
+
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/resource-role.yaml b/aws-sagemaker-dataqualityjobdefinition/resource-role.yaml
new file mode 100644
index 0000000..712401f
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/resource-role.yaml
@@ -0,0 +1,34 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+ This CloudFormation template creates a role assumed by CloudFormation
+ during CRUDL operations to mutate resources on behalf of the customer.
+
+Resources:
+ ExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ MaxSessionDuration: 8400
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: resources.cloudformation.amazonaws.com
+ Action: sts:AssumeRole
+ Path: "/"
+ Policies:
+ - PolicyName: ResourceTypePolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - "iam:PassRole"
+ - "sagemaker:CreateDataQualityJobDefinition"
+ - "sagemaker:DeleteDataQualityJobDefinition"
+ - "sagemaker:DescribeDataQualityJobDefinition"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/BaseHandlerStd.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/BaseHandlerStd.java
new file mode 100644
index 0000000..5ac23db
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/BaseHandlerStd.java
@@ -0,0 +1,38 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+/**
+ * Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers
+ */
+public abstract class BaseHandlerStd extends BaseHandler {
+
+ protected static final String DATA_QUALITY_ARN_SUBSTRING = ":data-quality-job-definition/";
+
+ @Override
+ public final ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+ return handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ proxy.newProxy(ClientBuilder::getClient),
+ logger
+ );
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger);
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CallbackContext.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CallbackContext.java
new file mode 100644
index 0000000..d55a635
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CallbackContext.java
@@ -0,0 +1,7 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.cloudformation.proxy.StdCallbackContext;
+
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext {
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ClientBuilder.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ClientBuilder.java
new file mode 100644
index 0000000..321a754
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ClientBuilder.java
@@ -0,0 +1,12 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+
+/**
+ * Provides APIs to build service client.
+ */
+public class ClientBuilder {
+ public static SageMakerClient getClient() {
+ return SageMakerClient.builder().build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Configuration.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Configuration.java
new file mode 100644
index 0000000..f12ee53
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Configuration.java
@@ -0,0 +1,7 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+class Configuration extends BaseConfiguration {
+ public Configuration() {
+ super("aws-sagemaker-dataqualityjobdefinition.json");
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandler.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandler.java
new file mode 100644
index 0000000..844ce3f
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandler.java
@@ -0,0 +1,109 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.CreateDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.CreateDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.util.Random;
+
+
+public class CreateHandler extends BaseHandlerStd {
+ public static final int ALLOWED_JOB_DEFINITION_NAME_LENGTH = 20;
+ public static final String CFN_RESOURCE_NAME_PREFIX = "CFN";
+ public static final int GUID_LENGTH = 12;
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = generateParameterName(request.getLogicalResourceIdentifier(),
+ request.getClientRequestToken());
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate("AWS-SageMaker-DataQualityJobDefinition::Create", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToCreateRequest)
+ .makeServiceCall(this::createResource)
+ .progress())
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+
+ /**
+ * Client invocation of the create request through the proxyClient, which is already initialised with
+ * caller credentials, region and retry settings
+ * @param awsRequest the aws service request to create a resource
+ * @param proxyClient the aws service client to make the call
+ * @return awsResponse create resource response
+ */
+ private CreateDataQualityJobDefinitionResponse createResource(
+ final CreateDataQualityJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ CreateDataQualityJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createDataQualityJobDefinition);
+ } catch (final ResourceInUseException e) {
+ throw new ResourceAlreadyExistsException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName());
+ } catch (final AwsServiceException e) {
+
+ // The exception thrown due to validation failure does not have error code set,
+ // hence we need to check it using error message
+ if(StringUtils.isNotBlank(e.getMessage()) && e.getMessage().contains("validation error detected")) {
+ throw new CfnInvalidRequestException(Action.CREATE.toString(), e);
+ }
+ Translator.throwCfnException(Action.CREATE.toString(), e);
+ }
+
+ return response;
+ }
+
+
+ // We support this special use case of auto-generating names only for CloudFormation.
+ // Name format: Prefix - logical resource id - randomString
+ private String generateParameterName(final String logicalResourceId, final String clientRequestToken) {
+ StringBuilder sb = new StringBuilder();
+ int endIndex = logicalResourceId.length() > ALLOWED_JOB_DEFINITION_NAME_LENGTH
+ ? ALLOWED_JOB_DEFINITION_NAME_LENGTH : logicalResourceId.length();
+
+ sb.append(CFN_RESOURCE_NAME_PREFIX);
+ sb.append("-");
+ sb.append(logicalResourceId.substring(0, endIndex));
+ sb.append("-");
+
+ sb.append(RandomStringUtils.random(
+ GUID_LENGTH,
+ 0,
+ 0,
+ true,
+ true,
+ null,
+ new Random(clientRequestToken.hashCode())));
+ return sb.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandler.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandler.java
new file mode 100644
index 0000000..bc1e345
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandler.java
@@ -0,0 +1,73 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DeleteDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DeleteDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class DeleteHandler extends BaseHandlerStd {
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = Utils.getResourceNameFromArn(model.getJobDefinitionArn(), DATA_QUALITY_ARN_SUBSTRING);
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate("AWS-SageMaker-DataQualityJobDefinition::Delete", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToDeleteRequest)
+ .makeServiceCall(this::deleteResource)
+ .done(awsResponse -> ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .build()));
+ }
+
+ /**
+ * Implement client invocation of the delete request through the proxyClient.
+ *
+ * @param awsRequest the aws service request to delete a resource
+ * @param proxyClient the aws service client to make the call
+ * @return delete resource response
+ */
+ private DeleteDataQualityJobDefinitionResponse deleteResource(
+ final DeleteDataQualityJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ DeleteDataQualityJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteDataQualityJobDefinition);
+ } catch (ResourceNotFoundException e) {
+ // NotFound responded from Delete handler will be considered as success by CFN backend service.
+ // This is to handle out of stack resource deletion (https://sage.amazon.com/questions/896677)
+ throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName());
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.DELETE.toString(), e);
+ }
+
+ return response;
+ }
+}
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandler.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandler.java
new file mode 100644
index 0000000..b8fe3c0
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandler.java
@@ -0,0 +1,79 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class ReadHandler extends BaseHandlerStd {
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = Utils.getResourceNameFromArn(model.getJobDefinitionArn(), DATA_QUALITY_ARN_SUBSTRING);
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return proxy.initiate("AWS-SageMaker-DataQualityJobDefinition::Read", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToReadRequest)
+ .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest, sdkProxyClient, model))
+ .done(this::constructResourceModelFromResponse);
+ }
+
+ /**
+ * Client invocation of the read request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ * @param awsRequest the aws service request to describe a resource
+ * @param proxyClient the aws service client to make the call
+ * @return describe resource response
+ */
+ private DescribeDataQualityJobDefinitionResponse readResource(
+ final DescribeDataQualityJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient,
+ final ResourceModel model) {
+
+ DescribeDataQualityJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeDataQualityJobDefinition);
+ } catch (final ResourceNotFoundException e) {
+ throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName(), e);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.READ.toString(), e);
+ }
+
+ return response;
+ }
+
+ /**
+ * Implement client invocation of the read request through the proxyClient, which is already
+ * initialised with caller credentials, correct region and retry settings
+ *
+ * @param awsResponse the aws service describe resource response
+ * @return progressEvent indicating success, in progress with delay callback or failed state
+ */
+ private ProgressEvent constructResourceModelFromResponse(
+ final DescribeDataQualityJobDefinitionResponse awsResponse) {
+ return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse));
+ }
+}
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Translator.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Translator.java
new file mode 100644
index 0000000..edb4262
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Translator.java
@@ -0,0 +1,61 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.cloudformation.exceptions.CfnAccessDeniedException;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
+import software.amazon.cloudformation.exceptions.CfnThrottlingException;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * This class contains translation methods for object other than api request/response.
+ * It also contains common methods required by other translators.
+ */
+public class Translator {
+
+ /**
+ * Throws Cfn exception corresponding to error code of the given exception.
+ *
+ * @param operation
+ * @param e exception
+ */
+ public static void throwCfnException(final String operation, final AwsServiceException e) {
+ if(e.awsErrorDetails() != null && StringUtils.isNotBlank(e.awsErrorDetails().errorCode())) {
+ switch (e.awsErrorDetails().errorCode()) {
+ case "UnauthorizedOperation":
+ throw new CfnAccessDeniedException(operation, e);
+ case "InvalidParameter":
+ case "InvalidParameterValue":
+ case "ValidationError":
+ throw new CfnInvalidRequestException(operation, e);
+ case "InternalError":
+ case "ServiceUnavailable":
+ throw new CfnServiceInternalErrorException(operation, e);
+ case "ResourceLimitExceeded":
+ throw new CfnServiceLimitExceededException(e);
+ case "ResourceNotFound":
+ throw new CfnNotFoundException(e);
+ case "ThrottlingException":
+ throw new CfnThrottlingException(operation, e);
+ default:
+ throw new CfnGeneralServiceException(operation, e);
+ }
+ }
+
+ throw new CfnGeneralServiceException(operation, e);
+ }
+
+ public static Stream streamOfOrEmpty(final Collection collection) {
+ return Optional.ofNullable(collection)
+ .map(Collection::stream)
+ .orElseGet(Stream::empty);
+ }
+
+}
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForRequest.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForRequest.java
new file mode 100644
index 0000000..9b061e4
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForRequest.java
@@ -0,0 +1,193 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.model.CreateDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityAppSpecification;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityBaselineConfig;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityJobInput;
+import software.amazon.awssdk.services.sagemaker.model.DeleteDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.EndpointInput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringNetworkConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutputConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringResources;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringS3Output;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStatisticsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition;
+import software.amazon.awssdk.services.sagemaker.model.Tag;
+import software.amazon.awssdk.services.sagemaker.model.VpcConfig;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * This class is a centralized placeholder for
+ * - api request construction
+ * - object translation to/from aws sdk
+ * - resource model construction for handlers like read/list
+ */
+final class TranslatorForRequest {
+
+ private TranslatorForRequest() {}
+
+ /**
+ * Request to create a resource
+ * @param model resource model
+ * @return createDataQualityJobDefinitionRequest - service request to create a resource
+ */
+ static CreateDataQualityJobDefinitionRequest translateToCreateRequest(final ResourceModel model) {
+ return CreateDataQualityJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .dataQualityAppSpecification(translate(model.getDataQualityAppSpecification()))
+ .dataQualityBaselineConfig(translate(model.getDataQualityBaselineConfig()))
+ .dataQualityJobInput(translate(model.getDataQualityJobInput()))
+ .dataQualityJobOutputConfig(translate(model.getDataQualityJobOutputConfig()))
+ .jobResources(translate(model.getJobResources()))
+ .networkConfig(translate(model.getNetworkConfig()))
+ .roleArn(model.getRoleArn())
+ .stoppingCondition(translate(model.getStoppingCondition()))
+ .tags(Translator.streamOfOrEmpty(model.getTags())
+ .map(curTag -> Tag.builder()
+ .key(curTag.getKey())
+ .value(curTag.getValue())
+ .build())
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ /**
+ * Request to read a resource
+ * @param model resource model
+ * @return describeDataQualityJobDefinitionRequest - the aws service request to describe a resource
+ */
+ static DescribeDataQualityJobDefinitionRequest translateToReadRequest(final ResourceModel model) {
+ return DescribeDataQualityJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .build();
+ }
+
+ /**
+ * Request to delete a resource
+ * @param model resource model
+ * @return deleteDataQualityJobDefinitionRequest the aws service request to delete a resource
+ */
+ static DeleteDataQualityJobDefinitionRequest translateToDeleteRequest(final ResourceModel model) {
+ return DeleteDataQualityJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .build();
+ }
+
+ static DataQualityAppSpecification translate(final software.amazon.sagemaker.dataqualityjobdefinition.DataQualityAppSpecification appSpec) {
+ return appSpec == null ? null : DataQualityAppSpecification.builder()
+ .containerArguments(appSpec.getContainerArguments())
+ .containerEntrypoint(appSpec.getContainerEntrypoint())
+ .imageUri(appSpec.getImageUri())
+ .postAnalyticsProcessorSourceUri(appSpec.getPostAnalyticsProcessorSourceUri())
+ .recordPreprocessorSourceUri(appSpec.getRecordPreprocessorSourceUri())
+ .environment(translateMapOfObjectsToMapOfStrings(appSpec.getEnvironment()))
+ .build();
+ }
+
+ static DataQualityBaselineConfig translate(final software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig baselineConfig) {
+ return baselineConfig == null ? null : DataQualityBaselineConfig.builder()
+ .baseliningJobName(baselineConfig.getBaseliningJobName())
+ .constraintsResource(translate(baselineConfig.getConstraintsResource()))
+ .statisticsResource(translate(baselineConfig.getStatisticsResource()))
+ .build();
+ }
+
+ static MonitoringConstraintsResource translate(final software.amazon.sagemaker.dataqualityjobdefinition.ConstraintsResource constraintsResource) {
+ return constraintsResource == null ? null : MonitoringConstraintsResource.builder().s3Uri(constraintsResource.getS3Uri()).build();
+ }
+
+ static MonitoringStatisticsResource translate(final software.amazon.sagemaker.dataqualityjobdefinition.StatisticsResource statisticsResource) {
+ return statisticsResource == null ? null : MonitoringStatisticsResource.builder().s3Uri(statisticsResource.getS3Uri()).build();
+ }
+
+
+ static DataQualityJobInput translate(final software.amazon.sagemaker.dataqualityjobdefinition.DataQualityJobInput jobInput) {
+ return jobInput == null ? null : DataQualityJobInput.builder()
+ .endpointInput(translate(jobInput.getEndpointInput()))
+ .build();
+ }
+ static EndpointInput translate(final software.amazon.sagemaker.dataqualityjobdefinition.EndpointInput endpointInput) {
+ return endpointInput == null ? null : EndpointInput.builder()
+ .endpointName(endpointInput.getEndpointName())
+ .localPath(endpointInput.getLocalPath())
+ .s3DataDistributionType(endpointInput.getS3DataDistributionType())
+ .s3InputMode(endpointInput.getS3InputMode())
+ .build();
+ }
+ static MonitoringOutputConfig translate(final software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutputConfig outputConfig) {
+ return outputConfig == null? null : MonitoringOutputConfig.builder()
+ .kmsKeyId(outputConfig.getKmsKeyId())
+ .monitoringOutputs(translateOutput(outputConfig.getMonitoringOutputs()))
+ .build();
+ }
+
+ static List translateOutput(final List monitoringOutputs) {
+ return monitoringOutputs == null ? null : monitoringOutputs.stream()
+ .map(monitoringOutput -> translate(monitoringOutput))
+ .collect(Collectors.toList());
+ }
+
+ static MonitoringOutput translate(final software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutput monitoringOutput) {
+ return monitoringOutput == null ? null : MonitoringOutput.builder()
+ .s3Output(translate(monitoringOutput.getS3Output()))
+ .build();
+ }
+
+ static MonitoringS3Output translate(final software.amazon.sagemaker.dataqualityjobdefinition.S3Output s3Output) {
+ return s3Output == null? null : MonitoringS3Output.builder()
+ .localPath(s3Output.getLocalPath())
+ .s3UploadMode(s3Output.getS3UploadMode())
+ .s3Uri(s3Output.getS3Uri())
+ .build();
+ }
+
+ static MonitoringResources translate(final software.amazon.sagemaker.dataqualityjobdefinition.MonitoringResources monitoringResources) {
+ return monitoringResources == null? null : MonitoringResources.builder()
+ .clusterConfig(translate(monitoringResources.getClusterConfig()))
+ .build();
+ }
+
+ static MonitoringClusterConfig translate(final software.amazon.sagemaker.dataqualityjobdefinition.ClusterConfig clusterConfig) {
+ return clusterConfig == null? null : MonitoringClusterConfig.builder()
+ .instanceCount(clusterConfig.getInstanceCount())
+ .instanceType(clusterConfig.getInstanceType())
+ .volumeKmsKeyId(clusterConfig.getVolumeKmsKeyId())
+ .volumeSizeInGB(clusterConfig.getVolumeSizeInGB())
+ .build();
+ }
+
+ static MonitoringNetworkConfig translate(final software.amazon.sagemaker.dataqualityjobdefinition.NetworkConfig networkConfig) {
+ return networkConfig == null? null : MonitoringNetworkConfig.builder()
+ .enableInterContainerTrafficEncryption(networkConfig.getEnableInterContainerTrafficEncryption())
+ .enableNetworkIsolation(networkConfig.getEnableNetworkIsolation())
+ .vpcConfig(translate(networkConfig.getVpcConfig()))
+ .build();
+ }
+
+ static VpcConfig translate(final software.amazon.sagemaker.dataqualityjobdefinition.VpcConfig vpcConfig) {
+ return vpcConfig == null? null : VpcConfig.builder()
+ .securityGroupIds(vpcConfig.getSecurityGroupIds())
+ .subnets(vpcConfig.getSubnets())
+ .build();
+ }
+
+ static MonitoringStoppingCondition translate(final software.amazon.sagemaker.dataqualityjobdefinition.StoppingCondition stoppingCondition) {
+ return stoppingCondition == null? null : MonitoringStoppingCondition.builder()
+ .maxRuntimeInSeconds(stoppingCondition.getMaxRuntimeInSeconds())
+ .build();
+ }
+
+ static Map translateMapOfObjectsToMapOfStrings(final Map mapOfObjects) {
+ return mapOfObjects == null ? null : mapOfObjects.entrySet().stream().collect(
+ Collectors.toMap(Map.Entry::getKey, e -> (String)e.getValue())
+ );
+ }
+
+}
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForResponse.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForResponse.java
new file mode 100644
index 0000000..7a48a0d
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorForResponse.java
@@ -0,0 +1,172 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.model.DataQualityAppSpecification;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityBaselineConfig;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityJobInput;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.EndpointInput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringNetworkConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutputConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringResources;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringS3Output;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStatisticsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition;
+import software.amazon.awssdk.services.sagemaker.model.VpcConfig;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class TranslatorForResponse {
+
+ private TranslatorForResponse() {
+ }
+
+ /**
+ * Translates resource object from sdk into a resource model
+ *
+ * @param awsResponse the aws service describe resource response
+ * @return model resource model
+ */
+ static ResourceModel translateFromReadResponse(final DescribeDataQualityJobDefinitionResponse awsResponse) {
+ return ResourceModel.builder()
+ .jobDefinitionArn(awsResponse.jobDefinitionArn())
+ .jobDefinitionName(awsResponse.jobDefinitionName())
+ .creationTime(awsResponse.creationTime().toString())
+ .dataQualityBaselineConfig(translate(awsResponse.dataQualityBaselineConfig()))
+ .dataQualityAppSpecification(translate(awsResponse.dataQualityAppSpecification()))
+ .dataQualityJobInput(translate(awsResponse.dataQualityJobInput()))
+ .dataQualityJobOutputConfig(translate(awsResponse.dataQualityJobOutputConfig()))
+ .jobResources(translate(awsResponse.jobResources()))
+ .networkConfig(translate(awsResponse.networkConfig()))
+ .roleArn(awsResponse.roleArn())
+ .stoppingCondition(translate(awsResponse.stoppingCondition()))
+ .build();
+ }
+
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig translate(
+ final DataQualityBaselineConfig baselineConfig) {
+ return baselineConfig == null? null : software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig.builder()
+ .baseliningJobName(baselineConfig.baseliningJobName())
+ .constraintsResource(translate(baselineConfig.constraintsResource()))
+ .statisticsResource(translate(baselineConfig.statisticsResource()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.ConstraintsResource translate(
+ final MonitoringConstraintsResource constraintsResource) {
+ return constraintsResource == null? null : software.amazon.sagemaker.dataqualityjobdefinition.ConstraintsResource.builder()
+ .s3Uri(constraintsResource.s3Uri())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.StatisticsResource translate(
+ final MonitoringStatisticsResource statisticsResource) {
+ return statisticsResource == null? null : software.amazon.sagemaker.dataqualityjobdefinition.StatisticsResource.builder()
+ .s3Uri(statisticsResource.s3Uri())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.DataQualityAppSpecification translate(
+ final DataQualityAppSpecification monitoringAppSpec) {
+ return monitoringAppSpec == null ? null : software.amazon.sagemaker.dataqualityjobdefinition.DataQualityAppSpecification.builder()
+ .containerArguments(monitoringAppSpec.containerArguments())
+ .containerEntrypoint(monitoringAppSpec.containerEntrypoint())
+ .imageUri(monitoringAppSpec.imageUri())
+ .postAnalyticsProcessorSourceUri(monitoringAppSpec.postAnalyticsProcessorSourceUri())
+ .recordPreprocessorSourceUri(monitoringAppSpec.recordPreprocessorSourceUri())
+ .environment(translateMapOfStringsMapOfObjects(monitoringAppSpec.environment()))
+ .build();
+ }
+
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.DataQualityJobInput translate(final DataQualityJobInput monitoringInput) {
+ return monitoringInput == null ? null : software.amazon.sagemaker.dataqualityjobdefinition.DataQualityJobInput.builder()
+ .endpointInput(translate(monitoringInput.endpointInput()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.EndpointInput translate(final EndpointInput endpointInput) {
+ return endpointInput == null ? null : software.amazon.sagemaker.dataqualityjobdefinition.EndpointInput.builder()
+ .endpointName(endpointInput.endpointName())
+ .localPath(endpointInput.localPath())
+ .s3DataDistributionType(endpointInput.s3DataDistributionType().toString())
+ .s3InputMode(endpointInput.s3InputMode().toString())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutputConfig translate(final MonitoringOutputConfig outputConfig) {
+ return outputConfig == null? null : software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutputConfig.builder()
+ .kmsKeyId(outputConfig.kmsKeyId())
+ .monitoringOutputs(translateOutput(outputConfig.monitoringOutputs()))
+ .build();
+ }
+
+ static List translateOutput(final List monitoringOutputs) {
+ return monitoringOutputs == null ? null : monitoringOutputs.stream()
+ .map(monitoringOutput -> translate(monitoringOutput))
+ .collect(Collectors.toList());
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutput translate(final MonitoringOutput monitoringOutput) {
+ return monitoringOutput == null ? null : software.amazon.sagemaker.dataqualityjobdefinition.MonitoringOutput.builder()
+ .s3Output(translate(monitoringOutput.s3Output()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.S3Output translate(final MonitoringS3Output s3Output) {
+ return s3Output == null? null : software.amazon.sagemaker.dataqualityjobdefinition.S3Output.builder()
+ .localPath(s3Output.localPath())
+ .s3UploadMode(s3Output.s3UploadMode().toString())
+ .s3Uri(s3Output.s3Uri())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.MonitoringResources translate(final MonitoringResources monitoringResources) {
+ return monitoringResources == null? null : software.amazon.sagemaker.dataqualityjobdefinition.MonitoringResources.builder()
+ .clusterConfig(translate(monitoringResources.clusterConfig()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.ClusterConfig translate(final MonitoringClusterConfig clusterConfig) {
+ return clusterConfig == null? null : software.amazon.sagemaker.dataqualityjobdefinition.ClusterConfig.builder()
+ .instanceCount(clusterConfig.instanceCount())
+ .instanceType(clusterConfig.instanceType().toString())
+ .volumeKmsKeyId(clusterConfig.volumeKmsKeyId())
+ .volumeSizeInGB(clusterConfig.volumeSizeInGB())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.NetworkConfig translate(final MonitoringNetworkConfig networkConfig) {
+ return networkConfig == null? null : software.amazon.sagemaker.dataqualityjobdefinition.NetworkConfig.builder()
+ .enableInterContainerTrafficEncryption(networkConfig.enableInterContainerTrafficEncryption())
+ .enableNetworkIsolation(networkConfig.enableNetworkIsolation())
+ .vpcConfig(translate(networkConfig.vpcConfig()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.VpcConfig translate(final VpcConfig vpcConfig) {
+ return vpcConfig == null? null : software.amazon.sagemaker.dataqualityjobdefinition.VpcConfig.builder()
+ .securityGroupIds(vpcConfig.securityGroupIds())
+ .subnets(vpcConfig.subnets())
+ .build();
+ }
+
+ static software.amazon.sagemaker.dataqualityjobdefinition.StoppingCondition translate(final MonitoringStoppingCondition stoppingCondition) {
+ return stoppingCondition == null? null : software.amazon.sagemaker.dataqualityjobdefinition.StoppingCondition.builder()
+ .maxRuntimeInSeconds(stoppingCondition.maxRuntimeInSeconds())
+ .build();
+ }
+
+ static Map translateMapOfStringsMapOfObjects(final Map mapOfStrings) {
+ return mapOfStrings == null ? null : mapOfStrings.entrySet().stream().collect(
+ Collectors.toMap(Map.Entry::getKey, e -> (Object)e.getValue())
+ );
+ }
+
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/UpdateHandler.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/UpdateHandler.java
new file mode 100644
index 0000000..556ed48
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/UpdateHandler.java
@@ -0,0 +1,27 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class UpdateHandler extends BaseHandler {
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+
+ logger.log(String.format("%s [%s] calling dummy update handler",
+ ResourceModel.TYPE_NAME, request.getDesiredResourceState()));
+
+ return ProgressEvent.builder()
+ .resourceModel(request.getDesiredResourceState())
+ .status(OperationStatus.SUCCESS)
+ .build();
+
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Utils.java b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Utils.java
new file mode 100644
index 0000000..880d7a5
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/main/java/software/amazon/sagemaker/dataqualityjobdefinition/Utils.java
@@ -0,0 +1,25 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+
+import org.apache.commons.lang3.StringUtils;
+
+public class Utils {
+
+ /**
+ * Get resource name from ARN.
+ *
+ * Since some resources use the physical id as the full arn, we need
+ * a way to go from that to the resource name; since we use just the name
+ * for all our api calls.
+ * @param resourceArn String representation of the Resource's ARN.
+ * @param substring The substring to partition on, that is followed
+ * by the resource name.
+ * @return The name portion of the ARN. Specifically the part that
+ * follows the first substring
+ */
+ public static String getResourceNameFromArn(final String resourceArn,
+ final String substring) {
+ return StringUtils.substringAfter(resourceArn, substring);
+ }
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/AbstractTestBase.java b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/AbstractTestBase.java
new file mode 100644
index 0000000..d570b1b
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/AbstractTestBase.java
@@ -0,0 +1,76 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.awssdk.awscore.AwsResponse;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.pagination.sync.SdkIterable;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.ProxyClient;
+
+import java.time.Instant;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+public class AbstractTestBase {
+ protected static final String TEST_ENDPOINT_NAME = "testEndpointName";
+ protected static final String TEST_ENDPOINT_LOCAL_PATH = "/opt/ml/processing/endpointdata";
+ protected static final String TEST_IMAGE_URI = "012345678912.dkr.ecr.us-west-2.amazonaws.com/montecarloanalysiscontainer:latest";
+ protected static final String TEST_ARN = "sampleArn";
+ protected static final Instant TEST_TIME = Instant.now();
+ protected static final String TEST_JOB_DEFINITION_ARN = "arn:aws:sagemaker:us-west-2:1234567890:data-quality-job-definition/testJobDefinitionName";
+ protected static final String TEST_JOB_DEFINITION_NAME = "testJobDefinitionName";
+ protected static final String TEST_ERROR_MESSAGE = "test error message";
+ protected static final Credentials MOCK_CREDENTIALS;
+ protected static final LoggerProxy logger;
+
+ static {
+ MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token");
+ logger = new LoggerProxy();
+ }
+ static ProxyClient MOCK_PROXY(
+ final AmazonWebServicesClientProxy proxy,
+ final SageMakerClient sagemakerClient) {
+ return new ProxyClient() {
+ @Override
+ public ResponseT
+ injectCredentialsAndInvokeV2(RequestT request, Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
+ }
+
+ @Override
+ public
+ CompletableFuture
+ injectCredentialsAndInvokeV2Async(RequestT request, Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public >
+ IterableT
+ injectCredentialsAndInvokeIterableV2(RequestT request, Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction);
+ }
+
+ @Override
+ public ResponseInputStream
+ injectCredentialsAndInvokeV2InputStream(RequestT requestT, Function> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResponseBytes
+ injectCredentialsAndInvokeV2Bytes(RequestT requestT, Function> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SageMakerClient client() {
+ return sagemakerClient;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandlerTest.java b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandlerTest.java
new file mode 100644
index 0000000..bd81519
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/CreateHandlerTest.java
@@ -0,0 +1,240 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.CreateDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.CreateDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceLimitExceededException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class CreateHandlerTest extends software.amazon.sagemaker.dataqualityjobdefinition.AbstractTestBase {
+
+ private final ResourceModel requestModel = ResourceModel.builder()
+ .creationTime(TEST_TIME.toString())
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .build();
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testCreateHandler_SimpleSuccess() {
+ final DescribeDataQualityJobDefinitionResponse describeDataQualityJobDefinitionResponse =
+ DescribeDataQualityJobDefinitionResponse.builder()
+ .creationTime(TEST_TIME)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ final CreateDataQualityJobDefinitionResponse createDataQualityJobDefinitionResponse = CreateDataQualityJobDefinitionResponse.builder()
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenReturn(describeDataQualityJobDefinitionResponse);
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenReturn(createDataQualityJobDefinitionResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .creationTime(TEST_TIME.toString())
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testCreateHandler_NoJobDefinitionName_Success() {
+ final DescribeDataQualityJobDefinitionResponse describeDataQualityJobDefinitionResponse =
+ DescribeDataQualityJobDefinitionResponse.builder()
+ .creationTime(TEST_TIME)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ final CreateDataQualityJobDefinitionResponse createDataQualityJobDefinitionResponse = CreateDataQualityJobDefinitionResponse.builder()
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenReturn(describeDataQualityJobDefinitionResponse);
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenReturn(createDataQualityJobDefinitionResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .clientRequestToken("token")
+ .logicalResourceIdentifier("logical_id")
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .creationTime(TEST_TIME.toString())
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testCreateHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(500)
+ .build();
+
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.CREATE));
+ }
+
+ @Test
+ public void testCreateHandler_DataQualityJobDefinitionAlreadyExists_Fails() {
+ final ResourceInUseException resourceInUseException = ResourceInUseException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenThrow(resourceInUseException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows( ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.AlreadyExists.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_JOB_DEFINITION_NAME));
+ }
+
+ @Test
+ public void testCreateHandler_ResourceLimitExceededException() {
+ final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenThrow(resourceLimitExceededException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.CREATE));
+ }
+
+ @Test
+ public void testCreateHandler_ValidationFailure() {
+ final AwsServiceException validationFailureException = SageMakerException.builder()
+ .message("1 validation error detected: Value null at 'jobDefinitionName' " +
+ "failed to satisfy constraint: Member must not be null")
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenThrow(validationFailureException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows( CfnInvalidRequestException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.InvalidRequest.getMessage(),
+ Action.CREATE));
+ }
+
+ @Test
+ public void testCreateHandler_NoExceptionMessage() {
+ final AwsServiceException someException = SageMakerException.builder()
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createDataQualityJobDefinition(any(CreateDataQualityJobDefinitionRequest.class)))
+ .thenThrow(someException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.CREATE));
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final CreateHandler handler = new CreateHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandlerTest.java b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandlerTest.java
new file mode 100644
index 0000000..b706a0b
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/DeleteHandlerTest.java
@@ -0,0 +1,155 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DeleteDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DeleteDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteHandlerTest extends software.amazon.sagemaker.dataqualityjobdefinition.AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testDeleteHandler_SimpleSuccess() {
+ final DeleteDataQualityJobDefinitionResponse deleteDataQualityJobDefinitionResponse = DeleteDataQualityJobDefinitionResponse.builder()
+ .build();
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ when(proxyClient.client().deleteDataQualityJobDefinition(any(DeleteDataQualityJobDefinitionRequest.class)))
+ .thenReturn(deleteDataQualityJobDefinitionResponse);
+
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo((OperationStatus.SUCCESS));
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ assertThat(response.getResourceModel()).isNull();
+ }
+
+ @Test
+ public void testDeleteHandler_WithoutJobDefinitionName_Success() {
+ final DeleteDataQualityJobDefinitionResponse deleteDataQualityJobDefinitionResponse = DeleteDataQualityJobDefinitionResponse.builder()
+ .build();
+
+ ResourceModel resourceModel = ResourceModel.builder()
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(resourceModel)
+ .build();
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(
+ DeleteDataQualityJobDefinitionRequest.class);
+
+
+ when(proxyClient.client().deleteDataQualityJobDefinition(any(DeleteDataQualityJobDefinitionRequest.class)))
+ .thenReturn(deleteDataQualityJobDefinitionResponse);
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ verify(proxyClient.client(), times(1)).deleteDataQualityJobDefinition(requestCaptor.capture());
+ assertEquals(TEST_JOB_DEFINITION_NAME, requestCaptor.getValue().jobDefinitionName());
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo((OperationStatus.SUCCESS));
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ assertThat(response.getResourceModel()).isNull();
+ }
+
+ @Test
+ public void testDeleteHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(500)
+ .build();
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ when(proxyClient.client().deleteDataQualityJobDefinition(any(DeleteDataQualityJobDefinitionRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.DELETE));
+ }
+
+ @Test
+ public void testDeleteHandler_DataQualityJobDefinitionDoesNotExists_Fails() {
+ when(proxyClient.client().deleteDataQualityJobDefinition(any(DeleteDataQualityJobDefinitionRequest.class)))
+ .thenThrow(ResourceNotFoundException.class);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_JOB_DEFINITION_NAME));
+ }
+
+
+ private ResourceModel getRequestResourceModel() {
+ return ResourceModel.builder()
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .build();
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final DeleteHandler handler = new DeleteHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+}
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandlerTest.java b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandlerTest.java
new file mode 100644
index 0000000..3eb2c22
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/ReadHandlerTest.java
@@ -0,0 +1,225 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityAppSpecification;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityBaselineConfig;
+import software.amazon.awssdk.services.sagemaker.model.DataQualityJobInput;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeDataQualityJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.EndpointInput;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@Slf4j
+@ExtendWith(MockitoExtension.class)
+public class ReadHandlerTest extends software.amazon.sagemaker.dataqualityjobdefinition.AbstractTestBase {
+
+ private static final String TEST_PROCESSING_JOB_NAME = "testProcessingJobName";
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testReadHandler_SimpleSuccess() {
+
+ DataQualityBaselineConfig dataQualityBaselineConfig = DataQualityBaselineConfig.builder()
+ .baseliningJobName(TEST_PROCESSING_JOB_NAME).build();
+
+ final DescribeDataQualityJobDefinitionResponse describeDataQualityJobDefinitionResponse =
+ DescribeDataQualityJobDefinitionResponse.builder()
+ .creationTime(TEST_TIME)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .dataQualityBaselineConfig(dataQualityBaselineConfig)
+ .roleArn(TEST_ARN)
+ .build();
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenReturn(describeDataQualityJobDefinitionResponse);
+
+ software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig resourceBaselineConfig =
+ software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig.builder()
+ .baseliningJobName(TEST_PROCESSING_JOB_NAME).build();
+
+ final ResourceModel expectedResourceModel = ResourceModel.builder()
+ .creationTime(TEST_TIME.toString())
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .dataQualityBaselineConfig(resourceBaselineConfig)
+ .roleArn(TEST_ARN)
+ .build();
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+
+ assertThat(response.getResourceModel()).isEqualTo(expectedResourceModel);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ verify(proxyClient.client()).describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class));
+ }
+
+ @Test
+ public void testReadHandler_WithoutJobDefinitionName_Success() {
+ DataQualityBaselineConfig dataQualityBaselineConfig = DataQualityBaselineConfig.builder()
+ .baseliningJobName(TEST_PROCESSING_JOB_NAME).build();
+
+ final DescribeDataQualityJobDefinitionResponse describeDataQualityJobDefinitionResponse =
+ DescribeDataQualityJobDefinitionResponse.builder()
+ .creationTime(TEST_TIME)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .dataQualityBaselineConfig(dataQualityBaselineConfig)
+ .roleArn(TEST_ARN)
+ .build();
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(
+ DescribeDataQualityJobDefinitionRequest.class);
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenReturn(describeDataQualityJobDefinitionResponse);
+
+ software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig resourceBaselineConfig =
+ software.amazon.sagemaker.dataqualityjobdefinition.DataQualityBaselineConfig.builder()
+ .baseliningJobName(TEST_PROCESSING_JOB_NAME).build();
+
+ final ResourceModel expectedResourceModel = ResourceModel.builder()
+ .creationTime(TEST_TIME.toString())
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .dataQualityBaselineConfig(resourceBaselineConfig)
+ .roleArn(TEST_ARN)
+ .build();
+
+ final ResourceModel resourceModel = ResourceModel.builder()
+ .jobDefinitionArn(TEST_JOB_DEFINITION_ARN)
+ .build();
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(resourceModel)
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ verify(proxyClient.client(), times(1)).describeDataQualityJobDefinition(requestCaptor.capture());
+ assertEquals(TEST_JOB_DEFINITION_NAME, requestCaptor.getValue().jobDefinitionName());
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+
+ assertThat(response.getResourceModel()).isEqualTo(expectedResourceModel);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ verify(proxyClient.client()).describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class));
+ }
+
+ @Test
+ public void testReadHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .message("test error message")
+ .statusCode(500)
+ .build();
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.READ));
+ }
+
+ @Test
+ public void testReadHandler_DataQualityJobDefinitionDoesNotExist_Fails() {
+ final AwsServiceException resourceNotFoundException = AwsServiceException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenThrow(resourceNotFoundException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.READ));
+ }
+
+ @Test
+ public void testReadHandler_ResourceNotFoundException() {
+ when(proxyClient.client().describeDataQualityJobDefinition(any(DescribeDataQualityJobDefinitionRequest.class)))
+ .thenThrow(ResourceNotFoundException.class);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_JOB_DEFINITION_NAME));
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final ReadHandler handler = new ReadHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+
+ private ResourceModel getRequestResourceModel() {
+ return ResourceModel.builder()
+ .jobDefinitionName(TEST_JOB_DEFINITION_NAME)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorTest.java b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorTest.java
new file mode 100644
index 0000000..2e86350
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/src/test/java/software/amazon/sagemaker/dataqualityjobdefinition/TranslatorTest.java
@@ -0,0 +1,145 @@
+package software.amazon.sagemaker.dataqualityjobdefinition;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.exceptions.CfnAccessDeniedException;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
+import software.amazon.cloudformation.exceptions.CfnThrottlingException;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TranslatorTest {
+
+ public static final String TEST_OPERATION = "someOperation";
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_UnauthorizedOperation() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("UnauthorizedOperation").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnAccessDeniedException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.AccessDenied.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_InvalidParameter() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InvalidParameter").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ assertThrows(CfnInvalidRequestException.class, () -> Translator.throwCfnException(TEST_OPERATION, ex));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_InvalidParameterValue() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InvalidParameterValue").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ assertThrows(CfnInvalidRequestException.class, () -> Translator.throwCfnException(TEST_OPERATION, ex));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_ValidationError() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("ValidationError").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ assertThrows(CfnInvalidRequestException.class, () -> Translator.throwCfnException(TEST_OPERATION, ex));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_InternalError() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InternalError").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnServiceInternalErrorException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_ServiceUnavailable() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("ServiceUnavailable").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnServiceInternalErrorException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_ResourceLimitExceeded() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("ResourceLimitExceeded").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ assertThrows(CfnServiceLimitExceededException.class, () -> Translator.throwCfnException(TEST_OPERATION, ex));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_ResourceNotFound() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("ResourceNotFound").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ assertThrows(CfnNotFoundException.class, () -> Translator.throwCfnException(TEST_OPERATION, ex));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_ThrottlingException() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("ThrottlingException").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnThrottlingException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.Throttling.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerErrorCodeForSageMakerErrorCode_UnknownException() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("Unknown").build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerError_NoErrorCode() {
+ AwsErrorDetails errorDetails = AwsErrorDetails.builder().build();
+ AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build();
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ TEST_OPERATION));
+ }
+
+ @Test
+ public void testGetHandlerError_NoErrorDetails() {
+ AwsServiceException ex = SageMakerException.builder().build();
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () ->
+ Translator.throwCfnException(TEST_OPERATION, ex));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ TEST_OPERATION));
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-dataqualityjobdefinition/template.yml b/aws-sagemaker-dataqualityjobdefinition/template.yml
new file mode 100644
index 0000000..651f3f8
--- /dev/null
+++ b/aws-sagemaker-dataqualityjobdefinition/template.yml
@@ -0,0 +1,23 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+Description: AWS SAM template for the AWS::SageMaker::DataQualityJobDefinition resource type
+
+Globals:
+ Function:
+ Timeout: 60 # docker start-up times can be long for SAM CLI
+ MemorySize: 256
+
+Resources:
+ TypeFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.sagemaker.dataqualityjobdefinition.HandlerWrapper::handleRequest
+ Runtime: java8
+ CodeUri: ./target/aws-sagemaker-dataqualityjobdefinition-1.0.jar
+
+ TestEntrypoint:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.sagemaker.dataqualityjobdefinition.HandlerWrapper::testEntrypoint
+ Runtime: java8
+ CodeUri: ./target/aws-sagemaker-dataqualityjobdefinition-1.0.jar
diff --git a/aws-sagemaker-featuregroup/.rpdk-config b/aws-sagemaker-featuregroup/.rpdk-config
new file mode 100644
index 0000000..4524d67
--- /dev/null
+++ b/aws-sagemaker-featuregroup/.rpdk-config
@@ -0,0 +1,17 @@
+{
+ "typeName": "AWS::SageMaker::FeatureGroup",
+ "language": "java",
+ "runtime": "java8",
+ "entrypoint": "software.amazon.sagemaker.featuregroup.HandlerWrapper::handleRequest",
+ "testEntrypoint": "software.amazon.sagemaker.featuregroup.HandlerWrapper::testEntrypoint",
+ "settings": {
+ "namespace": [
+ "software",
+ "amazon",
+ "sagemaker",
+ "featuregroup"
+ ],
+ "codegen_template_path": "default",
+ "protocolVersion": "2.0.0"
+ }
+}
diff --git a/aws-sagemaker-featuregroup/README.md b/aws-sagemaker-featuregroup/README.md
new file mode 100644
index 0000000..bbfcb85
--- /dev/null
+++ b/aws-sagemaker-featuregroup/README.md
@@ -0,0 +1,187 @@
+# AWS::SageMaker::FeatureGroup
+
+Resource Type definition for AWS::SageMaker::FeatureGroup
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::FeatureGroup",
+ "Properties" : {
+ "FeatureGroupName " : String ,
+ "RecordIdentifierFeatureName " : String ,
+ "EventTimeFeatureName " : String ,
+ "FeatureDefinitions " : [ FeatureDefinition , ... ] ,
+ "OnlineStoreConfig " : OnlineStoreConfig ,
+ "OfflineStoreConfig " : OfflineStoreConfig ,
+ "RoleArn " : String ,
+ "Description " : String ,
+ "Tags " : [ Tag , ... ]
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::FeatureGroup
+Properties:
+ FeatureGroupName : String
+ RecordIdentifierFeatureName : String
+ EventTimeFeatureName : String
+ FeatureDefinitions :
+ - FeatureDefinition
+ OnlineStoreConfig : OnlineStoreConfig
+ OfflineStoreConfig : OfflineStoreConfig
+ RoleArn : String
+ Description : String
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### FeatureGroupName
+
+The Name of the FeatureGroup.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RecordIdentifierFeatureName
+
+The Record Identifier Feature Name.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### EventTimeFeatureName
+
+The Event Time Feature Name.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### FeatureDefinitions
+
+An Array of Feature Definition
+
+_Required_: Yes
+
+_Type_: List of FeatureDefinition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### OnlineStoreConfig
+
+_Required_: No
+
+_Type_: OnlineStoreConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### OfflineStoreConfig
+
+_Required_: No
+
+_Type_: OfflineStoreConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+Role Arn
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Description
+
+Description about the FeatureGroup.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 128
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pair to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the FeatureGroupName.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+Returns the CreationTime
value.
+
+#### FeatureGroupStatus
+
+Returns the FeatureGroupStatus
value.
+
+#### FailureReason
+
+Returns the FailureReason
value.
+
+#### OfflineStoreStatus
+
+Returns the OfflineStoreStatus
value.
+
diff --git a/aws-sagemaker-featuregroup/aws-sagemaker-featuregroup.json b/aws-sagemaker-featuregroup/aws-sagemaker-featuregroup.json
new file mode 100644
index 0000000..8170a10
--- /dev/null
+++ b/aws-sagemaker-featuregroup/aws-sagemaker-featuregroup.json
@@ -0,0 +1,227 @@
+{
+ "typeName": "AWS::SageMaker::FeatureGroup",
+ "description": "Resource Type definition for AWS::SageMaker::FeatureGroup",
+ "additionalProperties": false,
+ "properties": {
+ "FeatureGroupName": {
+ "type": "string",
+ "description": "The Name of the FeatureGroup.",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}"
+ },
+ "RecordIdentifierFeatureName": {
+ "type": "string",
+ "description": "The Record Identifier Feature Name.",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}"
+ },
+ "EventTimeFeatureName": {
+ "type": "string",
+ "description": "The Event Time Feature Name.",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}"
+ },
+ "FeatureDefinitions": {
+ "type": "array",
+ "description": "An Array of Feature Definition",
+ "uniqueItems": false,
+ "minItems": 1,
+ "maxItems": 2500,
+ "items": {
+ "$ref": "#/definitions/FeatureDefinition"
+ }
+ },
+ "OnlineStoreConfig": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "SecurityConfig": {
+ "$ref": "#/definitions/OnlineStoreSecurityConfig"
+ },
+ "EnableOnlineStore": {
+ "type": "boolean"
+ }
+ }
+ },
+ "OfflineStoreConfig": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "S3StorageConfig": {
+ "$ref": "#/definitions/S3StorageConfig"
+ },
+ "DisableGlueTableCreation": {
+ "type": "boolean"
+ },
+ "DataCatalogConfig": {
+ "$ref": "#/definitions/DataCatalogConfig"
+ }
+ },
+ "required": ["S3StorageConfig"]
+ },
+ "RoleArn": {
+ "type": "string",
+ "description": "Role Arn",
+ "minLength": 20,
+ "maxLength": 2048,
+ "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$"
+ },
+ "Description": {
+ "type": "string",
+ "description": "Description about the FeatureGroup.",
+ "maxLength": 128
+ },
+ "Tags": {
+ "type": "array",
+ "description": "An array of key-value pair to apply to this resource.",
+ "uniqueItems": false,
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/definitions/Tag"
+ }
+ }
+ },
+ "definitions": {
+ "FeatureDefinition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "FeatureName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}"
+ },
+ "FeatureType": {
+ "type": "string",
+ "enum": [
+ "Integral",
+ "Fractional",
+ "String"
+ ]
+ }
+ },
+ "required": ["FeatureName", "FeatureType"]
+ },
+ "KmsKeyId": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "OnlineStoreSecurityConfig": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "KmsKeyId": {
+ "$ref": "#/definitions/KmsKeyId"
+ }
+ }
+ },
+ "S3StorageConfig": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "S3Uri": {
+ "type": "string",
+ "maxLength": 1024,
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$"
+ },
+ "KmsKeyId": {
+ "$ref": "#/definitions/KmsKeyId"
+ }
+ },
+ "required": ["S3Uri"]
+ },
+ "DataCatalogConfig": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "TableName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "pattern": "[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\t]*"
+ },
+ "Catalog": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "pattern": "[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\t]*"
+ },
+ "Database": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "pattern": "[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\t]*"
+ }
+ },
+ "required": ["TableName", "Catalog", "Database"]
+ },
+ "Tag": {
+ "type": "object",
+ "description" : "A key-value pair to associate with a resource.",
+ "additionalProperties": false,
+ "properties": {
+ "Value": {
+ "type": "string"
+ },
+ "Key": {
+ "type": "string"
+ }
+ },
+ "required": ["Value", "Key"]
+ }
+ },
+ "required": ["FeatureGroupName", "RecordIdentifierFeatureName", "EventTimeFeatureName", "FeatureDefinitions"],
+ "createOnlyProperties": [
+ "/properties/FeatureGroupName",
+ "/properties/RecordIdentifierFeatureName",
+ "/properties/EventTimeFeatureName",
+ "/properties/FeatureDefinitions",
+ "/properties/OnlineStoreConfig",
+ "/properties/OfflineStoreConfig",
+ "/properties/RoleArn",
+ "/properties/Description",
+ "/properties/Tags"
+ ],
+ "primaryIdentifier": ["/properties/FeatureGroupName"],
+ "readOnlyProperties": [
+ "/properties/CreationTime",
+ "/properties/FeatureGroupStatus",
+ "/properties/FailureReason",
+ "/properties/OfflineStoreStatus"
+ ],
+ "handlers": {
+ "create": {
+ "permissions": [
+ "iam:PassRole",
+ "kms:CreateGrant",
+ "kms:DescribeKey",
+ "glue:CreateTable",
+ "glue:GetTable",
+ "glue:CreateDatabase",
+ "glue:GetDatabase",
+ "sagemaker:CreateFeatureGroup",
+ "sagemaker:DescribeFeatureGroup"
+ ]
+ },
+ "read": {
+ "permissions": [
+ "sagemaker:DescribeFeatureGroup"
+ ]
+ },
+ "delete": {
+ "permissions": [
+ "sagemaker:DeleteFeatureGroup",
+ "sagemaker:DescribeFeatureGroup"
+ ]
+ },
+ "list": {
+ "permissions": [
+ "sagemaker:ListFeatureGroups"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/docs/README.md b/aws-sagemaker-featuregroup/docs/README.md
new file mode 100644
index 0000000..bbfcb85
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/README.md
@@ -0,0 +1,187 @@
+# AWS::SageMaker::FeatureGroup
+
+Resource Type definition for AWS::SageMaker::FeatureGroup
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::FeatureGroup",
+ "Properties" : {
+ "FeatureGroupName " : String ,
+ "RecordIdentifierFeatureName " : String ,
+ "EventTimeFeatureName " : String ,
+ "FeatureDefinitions " : [ FeatureDefinition , ... ] ,
+ "OnlineStoreConfig " : OnlineStoreConfig ,
+ "OfflineStoreConfig " : OfflineStoreConfig ,
+ "RoleArn " : String ,
+ "Description " : String ,
+ "Tags " : [ Tag , ... ]
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::FeatureGroup
+Properties:
+ FeatureGroupName : String
+ RecordIdentifierFeatureName : String
+ EventTimeFeatureName : String
+ FeatureDefinitions :
+ - FeatureDefinition
+ OnlineStoreConfig : OnlineStoreConfig
+ OfflineStoreConfig : OfflineStoreConfig
+ RoleArn : String
+ Description : String
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### FeatureGroupName
+
+The Name of the FeatureGroup.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RecordIdentifierFeatureName
+
+The Record Identifier Feature Name.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### EventTimeFeatureName
+
+The Event Time Feature Name.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### FeatureDefinitions
+
+An Array of Feature Definition
+
+_Required_: Yes
+
+_Type_: List of FeatureDefinition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### OnlineStoreConfig
+
+_Required_: No
+
+_Type_: OnlineStoreConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### OfflineStoreConfig
+
+_Required_: No
+
+_Type_: OfflineStoreConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+Role Arn
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Description
+
+Description about the FeatureGroup.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 128
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pair to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the FeatureGroupName.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+Returns the CreationTime
value.
+
+#### FeatureGroupStatus
+
+Returns the FeatureGroupStatus
value.
+
+#### FailureReason
+
+Returns the FailureReason
value.
+
+#### OfflineStoreStatus
+
+Returns the OfflineStoreStatus
value.
+
diff --git a/aws-sagemaker-featuregroup/docs/datacatalogconfig.md b/aws-sagemaker-featuregroup/docs/datacatalogconfig.md
new file mode 100644
index 0000000..9ead2e4
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/datacatalogconfig.md
@@ -0,0 +1,68 @@
+# AWS::SageMaker::FeatureGroup DataCatalogConfig
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "TableName " : String ,
+ "Catalog " : String ,
+ "Database " : String
+}
+
+
+### YAML
+
+
+TableName : String
+Catalog : String
+Database : String
+
+
+## Properties
+
+#### TableName
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 255
+
+_Pattern_: [\u0020-\uD7FF\uE000-\uFFFD\uD800\uDC00-\uDBFF\uDFFF ]*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Catalog
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 255
+
+_Pattern_: [\u0020-\uD7FF\uE000-\uFFFD\uD800\uDC00-\uDBFF\uDFFF ]*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Database
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 255
+
+_Pattern_: [\u0020-\uD7FF\uE000-\uFFFD\uD800\uDC00-\uDBFF\uDFFF ]*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/featuredefinition.md b/aws-sagemaker-featuregroup/docs/featuredefinition.md
new file mode 100644
index 0000000..c0c29a2
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/featuredefinition.md
@@ -0,0 +1,48 @@
+# AWS::SageMaker::FeatureGroup FeatureDefinition
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "FeatureName " : String ,
+ "FeatureType " : String
+}
+
+
+### YAML
+
+
+FeatureName : String
+FeatureType : String
+
+
+## Properties
+
+#### FeatureName
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 64
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,63}
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### FeatureType
+
+_Required_: Yes
+
+_Type_: String
+
+_Allowed Values_: Integral
| Fractional
| String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/offlinestoreconfig.md b/aws-sagemaker-featuregroup/docs/offlinestoreconfig.md
new file mode 100644
index 0000000..39c7054
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/offlinestoreconfig.md
@@ -0,0 +1,50 @@
+# AWS::SageMaker::FeatureGroup OfflineStoreConfig
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3StorageConfig " : S3StorageConfig ,
+ "DisableGlueTableCreation " : Boolean ,
+ "DataCatalogConfig " : DataCatalogConfig
+}
+
+
+### YAML
+
+
+S3StorageConfig : S3StorageConfig
+DisableGlueTableCreation : Boolean
+DataCatalogConfig : DataCatalogConfig
+
+
+## Properties
+
+#### S3StorageConfig
+
+_Required_: Yes
+
+_Type_: S3StorageConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### DisableGlueTableCreation
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### DataCatalogConfig
+
+_Required_: No
+
+_Type_: DataCatalogConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/onlinestoreconfig.md b/aws-sagemaker-featuregroup/docs/onlinestoreconfig.md
new file mode 100644
index 0000000..dbbad06
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/onlinestoreconfig.md
@@ -0,0 +1,40 @@
+# AWS::SageMaker::FeatureGroup OnlineStoreConfig
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "SecurityConfig " : OnlineStoreSecurityConfig ,
+ "EnableOnlineStore " : Boolean
+}
+
+
+### YAML
+
+
+SecurityConfig : OnlineStoreSecurityConfig
+EnableOnlineStore : Boolean
+
+
+## Properties
+
+#### SecurityConfig
+
+_Required_: No
+
+_Type_: OnlineStoreSecurityConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### EnableOnlineStore
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/onlinestoresecurityconfig.md b/aws-sagemaker-featuregroup/docs/onlinestoresecurityconfig.md
new file mode 100644
index 0000000..2f48ea6
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/onlinestoresecurityconfig.md
@@ -0,0 +1,32 @@
+# AWS::SageMaker::FeatureGroup OnlineStoreSecurityConfig
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "KmsKeyId " : String
+}
+
+
+### YAML
+
+
+KmsKeyId : String
+
+
+## Properties
+
+#### KmsKeyId
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 2048
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/s3storageconfig.md b/aws-sagemaker-featuregroup/docs/s3storageconfig.md
new file mode 100644
index 0000000..0dc62e1
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/s3storageconfig.md
@@ -0,0 +1,46 @@
+# AWS::SageMaker::FeatureGroup S3StorageConfig
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Uri " : String ,
+ "KmsKeyId " : String
+}
+
+
+### YAML
+
+
+S3Uri : String
+KmsKeyId : String
+
+
+## Properties
+
+#### S3Uri
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### KmsKeyId
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 2048
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/docs/tag.md b/aws-sagemaker-featuregroup/docs/tag.md
new file mode 100644
index 0000000..6043b6a
--- /dev/null
+++ b/aws-sagemaker-featuregroup/docs/tag.md
@@ -0,0 +1,42 @@
+# AWS::SageMaker::FeatureGroup Tag
+
+A key-value pair to associate with a resource.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Value " : String ,
+ "Key " : String
+}
+
+
+### YAML
+
+
+Value : String
+Key : String
+
+
+## Properties
+
+#### Value
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Key
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-featuregroup/lombok.config b/aws-sagemaker-featuregroup/lombok.config
new file mode 100644
index 0000000..7a21e88
--- /dev/null
+++ b/aws-sagemaker-featuregroup/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/aws-sagemaker-featuregroup/pom.xml b/aws-sagemaker-featuregroup/pom.xml
new file mode 100644
index 0000000..594b4d9
--- /dev/null
+++ b/aws-sagemaker-featuregroup/pom.xml
@@ -0,0 +1,210 @@
+
+
+ 4.0.0
+
+ software.amazon.sagemaker.featuregroup
+ aws-sagemaker-featuregroup-handler
+ aws-sagemaker-featuregroup-handler
+ 1.0-SNAPSHOT
+ jar
+
+
+ 1.8
+ 1.8
+ UTF-8
+ UTF-8
+
+
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ [2.0.0,3.0.0)
+
+
+
+ software.amazon.awssdk
+ sagemaker
+ 2.15.41
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.4
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ 3.12.2
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.5.0-M1
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 2.26.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 2.26.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ -Xlint:all,-options,-processing
+ -Werror
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.3
+
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+ generate
+ generate-sources
+
+ exec
+
+
+ cfn
+ generate
+ ${project.basedir}
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.basedir}/target/generated-sources/rpdk
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.4
+
+
+ maven-surefire-plugin
+ 3.0.0-M3
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+
+ **/BaseConfiguration*
+ **/BaseHandler*
+ **/HandlerWrapper*
+ **/ResourceModel*
+
+
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+ jacoco-check
+
+ check
+
+
+
+
+ PACKAGE
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.5
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.5
+
+
+
+
+
+
+
+
+
+
+
+ ${project.basedir}
+
+ aws-sagemaker-featuregroup.json
+
+
+
+
+
diff --git a/aws-sagemaker-featuregroup/resource-role.yaml b/aws-sagemaker-featuregroup/resource-role.yaml
new file mode 100644
index 0000000..f66a215
--- /dev/null
+++ b/aws-sagemaker-featuregroup/resource-role.yaml
@@ -0,0 +1,40 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+ This CloudFormation template creates a role assumed by CloudFormation
+ during CRUDL operations to mutate resources on behalf of the customer.
+
+Resources:
+ ExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ MaxSessionDuration: 8400
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: resources.cloudformation.amazonaws.com
+ Action: sts:AssumeRole
+ Path: "/"
+ Policies:
+ - PolicyName: ResourceTypePolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - "glue:CreateDatabase"
+ - "glue:CreateTable"
+ - "glue:GetDatabase"
+ - "glue:GetTable"
+ - "iam:PassRole"
+ - "kms:CreateGrant"
+ - "sagemaker:CreateFeatureGroup"
+ - "sagemaker:DeleteFeatureGroup"
+ - "sagemaker:DescribeFeatureGroup"
+ - "sagemaker:ListFeatureGroups"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/BaseHandlerStd.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/BaseHandlerStd.java
new file mode 100644
index 0000000..8120494
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/BaseHandlerStd.java
@@ -0,0 +1,36 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+/**
+ * Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers
+ */
+public abstract class BaseHandlerStd extends BaseHandler {
+
+ @Override
+ public final ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+ return handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ proxy.newProxy(ClientBuilder::getClient),
+ logger
+ );
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger);
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CallbackContext.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CallbackContext.java
new file mode 100644
index 0000000..f0cb831
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CallbackContext.java
@@ -0,0 +1,10 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.cloudformation.proxy.StdCallbackContext;
+
+@lombok.Getter
+@lombok.Setter
+@lombok.ToString
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext {
+}
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ClientBuilder.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ClientBuilder.java
new file mode 100644
index 0000000..18e7b5c
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ClientBuilder.java
@@ -0,0 +1,12 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+
+/**
+ * Provides APIs to build service client.
+ */
+public class ClientBuilder {
+ public static SageMakerClient getClient() {
+ return SageMakerClient.builder().build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Configuration.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Configuration.java
new file mode 100644
index 0000000..c7b44a8
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Configuration.java
@@ -0,0 +1,7 @@
+package software.amazon.sagemaker.featuregroup;
+
+class Configuration extends BaseConfiguration {
+ public Configuration() {
+ super("aws-sagemaker-featuregroup.json");
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CreateHandler.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CreateHandler.java
new file mode 100644
index 0000000..ee6ad6c
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/CreateHandler.java
@@ -0,0 +1,110 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.CreateFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.CreateFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class CreateHandler extends BaseHandlerStd {
+
+ private static final String OPERATION = "AWS-SageMaker-FeatureGroup::Create";
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate(OPERATION, proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToCreateRequest)
+ .makeServiceCall(this::createResource)
+ .stabilize(this::stabilizedOnCreate)
+ .progress())
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+
+ /**
+ * Client invocation of the create request through the proxyClient, which is already initialised with
+ * caller credentials, region and retry settings
+ * @param awsRequest the aws service request to create a resource
+ * @param proxyClient the aws service client to make the call
+ * @return awsResponse create resource response
+ */
+ private CreateFeatureGroupResponse createResource(
+ final CreateFeatureGroupRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ CreateFeatureGroupResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createFeatureGroup);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.CREATE.toString(), ResourceModel.TYPE_NAME,
+ awsRequest.featureGroupName(), e);
+ }
+ return response;
+ }
+
+ /**
+ * This is used to ensure FeatureGroup resource has moved from Creating to Created/Failed state.
+ * @param awsRequest the aws service request to create a resource
+ * @param awsResponse the aws service response to create a resource
+ * @param proxyClient the aws service client to make the call
+ * @param model Resource Model
+ * @param callbackContext call back context
+ * @return boolean flag indicate if the creation is stabilized
+ */
+ private boolean stabilizedOnCreate(
+ final CreateFeatureGroupRequest awsRequest,
+ final CreateFeatureGroupResponse awsResponse,
+ final ProxyClient proxyClient,
+ final ResourceModel model,
+ final CallbackContext callbackContext) {
+
+ if (model.getFeatureGroupName() == null) {
+ model.setFeatureGroupName(awsRequest.featureGroupName());
+ }
+
+ final FeatureGroupStatus featureGroupStatus;
+ try {
+ featureGroupStatus = proxyClient.injectCredentialsAndInvokeV2(
+ TranslatorForRequest.translateToReadRequest(model),
+ proxyClient.client()::describeFeatureGroup).featureGroupStatus();
+ } catch (ResourceNotFoundException rnfe) {
+ logger.log(String.format("Resource not found for %s, stabilizing.", model.getPrimaryIdentifier()));
+ return false;
+ }
+
+ switch (featureGroupStatus) {
+ case CREATED:
+ logger.log(String.format("%s [%s] has been stabilized with status %s.", ResourceModel.TYPE_NAME,
+ model.getPrimaryIdentifier(), featureGroupStatus));
+ return true;
+ case CREATING:
+ logger.log(String.format("%s [%s] is stabilizing.", ResourceModel.TYPE_NAME,
+ model.getPrimaryIdentifier()));
+ return false;
+ default:
+ logger.log(String.format("%s [%s] failed to stabilize with status: %s.", ResourceModel.TYPE_NAME,
+ model.getPrimaryIdentifier(), featureGroupStatus));
+ throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getFeatureGroupName());
+ }
+ }
+}
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/DeleteHandler.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/DeleteHandler.java
new file mode 100644
index 0000000..4a7bbbb
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/DeleteHandler.java
@@ -0,0 +1,103 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DeleteFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DeleteFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class DeleteHandler extends BaseHandlerStd {
+
+ private static final String OPERATION = "AWS-SageMaker-FeatureGroup::Delete";
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate(OPERATION, proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToDeleteRequest)
+ .makeServiceCall(this::deleteResource)
+ .stabilize(this::stabilizedOnDelete)
+ .done(awsResponse -> ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .build()));
+ }
+
+ /**
+ * Implement client invocation of the delete request through the proxyClient.
+ * @param awsRequest the aws service request to delete a resource
+ * @param proxyClient the aws service client to make the call
+ * @return delete resource response
+ */
+ private DeleteFeatureGroupResponse deleteResource(
+ final DeleteFeatureGroupRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ DeleteFeatureGroupResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteFeatureGroup);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.DELETE.toString(), ResourceModel.TYPE_NAME,
+ awsRequest.featureGroupName(), e);
+ }
+
+ return response;
+ }
+
+ /**
+ * Stabilization is required to ensure FeatureGroup resource deletion has been completed.
+ * @param awsRequest the aws service request to delete a resource
+ * @param awsResponse the aws service response to delete a resource
+ * @param proxyClient the aws service client to make the call
+ * @param model resource model
+ * @param callbackContext callback context
+ * @return boolean state of stabilized or not
+ */
+ private boolean stabilizedOnDelete(
+ final DeleteFeatureGroupRequest awsRequest,
+ final DeleteFeatureGroupResponse awsResponse,
+ final ProxyClient proxyClient,
+ final ResourceModel model,
+ final CallbackContext callbackContext) {
+ if (model.getFeatureGroupName() == null) {
+ model.setFeatureGroupName(awsRequest.featureGroupName());
+ }
+
+ try {
+ final FeatureGroupStatus featureGroupStatus =
+ proxyClient.injectCredentialsAndInvokeV2(TranslatorForRequest.translateToReadRequest(model),
+ proxyClient.client()::describeFeatureGroup).featureGroupStatus();
+
+ switch (featureGroupStatus) {
+ case DELETING:
+ logger.log(String.format("%s with name [%s] is stabilizing while delete.",
+ ResourceModel.TYPE_NAME, model.getPrimaryIdentifier()));
+ return false;
+ default:
+ throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getFeatureGroupName());
+ }
+ } catch (ResourceNotFoundException e) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ListHandler.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ListHandler.java
new file mode 100644
index 0000000..df31b72
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ListHandler.java
@@ -0,0 +1,71 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsRequest;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsResponse;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class ListHandler extends BaseHandlerStd {
+
+ private static final String OPERATION = "AWS-SageMaker-FeatureGroup::List";
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ return proxy.initiate(OPERATION, proxyClient, model, callbackContext)
+ .translateToServiceRequest(resourceModel -> TranslatorForRequest.translateToListRequest(request.getNextToken()))
+ .makeServiceCall((awsRequest, sdkProxyClient) -> listResources(awsRequest, sdkProxyClient))
+ .done(this::constructResourceModelFromResponse);
+ }
+
+ /**
+ * Client invocation of the list request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ * @param awsRequest the aws service request to list a resource
+ * @param proxyClient the aws service client to make the call
+ * @return response ListFeatureGroupsResponse
+ */
+ private ListFeatureGroupsResponse listResources(
+ final ListFeatureGroupsRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ ListFeatureGroupsResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::listFeatureGroups);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.LIST.toString(), ResourceModel.TYPE_NAME, null, e);
+ }
+
+ return response;
+ }
+
+ /**
+ * Build the Progress Event object from the SageMaker ListMonitoringSchedules response.
+ * @param listResponse the aws service list resource response
+ * @return progressEvent indicating success, in progress with delay callback or failed state
+ */
+ private ProgressEvent constructResourceModelFromResponse(
+ final ListFeatureGroupsResponse listResponse) {
+ return ProgressEvent.builder()
+ .nextToken(listResponse.nextToken())
+ .resourceModels(TranslatorForResponse.translateFromListResponse(listResponse))
+ .status(OperationStatus.SUCCESS)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ReadHandler.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ReadHandler.java
new file mode 100644
index 0000000..20f3732
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/ReadHandler.java
@@ -0,0 +1,72 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupResponse;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class ReadHandler extends BaseHandlerStd {
+
+ private static final String OPERATION = "AWS-SageMaker-FeatureGroup::Read";
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ return proxy.initiate(OPERATION, proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToReadRequest)
+ .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest, sdkProxyClient, model))
+ .done(this::constructResourceModelFromResponse);
+ }
+
+ /**
+ * Client invocation of the read request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ * @param awsRequest the aws service request to describe a resource
+ * @param proxyClient the aws service client to make the call
+ * @param model Resource Model
+ * @return describe resource response
+ */
+ private DescribeFeatureGroupResponse readResource(
+ final DescribeFeatureGroupRequest awsRequest,
+ final ProxyClient proxyClient,
+ final ResourceModel model) {
+
+ DescribeFeatureGroupResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeFeatureGroup);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.READ.toString(), ResourceModel.TYPE_NAME,
+ awsRequest.featureGroupName(), e);
+ }
+
+ return response;
+ }
+
+ /**
+ * Implement client invocation of the read request through the proxyClient, which is already
+ * initialised with caller credentials, correct region and retry settings
+ *
+ * @param awsResponse the aws service describe resource response
+ * @return progressEvent indicating success, in progress with delay callback or failed state
+ */
+ private ProgressEvent constructResourceModelFromResponse(
+ final DescribeFeatureGroupResponse awsResponse) {
+ return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse));
+ }
+}
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Translator.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Translator.java
new file mode 100644
index 0000000..cb1ffb7
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/Translator.java
@@ -0,0 +1,79 @@
+package software.amazon.sagemaker.featuregroup;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceLimitExceededException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnAccessDeniedException;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
+import software.amazon.cloudformation.exceptions.CfnThrottlingException;
+import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * This class contains translation methods for object other than api request/response.
+ * It also contains common methods required by other translators.
+ */
+public class Translator {
+
+ /**
+ * Throws Cfn exception corresponding to error code of the given exception.
+ *
+ * @param operation operation
+ * @param resourceType resource type
+ * @param resourceName resource name
+ * @param e exception
+ */
+ static void throwCfnException(
+ final String operation,
+ final String resourceType,
+ final String resourceName,
+ final AwsServiceException e
+ ) {
+
+ if (e instanceof ResourceInUseException) {
+ throw new ResourceAlreadyExistsException(resourceType, resourceName, e);
+ }
+
+ if (e instanceof ResourceNotFoundException) {
+ throw new CfnNotFoundException(resourceType, resourceName, e);
+ }
+
+ if (e instanceof ResourceLimitExceededException) {
+ throw new CfnServiceLimitExceededException(resourceType, e.getMessage(), e);
+ }
+
+ if(e.awsErrorDetails() != null && StringUtils.isNotBlank(e.awsErrorDetails().errorCode())) {
+ String errorMessage = e.awsErrorDetails().errorMessage();
+ switch (e.awsErrorDetails().errorCode()) {
+ case "UnauthorizedOperation":
+ throw new CfnAccessDeniedException(errorMessage, e);
+ case "ValidationException":
+ throw new CfnInvalidRequestException(errorMessage, e);
+ case "InternalError":
+ case "ServiceUnavailable":
+ throw new CfnServiceInternalErrorException(errorMessage, e);
+ case "ThrottlingException":
+ throw new CfnThrottlingException(errorMessage, e);
+ default:
+ throw new CfnGeneralServiceException(errorMessage, e);
+ }
+ }
+
+ throw new CfnGeneralServiceException(operation, e);
+ }
+
+ static Stream streamOfOrEmpty(final Collection collection) {
+ return Optional.ofNullable(collection)
+ .map(Collection::stream)
+ .orElseGet(Stream::empty);
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForRequest.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForRequest.java
new file mode 100644
index 0000000..5c0cce5
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForRequest.java
@@ -0,0 +1,129 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.services.sagemaker.model.CreateFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DataCatalogConfig;
+import software.amazon.awssdk.services.sagemaker.model.DeleteFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.FeatureDefinition;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsRequest;
+import software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig;
+import software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig;
+import software.amazon.awssdk.services.sagemaker.model.OnlineStoreSecurityConfig;
+import software.amazon.awssdk.services.sagemaker.model.S3StorageConfig;
+import software.amazon.awssdk.services.sagemaker.model.Tag;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This class is a centralized placeholder for
+ * - api request construction
+ * - object translation to/from aws sdk
+ * - resource model construction for handlers like read/list
+ */
+final class TranslatorForRequest {
+
+ private TranslatorForRequest() {}
+
+ /**
+ * Request to create a resource
+ * @param model resource model
+ * @return createFeatureGroupRequest - service request to create a resource
+ */
+ static CreateFeatureGroupRequest translateToCreateRequest(final ResourceModel model) {
+ return CreateFeatureGroupRequest.builder()
+ .featureGroupName(model.getFeatureGroupName())
+ .recordIdentifierFeatureName(model.getRecordIdentifierFeatureName())
+ .eventTimeFeatureName(model.getEventTimeFeatureName())
+ .featureDefinitions(translateFeatureDefinitions(model.getFeatureDefinitions()))
+ .onlineStoreConfig(translateOnlineStoreConfig(model.getOnlineStoreConfig()))
+ .offlineStoreConfig(translateOfflineStoreConfig(model.getOfflineStoreConfig()))
+ .description(model.getDescription())
+ .roleArn(model.getRoleArn())
+ .tags(Translator.streamOfOrEmpty(model.getTags())
+ .map(t -> Tag.builder()
+ .key(t.getKey())
+ .value(t.getValue())
+ .build())
+ .collect(Collectors.toList())
+ ).build();
+ }
+
+ /**
+ * Request to read a resource
+ * @param model resource model
+ * @return describeFeatureGroupRequest - the aws service request to describe a resource
+ */
+ static DescribeFeatureGroupRequest translateToReadRequest(final ResourceModel model) {
+ return DescribeFeatureGroupRequest.builder()
+ .featureGroupName(model.getFeatureGroupName())
+ .build();
+ }
+
+ /**
+ * Request to delete a resource
+ * @param model resource model
+ * @return deleteFeatureGroupRequest - the aws service request to delete a resource
+ */
+ static DeleteFeatureGroupRequest translateToDeleteRequest(final ResourceModel model) {
+ return DeleteFeatureGroupRequest.builder()
+ .featureGroupName(model.getFeatureGroupName())
+ .build();
+ }
+
+ /**
+ * Request to list properties of a previously created resource
+ * @param nextToken token passed to the aws service describe resource request
+ * @return awsRequest the aws service request to describe resources within aws account
+ */
+ static ListFeatureGroupsRequest translateToListRequest(final String nextToken) {
+ return ListFeatureGroupsRequest.builder().nextToken(nextToken).build();
+ }
+
+ private static List translateFeatureDefinitions(
+ List origin) {
+ if (origin == null) {
+ return null;
+ }
+ return origin.stream()
+ .map(f -> FeatureDefinition.builder()
+ .featureName(f.getFeatureName())
+ .featureType(f.getFeatureType())
+ .build())
+ .collect(Collectors.toList());
+ }
+
+ private static OnlineStoreConfig translateOnlineStoreConfig(
+ software.amazon.sagemaker.featuregroup.OnlineStoreConfig origin) {
+ if (origin == null) {
+ return null;
+ }
+ return OnlineStoreConfig.builder()
+ .securityConfig(origin.getSecurityConfig() == null ? null : OnlineStoreSecurityConfig.builder()
+ .kmsKeyId(origin.getSecurityConfig().getKmsKeyId())
+ .build())
+ .enableOnlineStore(origin.getEnableOnlineStore())
+ .build();
+ }
+
+ private static OfflineStoreConfig translateOfflineStoreConfig(
+ software.amazon.sagemaker.featuregroup.OfflineStoreConfig origin) {
+ if (origin == null) {
+ return null;
+ }
+ return OfflineStoreConfig.builder()
+ .disableGlueTableCreation(origin.getDisableGlueTableCreation())
+ .dataCatalogConfig(origin.getDataCatalogConfig() == null ? null :
+ DataCatalogConfig.builder()
+ .tableName(origin.getDataCatalogConfig().getTableName())
+ .catalog(origin.getDataCatalogConfig().getCatalog())
+ .database(origin.getDataCatalogConfig().getDatabase())
+ .build())
+ .s3StorageConfig(origin.getS3StorageConfig() == null ? null :
+ S3StorageConfig.builder()
+ .kmsKeyId(origin.getS3StorageConfig().getKmsKeyId())
+ .s3Uri(origin.getS3StorageConfig().getS3Uri())
+ .build())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForResponse.java b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForResponse.java
new file mode 100644
index 0000000..eb66a85
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/main/java/software/amazon/sagemaker/featuregroup/TranslatorForResponse.java
@@ -0,0 +1,87 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsResponse;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class TranslatorForResponse {
+
+ private TranslatorForResponse() {}
+
+ /**
+ * Translates resource object from sdk into a resource model
+ * @param awsResponse the aws service describe resource response
+ * @return model resource model
+ */
+ static ResourceModel translateFromReadResponse(final DescribeFeatureGroupResponse awsResponse) {
+ return ResourceModel.builder()
+ .featureGroupName(awsResponse.featureGroupName())
+ .recordIdentifierFeatureName(awsResponse.recordIdentifierFeatureName())
+ .eventTimeFeatureName(awsResponse.eventTimeFeatureName())
+ .featureDefinitions(translateToFeatureDefinitions(awsResponse.featureDefinitions()))
+ .onlineStoreConfig(translateToOnlineStoreConfig(awsResponse.onlineStoreConfig()))
+ .offlineStoreConfig(translateToOfflineStoreConfig(awsResponse.offlineStoreConfig()))
+ .description(awsResponse.description())
+ .roleArn(awsResponse.roleArn())
+ .build();
+ }
+
+ /**
+ * Translates resource objects from sdk into a resource model
+ * @param awsResponse the aws service list resource response
+ * @return list of resource models
+ */
+ static List translateFromListResponse(final ListFeatureGroupsResponse awsResponse) {
+ return Translator.streamOfOrEmpty(awsResponse.featureGroupSummaries())
+ .map(summary -> ResourceModel.builder()
+ .featureGroupName(summary.featureGroupName())
+ .build())
+ .collect(Collectors.toList());
+ }
+
+ private static List translateToFeatureDefinitions(
+ List origin) {
+ return origin.stream()
+ .map(f -> FeatureDefinition.builder()
+ .featureName(f.featureName())
+ .featureType(f.featureType().toString())
+ .build())
+ .collect(Collectors.toList());
+ }
+
+ private static OnlineStoreConfig translateToOnlineStoreConfig(
+ software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig origin) {
+ if (origin == null) {
+ return null;
+ }
+ return OnlineStoreConfig.builder()
+ .securityConfig(origin.securityConfig() == null ? null : OnlineStoreSecurityConfig.builder()
+ .kmsKeyId(origin.securityConfig().kmsKeyId())
+ .build())
+ .enableOnlineStore(origin.enableOnlineStore())
+ .build();
+ }
+
+ private static OfflineStoreConfig translateToOfflineStoreConfig(
+ software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig origin) {
+ if (origin == null) {
+ return null;
+ }
+ return OfflineStoreConfig.builder()
+ .disableGlueTableCreation(origin.disableGlueTableCreation())
+ .dataCatalogConfig(origin.dataCatalogConfig() == null ? null :
+ DataCatalogConfig.builder()
+ .tableName(origin.dataCatalogConfig().tableName())
+ .catalog(origin.dataCatalogConfig().catalog())
+ .database(origin.dataCatalogConfig().database())
+ .build())
+ .s3StorageConfig(origin.s3StorageConfig() == null ? null :
+ S3StorageConfig.builder()
+ .kmsKeyId(origin.s3StorageConfig().kmsKeyId())
+ .s3Uri(origin.s3StorageConfig().s3Uri())
+ .build())
+ .build();
+ }
+}
diff --git a/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/AbstractTestBase.java b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/AbstractTestBase.java
new file mode 100644
index 0000000..3aa31b1
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/AbstractTestBase.java
@@ -0,0 +1,78 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.awssdk.awscore.AwsResponse;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.pagination.sync.SdkIterable;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.ProxyClient;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+public class AbstractTestBase {
+
+ protected static final String TEST_FEATURE_GROUP_NAME = "test-feature-group-name";
+ protected static final String TEST_FEATURE_GROUP_ARN = "test-arn";
+ protected static final String TEST_EVENT_TIME_FEATURE_NAME = "test-event-time-feature-name";
+ protected static final String TEST_RECORD_ID_FEATURE_NAME = "test-record-id-feature-name";
+ protected static final String TEST_DESCRIPTION = "test-description";
+ protected static final String TEST_ROLE_ARN = "test-role-arn";
+ protected static final Instant TEST_TIME = Instant.now();
+ protected static final String TEST_ERROR_MESSAGE = "test error message";
+ protected static final Credentials MOCK_CREDENTIALS;
+ protected static final LoggerProxy logger;
+
+ static {
+ MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token");
+ logger = new LoggerProxy();
+ }
+ static ProxyClient MOCK_PROXY(
+ final AmazonWebServicesClientProxy proxy,
+ final SageMakerClient sagemakerClient) {
+ return new ProxyClient() {
+ @Override
+ public ResponseT
+ injectCredentialsAndInvokeV2(RequestT request, Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
+ }
+
+ @Override
+ public
+ CompletableFuture
+ injectCredentialsAndInvokeV2Async(RequestT request, Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public >
+ IterableT
+ injectCredentialsAndInvokeIterableV2(RequestT request, Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction);
+ }
+
+ @Override
+ public ResponseInputStream
+ injectCredentialsAndInvokeV2InputStream(RequestT requestT, Function> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResponseBytes
+ injectCredentialsAndInvokeV2Bytes(RequestT requestT, Function> function) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SageMakerClient client() {
+ return sagemakerClient;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/CreateHandlerTest.java b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/CreateHandlerTest.java
new file mode 100644
index 0000000..a6bd2c9
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/CreateHandlerTest.java
@@ -0,0 +1,428 @@
+package software.amazon.sagemaker.featuregroup;
+
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.CreateFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.CreateFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.FeatureType;
+import software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatusValue;
+import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceLimitExceededException;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
+import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class CreateHandlerTest extends AbstractTestBase {
+
+ private final ResourceModel requestModel = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .build();
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testCreateHandler_SimpleSuccess() {
+ List featureDefinitions =
+ Arrays.asList(
+ software.amazon.awssdk.services.sagemaker.model.FeatureDefinition.builder()
+ .featureName("year").featureType(FeatureType.INTEGRAL.toString()).build(),
+ software.amazon.awssdk.services.sagemaker.model.FeatureDefinition.builder()
+ .featureName("name").featureType(FeatureType.STRING.toString()).build()
+ );
+
+ software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig onlineStoreConfig =
+ software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig.builder()
+ .enableOnlineStore(true)
+ .securityConfig(
+ software.amazon.awssdk.services.sagemaker.model
+ .OnlineStoreSecurityConfig.builder()
+ .kmsKeyId("kms").build()
+ )
+ .build();
+
+ software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig offlineStoreConfig =
+ software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig.builder()
+ .dataCatalogConfig(software.amazon.awssdk.services.sagemaker.model.DataCatalogConfig.builder()
+ .catalog("c").database("d").tableName("t").build()
+ )
+ .s3StorageConfig(software.amazon.awssdk.services.sagemaker.model.S3StorageConfig.builder()
+ .s3Uri("s3").kmsKeyId("kms").build()
+ )
+ .disableGlueTableCreation(false)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureDefinitions(featureDefinitions)
+ .onlineStoreConfig(onlineStoreConfig)
+ .offlineStoreConfig(offlineStoreConfig)
+ .featureGroupStatus(FeatureGroupStatus.CREATED)
+ .failureReason("none")
+ .offlineStoreStatus(software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatus.builder()
+ .blockedReason("no")
+ .status(OfflineStoreStatusValue.ACTIVE)
+ .build())
+ .build();
+
+ final CreateFeatureGroupResponse createFeatureGroupResponse = CreateFeatureGroupResponse.builder()
+ .featureGroupArn(TEST_FEATURE_GROUP_ARN)
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse);
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenReturn(createFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .featureDefinitions(Arrays.asList(
+ FeatureDefinition.builder().featureName("year").featureType(FeatureType.INTEGRAL.toString()).build(),
+ FeatureDefinition.builder().featureName("name").featureType(FeatureType.STRING.toString()).build()
+ ))
+ .onlineStoreConfig(OnlineStoreConfig
+ .builder()
+ .enableOnlineStore(true)
+ .securityConfig(OnlineStoreSecurityConfig.builder().kmsKeyId("kms").build())
+ .build()
+ )
+ .offlineStoreConfig(OfflineStoreConfig.builder()
+ .dataCatalogConfig(DataCatalogConfig.builder().catalog("c").database("d").tableName("t").build())
+ .s3StorageConfig(S3StorageConfig.builder().s3Uri("s3").kmsKeyId("kms").build())
+ .disableGlueTableCreation(false).build())
+ .build();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testCreateHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .awsErrorDetails(AwsErrorDetails.builder()
+ .errorCode("InternalError")
+ .errorMessage(TEST_ERROR_MESSAGE)
+ .build())
+ .statusCode(500)
+ .build();
+
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(),
+ serviceInternalException.awsErrorDetails().errorMessage()));
+ }
+
+ @Test
+ public void testCreateHandler_FeatureGroupAlreadyExists_Fails() {
+ final ResourceInUseException resourceInUseException = ResourceInUseException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenThrow(resourceInUseException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.AlreadyExists.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_FEATURE_GROUP_NAME));
+ }
+
+ @Test
+ public void testCreateHandler_ResourceLimitExceededException() {
+ final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder()
+ .message(TEST_ERROR_MESSAGE)
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenThrow(resourceLimitExceededException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(CfnServiceLimitExceededException.class, () -> invokeHandleRequest(request));
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceLimitExceeded.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_ERROR_MESSAGE));
+ }
+
+ @Test
+ public void testCreateHandler_ValidationFailure() {
+ final AwsServiceException validationFailureException = SageMakerException.builder()
+ .awsErrorDetails(AwsErrorDetails.builder()
+ .errorCode("ValidationException")
+ .errorMessage("Value null at 'featureGroupName' failed to " +
+ "satisfy constraint: Member must not be null")
+ .build())
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenThrow(validationFailureException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(CfnInvalidRequestException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.InvalidRequest.getMessage(),
+ validationFailureException.awsErrorDetails().errorMessage()));
+ }
+
+ @Test
+ public void testCreateHandler_NoExceptionMessage() {
+ final AwsServiceException someException = SageMakerException.builder()
+ .statusCode(400)
+ .build();
+
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenThrow(someException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.CREATE));
+ }
+
+ @Test
+ public void testCreateHandler_VerifyStabilization_EventualConsistency() {
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse2 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATING)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse3 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATED)
+ .build();
+
+ final CreateFeatureGroupResponse createFeatureGroupResponse = CreateFeatureGroupResponse.builder()
+ .featureGroupArn(TEST_FEATURE_GROUP_ARN)
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenThrow(ResourceNotFoundException.builder().build())
+ .thenReturn(describeFeatureGroupResponse2).thenReturn(describeFeatureGroupResponse3);
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenReturn(createFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .featureDefinitions(Collections.emptyList())
+ .build();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testCreateHandler_VerifyStabilization_Success() {
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse1 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATING)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse2 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATED)
+ .build();
+
+ final CreateFeatureGroupResponse createFeatureGroupResponse = CreateFeatureGroupResponse.builder()
+ .featureGroupArn(TEST_FEATURE_GROUP_ARN)
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse1).thenReturn(describeFeatureGroupResponse2);
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenReturn(createFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .featureDefinitions(Collections.emptyList())
+ .build();
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testCreateHandler_VerifyStabilization_Failed() {
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse1 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATING)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse2 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.CREATE_FAILED)
+ .build();
+
+ final CreateFeatureGroupResponse createFeatureGroupResponse = CreateFeatureGroupResponse.builder()
+ .featureGroupArn(TEST_FEATURE_GROUP_ARN)
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse1).thenReturn(describeFeatureGroupResponse2);
+ when(proxyClient.client().createFeatureGroup(any(CreateFeatureGroupRequest.class)))
+ .thenReturn(createFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(requestModel)
+ .build();
+
+ Exception exception = assertThrows(CfnNotStabilizedException.class, () -> invokeHandleRequest(request));
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotStabilized.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_FEATURE_GROUP_NAME));
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final CreateHandler handler = new CreateHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+}
diff --git a/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/DeleteHandlerTest.java b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/DeleteHandlerTest.java
new file mode 100644
index 0000000..27e2375
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/DeleteHandlerTest.java
@@ -0,0 +1,205 @@
+package software.amazon.sagemaker.featuregroup;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DeleteFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DeleteFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteHandlerTest extends AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testDeleteHandler_SimpleSuccess() {
+ final DeleteFeatureGroupResponse deleteFeatureGroupResponse = DeleteFeatureGroupResponse.builder()
+ .build();
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenThrow(ResourceNotFoundException.class);
+ when(proxyClient.client().deleteFeatureGroup(any(DeleteFeatureGroupRequest.class)))
+ .thenReturn(deleteFeatureGroupResponse);
+
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo((OperationStatus.SUCCESS));
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ assertThat(response.getResourceModel()).isNull();
+ }
+
+ @Test
+ public void testDeleteHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .awsErrorDetails(AwsErrorDetails.builder()
+ .errorMessage(TEST_ERROR_MESSAGE)
+ .errorCode("InternalError")
+ .build())
+ .statusCode(500)
+ .build();
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ when(proxyClient.client().deleteFeatureGroup(any(DeleteFeatureGroupRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ Exception exception = assertThrows(CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(),
+ serviceInternalException.awsErrorDetails().errorMessage()));
+ }
+
+ @Test
+ public void testDeleteHandler_FeatureGroupDoesNotExists_Fails() {
+ when(proxyClient.client().deleteFeatureGroup(any(DeleteFeatureGroupRequest.class)))
+ .thenThrow(ResourceNotFoundException.class);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_FEATURE_GROUP_NAME));
+ }
+
+ @Test
+ public void testDeleteHandler_VerifyStabilization_SuccessfulDelete() {
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse1 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.DELETING)
+ .build();
+
+ final DeleteFeatureGroupResponse deleteFeatureGroupResponse = DeleteFeatureGroupResponse.builder()
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse1).thenThrow(ResourceNotFoundException.class);
+ when(proxyClient.client().deleteFeatureGroup(any(DeleteFeatureGroupRequest.class)))
+ .thenReturn(deleteFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testDeleteHandler_VerifyStabilization_ResourceNotDeleted() {
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse1 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.DELETING)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse2 =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureGroupStatus(FeatureGroupStatus.DELETE_FAILED)
+ .build();
+
+ final DeleteFeatureGroupResponse deleteFeatureGroupResponse = DeleteFeatureGroupResponse.builder()
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse1).thenReturn(describeFeatureGroupResponse2);
+ when(proxyClient.client().deleteFeatureGroup(any(DeleteFeatureGroupRequest.class)))
+ .thenReturn(deleteFeatureGroupResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows(CfnNotStabilizedException.class, () -> invokeHandleRequest(request));
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotStabilized.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_FEATURE_GROUP_NAME));
+ }
+
+
+ private ResourceModel getRequestResourceModel() {
+ return ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .build();
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final DeleteHandler handler = new DeleteHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+}
diff --git a/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ListHandlerTest.java b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ListHandlerTest.java
new file mode 100644
index 0000000..723f7c6
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ListHandlerTest.java
@@ -0,0 +1,130 @@
+package software.amazon.sagemaker.featuregroup;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupSummary;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsRequest;
+import software.amazon.awssdk.services.sagemaker.model.ListFeatureGroupsResponse;
+import software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatus;
+import software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatusValue;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class ListHandlerTest extends AbstractTestBase {
+
+ public static final String TEST_TOKEN = "testToken";
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testListHandler_SimpleSuccess() {
+ final FeatureGroupSummary featureGroupSummary = FeatureGroupSummary.builder()
+ .creationTime(TEST_TIME)
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .featureGroupArn(TEST_FEATURE_GROUP_ARN)
+ .featureGroupStatus(FeatureGroupStatus.CREATED)
+ .offlineStoreStatus(OfflineStoreStatus.builder()
+ .status(OfflineStoreStatusValue.ACTIVE)
+ .build())
+ .build();
+
+ final ListFeatureGroupsResponse listFeatureGroupsResponse =
+ ListFeatureGroupsResponse.builder()
+ .featureGroupSummaries(Arrays.asList(featureGroupSummary))
+ .nextToken(TEST_TOKEN)
+ .build();
+
+ when(proxyClient.client().listFeatureGroups(any(ListFeatureGroupsRequest.class)))
+ .thenReturn(listFeatureGroupsResponse);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .build();
+
+ List expectedModels = new ArrayList<>();
+ expectedModels.add(expectedModelFromResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isNull();
+ assertThat(response.getResourceModels()).isEqualTo(expectedModels);
+ assertThat(response.getNextToken()).isEqualTo(TEST_TOKEN);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void testListHandler_SimpleSuccess_NoFeatureGroupExist() {
+ final ListFeatureGroupsResponse listFeatureGroupsResponse =
+ ListFeatureGroupsResponse.builder()
+ .featureGroupSummaries(Collections.emptyList())
+ .nextToken(null)
+ .build();
+
+ when(proxyClient.client().listFeatureGroups(any(ListFeatureGroupsRequest.class)))
+ .thenReturn(listFeatureGroupsResponse);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isNull();
+ assertThat(response.getResourceModels()).isEqualTo(Collections.emptyList());
+ assertThat(response.getNextToken()).isNull();
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final ListHandler handler = new ListHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+
+ private ResourceModel getRequestResourceModel() {
+ return ResourceModel.builder().build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ReadHandlerTest.java b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ReadHandlerTest.java
new file mode 100644
index 0000000..d33a045
--- /dev/null
+++ b/aws-sagemaker-featuregroup/src/test/java/software/amazon/sagemaker/featuregroup/ReadHandlerTest.java
@@ -0,0 +1,192 @@
+package software.amazon.sagemaker.featuregroup;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeFeatureGroupResponse;
+import software.amazon.awssdk.services.sagemaker.model.FeatureDefinition;
+import software.amazon.awssdk.services.sagemaker.model.FeatureGroupStatus;
+import software.amazon.awssdk.services.sagemaker.model.FeatureType;
+import software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatusValue;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.sagemaker.model.SageMakerException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@Slf4j
+@ExtendWith(MockitoExtension.class)
+public class ReadHandlerTest extends AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private ProxyClient proxyClient;
+
+ @Mock
+ SageMakerClient sdkClient;
+
+ @BeforeEach
+ public void setup() {
+ proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis());
+ sdkClient = mock(SageMakerClient.class);
+ proxyClient = MOCK_PROXY(proxy, sdkClient);
+ }
+
+ @Test
+ public void testReadHandler_SimpleSuccess() {
+ List featureDefinitions =
+ Arrays.asList(
+ software.amazon.awssdk.services.sagemaker.model.FeatureDefinition.builder()
+ .featureName("year").featureType(FeatureType.INTEGRAL.toString()).build(),
+ software.amazon.awssdk.services.sagemaker.model.FeatureDefinition.builder()
+ .featureName("name").featureType(FeatureType.STRING.toString()).build()
+ );
+
+ software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig onlineStoreConfig =
+ software.amazon.awssdk.services.sagemaker.model.OnlineStoreConfig.builder()
+ .enableOnlineStore(true)
+ .securityConfig(
+ software.amazon.awssdk.services.sagemaker.model
+ .OnlineStoreSecurityConfig.builder()
+ .kmsKeyId("kms").build()
+ )
+ .build();
+
+ software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig offlineStoreConfig =
+ software.amazon.awssdk.services.sagemaker.model.OfflineStoreConfig.builder()
+ .dataCatalogConfig(software.amazon.awssdk.services.sagemaker.model.DataCatalogConfig.builder()
+ .catalog("c").database("d").tableName("t").build()
+ )
+ .s3StorageConfig(software.amazon.awssdk.services.sagemaker.model.S3StorageConfig.builder()
+ .s3Uri("s3").kmsKeyId("kms").build()
+ )
+ .disableGlueTableCreation(false)
+ .build();
+
+ final DescribeFeatureGroupResponse describeFeatureGroupResponse =
+ DescribeFeatureGroupResponse.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .creationTime(TEST_TIME)
+ .featureDefinitions(featureDefinitions)
+ .onlineStoreConfig(onlineStoreConfig)
+ .offlineStoreConfig(offlineStoreConfig)
+ .featureGroupStatus(FeatureGroupStatus.CREATED)
+ .failureReason("none")
+ .offlineStoreStatus(software.amazon.awssdk.services.sagemaker.model.OfflineStoreStatus.builder()
+ .blockedReason("no")
+ .status(OfflineStoreStatusValue.ACTIVE)
+ .build())
+ .build();
+
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenReturn(describeFeatureGroupResponse);
+
+ ResourceModel expectedModelFromResponse = ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .eventTimeFeatureName(TEST_EVENT_TIME_FEATURE_NAME)
+ .recordIdentifierFeatureName(TEST_RECORD_ID_FEATURE_NAME)
+ .description(TEST_DESCRIPTION)
+ .roleArn(TEST_ROLE_ARN)
+ .featureDefinitions(Arrays.asList(
+ software.amazon.sagemaker.featuregroup.FeatureDefinition.builder().featureName("year").featureType(FeatureType.INTEGRAL.toString()).build(),
+ software.amazon.sagemaker.featuregroup.FeatureDefinition.builder().featureName("name").featureType(FeatureType.STRING.toString()).build()
+ ))
+ .onlineStoreConfig(OnlineStoreConfig
+ .builder()
+ .enableOnlineStore(true)
+ .securityConfig(OnlineStoreSecurityConfig.builder().kmsKeyId("kms").build())
+ .build()
+ )
+ .offlineStoreConfig(OfflineStoreConfig.builder()
+ .dataCatalogConfig(DataCatalogConfig.builder().catalog("c").database("d").tableName("t").build())
+ .s3StorageConfig(S3StorageConfig.builder().s3Uri("s3").kmsKeyId("kms").build())
+ .disableGlueTableCreation(false).build())
+ .build();
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+ final ProgressEvent response = invokeHandleRequest(request);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getResourceModel()).isEqualTo(expectedModelFromResponse);
+ assertThat(response.getMessage()).isNull();
+ assertThat(response.getErrorCode()).isNull();
+ verify(proxyClient.client()).describeFeatureGroup(any(DescribeFeatureGroupRequest.class));
+ }
+
+ @Test
+ public void testReadHandler_ServiceInternalException() {
+ final AwsServiceException serviceInternalException = SageMakerException.builder()
+ .message("test error message")
+ .statusCode(500)
+ .build();
+
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenThrow(serviceInternalException);
+
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+
+ Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request));
+
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(),
+ Action.READ));
+ }
+
+ @Test
+ public void testReadHandler_ResourceNotFoundException() {
+ when(proxyClient.client().describeFeatureGroup(any(DescribeFeatureGroupRequest.class)))
+ .thenThrow(ResourceNotFoundException.class);
+ final ResourceHandlerRequest request = ResourceHandlerRequest.builder()
+ .desiredResourceState(getRequestResourceModel())
+ .build();
+ Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request));
+ assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(),
+ ResourceModel.TYPE_NAME, TEST_FEATURE_GROUP_NAME));
+ }
+
+ private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) {
+ final ReadHandler handler = new ReadHandler();
+ return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
+ }
+
+ private ResourceModel getRequestResourceModel() {
+ return ResourceModel.builder()
+ .featureGroupName(TEST_FEATURE_GROUP_NAME)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-featuregroup/template.yml b/aws-sagemaker-featuregroup/template.yml
new file mode 100644
index 0000000..2898b44
--- /dev/null
+++ b/aws-sagemaker-featuregroup/template.yml
@@ -0,0 +1,24 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+Description: AWS SAM template for the AWS::SageMaker::FeatureGroup resource type
+
+Globals:
+ Function:
+ Timeout: 180 # docker start-up times can be long for SAM CLI
+ MemorySize: 256
+
+Resources:
+ TypeFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.sagemaker.featuregroup.HandlerWrapper::handleRequest
+ Runtime: java8
+ CodeUri: ./target/aws-sagemaker-featuregroup-handler-1.0-SNAPSHOT.jar
+
+ TestEntrypoint:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: software.amazon.sagemaker.featuregroup.HandlerWrapper::testEntrypoint
+ Runtime: java8
+ CodeUri: ./target/aws-sagemaker-featuregroup-handler-1.0-SNAPSHOT.jar
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/.rpdk-config b/aws-sagemaker-modelbiasjobdefinition/.rpdk-config
new file mode 100644
index 0000000..c2838ca
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/.rpdk-config
@@ -0,0 +1,16 @@
+{
+ "typeName": "AWS::SageMaker::ModelBiasJobDefinition",
+ "language": "java",
+ "runtime": "java8",
+ "entrypoint": "software.amazon.sagemaker.modelbiasjobdefinition.HandlerWrapper::handleRequest",
+ "testEntrypoint": "software.amazon.sagemaker.modelbiasjobdefinition.HandlerWrapper::testEntrypoint",
+ "settings": {
+ "namespace": [
+ "software",
+ "amazon",
+ "sagemaker",
+ "modelbiasjobdefinition"
+ ],
+ "protocolVersion": "2.0.0"
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/README.md b/aws-sagemaker-modelbiasjobdefinition/README.md
new file mode 100644
index 0000000..0bbd3e4
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/README.md
@@ -0,0 +1,178 @@
+# AWS::SageMaker::ModelBiasJobDefinition
+
+Resource Type definition for AWS::SageMaker::ModelBiasJobDefinition
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::ModelBiasJobDefinition",
+ "Properties" : {
+ "JobDefinitionName " : String ,
+ "ModelBiasBaselineConfig " : ModelBiasBaselineConfig ,
+ "ModelBiasAppSpecification " : ModelBiasAppSpecification ,
+ "ModelBiasJobInput " : ModelBiasJobInput ,
+ "ModelBiasJobOutputConfig " : MonitoringOutputConfig ,
+ "JobResources " : MonitoringResources ,
+ "NetworkConfig " : NetworkConfig ,
+ "RoleArn " : String ,
+ "StoppingCondition " : StoppingCondition ,
+ "Tags " : [ Tag , ... ] ,
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::ModelBiasJobDefinition
+Properties:
+ JobDefinitionName : String
+ ModelBiasBaselineConfig : ModelBiasBaselineConfig
+ ModelBiasAppSpecification : ModelBiasAppSpecification
+ ModelBiasJobInput : ModelBiasJobInput
+ ModelBiasJobOutputConfig : MonitoringOutputConfig
+ JobResources : MonitoringResources
+ NetworkConfig : NetworkConfig
+ RoleArn : String
+ StoppingCondition : StoppingCondition
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### JobDefinitionName
+
+The name of the job definition.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+_Required_: No
+
+_Type_: ModelBiasBaselineConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasAppSpecification
+
+Container image configuration object for the monitoring job.
+
+_Required_: Yes
+
+_Type_: ModelBiasAppSpecification
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasJobInput
+
+The inputs for a monitoring job.
+
+_Required_: Yes
+
+_Type_: ModelBiasJobInput
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasJobOutputConfig
+
+The output configuration for monitoring jobs.
+
+_Required_: Yes
+
+_Type_: MonitoringOutputConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### JobResources
+
+Identifies the resources to deploy for a monitoring job.
+
+_Required_: Yes
+
+_Type_: MonitoringResources
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+_Required_: No
+
+_Type_: NetworkConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+_Required_: No
+
+_Type_: StoppingCondition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pairs to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the JobDefinitionArn.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+The time at which the job definition was created.
+
+#### JobDefinitionArn
+
+The Amazon Resource Name (ARN) of job definition.
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/aws-sagemaker-modelbiasjobdefinition.json b/aws-sagemaker-modelbiasjobdefinition/aws-sagemaker-modelbiasjobdefinition.json
new file mode 100644
index 0000000..5e39c96
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/aws-sagemaker-modelbiasjobdefinition.json
@@ -0,0 +1,467 @@
+{
+ "typeName" : "AWS::SageMaker::ModelBiasJobDefinition",
+ "description" : "Resource Type definition for AWS::SageMaker::ModelBiasJobDefinition",
+ "additionalProperties" : false,
+ "properties" : {
+ "JobDefinitionArn" : {
+ "description": "The Amazon Resource Name (ARN) of job definition.",
+ "type" : "string",
+ "minLength": 1,
+ "maxLength": 256
+ },
+ "JobDefinitionName" : {
+ "$ref" : "#/definitions/JobDefinitionName"
+ },
+ "ModelBiasBaselineConfig": {
+ "$ref": "#/definitions/ModelBiasBaselineConfig"
+ },
+ "ModelBiasAppSpecification": {
+ "$ref": "#/definitions/ModelBiasAppSpecification"
+ },
+ "ModelBiasJobInput": {
+ "$ref": "#/definitions/ModelBiasJobInput"
+ },
+ "ModelBiasJobOutputConfig": {
+ "$ref": "#/definitions/MonitoringOutputConfig"
+ },
+ "JobResources": {
+ "$ref": "#/definitions/MonitoringResources"
+ },
+ "NetworkConfig": {
+ "$ref": "#/definitions/NetworkConfig"
+ },
+ "RoleArn": {
+ "description": "The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.",
+ "type" : "string",
+ "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$",
+ "minLength": 20,
+ "maxLength": 2048
+ },
+ "StoppingCondition": {
+ "$ref": "#/definitions/StoppingCondition"
+ },
+ "Tags" : {
+ "type" : "array",
+ "maxItems" : 50,
+ "description" : "An array of key-value pairs to apply to this resource.",
+ "items" : {
+ "$ref" : "#/definitions/Tag"
+ }
+ },
+ "CreationTime": {
+ "description": "The time at which the job definition was created.",
+ "type": "string"
+ }
+ },
+ "definitions" : {
+ "ModelBiasBaselineConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Baseline configuration used to validate that the data conforms to the specified constraints and statistics.",
+ "properties" : {
+ "BaseliningJobName": {
+ "$ref": "#/definitions/ProcessingJobName"
+ },
+ "ConstraintsResource": {
+ "$ref": "#/definitions/ConstraintsResource"
+ }
+ }
+ },
+ "ConstraintsResource" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The baseline constraints resource for a monitoring job.",
+ "properties" : {
+ "S3Uri": {
+ "description": "The Amazon S3 URI for baseline constraint file in Amazon S3 that the current monitoring job should validated against.",
+ "$ref": "#/definitions/S3Uri"
+ }
+ }
+ },
+ "S3Uri": {
+ "type": "string",
+ "description": "The Amazon S3 URI.",
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$",
+ "maxLength": 1024
+ },
+ "Environment" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "Sets the environment variables in the Docker container",
+ "patternProperties" : {
+ "[a-zA-Z_][a-zA-Z0-9_]*": {
+ "type": "string",
+ "minLength" : 1,
+ "maxLength" : 256
+ },
+ "[\\S\\s]*": {
+ "type": "string",
+ "maxLength" : 256
+ }
+ }
+ },
+ "ModelBiasAppSpecification" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Container image configuration object for the monitoring job.",
+ "properties" : {
+ "ImageUri": {
+ "type" : "string",
+ "description" : "The container image to be run by the monitoring job.",
+ "pattern": ".*",
+ "maxLength" : 255
+ },
+ "ConfigUri": {
+ "type" : "string",
+ "description" : "The S3 URI to an analysis configuration file",
+ "pattern": ".*",
+ "maxLength" : 255
+ },
+ "Environment": {
+ "$ref": "#/definitions/Environment"
+ }
+ },
+ "required" : [ "ImageUri", "ConfigUri" ]
+ },
+ "ModelBiasJobInput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "The inputs for a monitoring job.",
+ "properties" : {
+ "EndpointInput": {
+ "$ref" : "#/definitions/EndpointInput"
+ },
+ "GroundTruthS3Input": {
+ "$ref" : "#/definitions/MonitoringGroundTruthS3Input"
+ }
+ },
+ "required": [ "EndpointInput", "GroundTruthS3Input" ]
+ },
+ "EndpointInput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The endpoint for a monitoring job.",
+ "properties" : {
+ "EndpointName": {
+ "$ref" : "#/definitions/EndpointName"
+ },
+ "LocalPath": {
+ "type" : "string",
+ "description" : "Path to the filesystem where the endpoint data is available to the container.",
+ "pattern": ".*",
+ "maxLength" : 256
+ },
+ "S3DataDistributionType": {
+ "type" : "string",
+ "description" : "Whether input data distributed in Amazon S3 is fully replicated or sharded by an S3 key. Defauts to FullyReplicated",
+ "enum":[
+ "FullyReplicated",
+ "ShardedByS3Key"
+ ]
+ },
+ "S3InputMode": {
+ "type" : "string",
+ "description" : "Whether the Pipe or File is used as the input mode for transfering data for the monitoring job. Pipe mode is recommended for large datasets. File mode is useful for small files that fit in memory. Defaults to File.",
+ "enum":[
+ "Pipe",
+ "File"
+ ]
+ },
+ "StartTimeOffset": {
+ "description" : "Monitoring start time offset, e.g. -PT1H",
+ "$ref": "#/definitions/MonitoringTimeOffsetString"
+ },
+ "EndTimeOffset": {
+ "description" : "Monitoring end time offset, e.g. PT0H",
+ "$ref": "#/definitions/MonitoringTimeOffsetString"
+ },
+ "FeaturesAttribute": {
+ "type" : "string",
+ "description" : "JSONpath to locate features in JSONlines dataset",
+ "maxLength" : 256
+ },
+ "InferenceAttribute": {
+ "type" : "string",
+ "description" : "Index or JSONpath to locate predicted label(s)",
+ "maxLength" : 256
+ },
+ "ProbabilityAttribute": {
+ "type" : "string",
+ "description" : "Index or JSONpath to locate probabilities",
+ "maxLength" : 256
+ },
+ "ProbabilityThresholdAttribute": {
+ "type" : "number",
+ "format": "double"
+ }
+ },
+ "required" : [ "EndpointName", "LocalPath" ]
+ },
+ "MonitoringOutputConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "The output configuration for monitoring jobs.",
+ "properties" : {
+ "KmsKeyId": {
+ "type" : "string",
+ "description" : "The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption.",
+ "pattern": ".*",
+ "maxLength" : 2048
+ },
+ "MonitoringOutputs" : {
+ "type" : "array",
+ "description" : "Monitoring outputs for monitoring jobs. This is where the output of the periodic monitoring jobs is uploaded.",
+ "minLength" : 1,
+ "maxLength" : 1,
+ "items" : {
+ "$ref" : "#/definitions/MonitoringOutput"
+ }
+ }
+ },
+ "required" : [ "MonitoringOutputs" ]
+ },
+ "MonitoringOutput" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description" : "The output object for a monitoring job.",
+ "properties" : {
+ "S3Output": {
+ "$ref" : "#/definitions/S3Output"
+ }
+ },
+ "required": [ "S3Output" ]
+ },
+ "S3Output" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Information about where and how to store the results of a monitoring job.",
+ "properties" : {
+ "LocalPath": {
+ "type" : "string",
+ "description" : "The local path to the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job. LocalPath is an absolute path for the output data.",
+ "pattern": ".*",
+ "maxLength" : 256
+ },
+ "S3UploadMode" : {
+ "type" : "string",
+ "description" : "Whether to upload the results of the monitoring job continuously or after the job completes.",
+ "enum":[
+ "Continuous",
+ "EndOfJob"
+ ]
+ },
+ "S3Uri" : {
+ "type" : "string",
+ "description" : "A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.",
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$",
+ "maxLength" : 512
+ }
+ },
+ "required" : [ "LocalPath", "S3Uri" ]
+ },
+ "MonitoringResources" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Identifies the resources to deploy for a monitoring job.",
+ "properties" : {
+ "ClusterConfig": {
+ "$ref" : "#/definitions/ClusterConfig"
+ }
+ },
+ "required" : [ "ClusterConfig" ]
+ },
+ "ClusterConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Configuration for the cluster used to run model monitoring jobs.",
+ "properties" : {
+ "InstanceCount": {
+ "description" : "The number of ML compute instances to use in the model monitoring job. For distributed processing jobs, specify a value greater than 1. The default value is 1.",
+ "type" : "integer",
+ "minimum" : 1,
+ "maximum" : 100
+ },
+ "InstanceType": {
+ "description" : "The ML compute instance type for the processing job.",
+ "type" : "string"
+ },
+ "VolumeKmsKeyId": {
+ "description" : "The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s) that run the model monitoring job.",
+ "type" : "string",
+ "minimum" : 1,
+ "maximum" : 2048
+ },
+ "VolumeSizeInGB": {
+ "description" : "The size of the ML storage volume, in gigabytes, that you want to provision. You must specify sufficient ML storage for your scenario.",
+ "type" : "integer",
+ "minimum" : 1,
+ "maximum" : 16384
+ }
+ },
+ "required" : [ "InstanceCount", "InstanceType", "VolumeSizeInGB" ]
+ },
+ "NetworkConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.",
+ "properties" : {
+ "EnableInterContainerTrafficEncryption": {
+ "description" : "Whether to encrypt all communications between distributed processing jobs. Choose True to encrypt communications. Encryption provides greater security for distributed processing jobs, but the processing might take longer.",
+ "type" : "boolean"
+ },
+ "EnableNetworkIsolation": {
+ "description" : "Whether to allow inbound and outbound network calls to and from the containers used for the processing job.",
+ "type" : "boolean"
+ },
+ "VpcConfig": {
+ "$ref" : "#/definitions/VpcConfig"
+ }
+ }
+ },
+ "VpcConfig" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.",
+ "properties" : {
+ "SecurityGroupIds": {
+ "description" : "The VPC security group IDs, in the form sg-xxxxxxxx. Specify the security groups for the VPC that is specified in the Subnets field.",
+ "type" : "array",
+ "minItems" : 1,
+ "maxItems" : 5,
+ "items" : {
+ "type" : "string",
+ "maxLength": 32,
+ "pattern": "[-0-9a-zA-Z]+"
+ }
+ },
+ "Subnets": {
+ "description" : "The ID of the subnets in the VPC to which you want to connect to your monitoring jobs.",
+ "type" : "array",
+ "minItems" : 1,
+ "maxItems" : 16,
+ "items" : {
+ "type" : "string",
+ "maxLength": 32,
+ "pattern": "[-0-9a-zA-Z]+"
+ }
+ }
+ },
+ "required" : [ "SecurityGroupIds", "Subnets" ]
+ },
+ "StoppingCondition" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Specifies a time limit for how long the monitoring job is allowed to run.",
+ "properties" : {
+ "MaxRuntimeInSeconds": {
+ "description": "The maximum runtime allowed in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 86400
+ }
+ },
+ "required" : [ "MaxRuntimeInSeconds" ]
+ },
+ "Tag" : {
+ "description" : "A key-value pair to associate with a resource.",
+ "type" : "object",
+ "additionalProperties" : false,
+ "properties" : {
+ "Key" : {
+ "type" : "string",
+ "description" : "The key name of the tag. You can specify a value that is 1 to 127 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "minLength" : 1,
+ "maxLength" : 128,
+ "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$"
+ },
+ "Value" : {
+ "type" : "string",
+ "description" : "The value for the tag. You can specify a value that is 1 to 255 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ",
+ "maxLength" : 256,
+ "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$"
+ }
+ },
+ "required" : [ "Key", "Value" ]
+ },
+ "EndpointName": {
+ "type" : "string",
+ "description" : "The name of the endpoint used to run the monitoring job.",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*",
+ "maxLength" : 63
+ },
+ "JobDefinitionName": {
+ "type" : "string",
+ "description" : "The name of the job definition.",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$",
+ "maxLength" : 63
+ },
+ "ProcessingJobName": {
+ "type" : "string",
+ "description" : "The name of a processing job",
+ "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$",
+ "minLength" : 1,
+ "maxLength" : 63
+ },
+ "MonitoringTimeOffsetString": {
+ "type" : "string",
+ "description" : "The time offsets in ISO duration format",
+ "pattern": "^.?P.*",
+ "minLength" : 1,
+ "maxLength" : 15
+ },
+ "MonitoringGroundTruthS3Input" : {
+ "type" : "object",
+ "additionalProperties" : false,
+ "description": "Ground truth input provided in S3 ",
+ "properties" : {
+ "S3Uri" : {
+ "type" : "string",
+ "description" : "A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.",
+ "pattern": "^(https|s3)://([^/]+)/?(.*)$",
+ "maxLength" : 512
+ }
+ },
+ "required" : [ "S3Uri" ]
+ }
+ },
+ "required" : [ "ModelBiasAppSpecification", "ModelBiasJobInput", "ModelBiasJobOutputConfig", "JobResources", "RoleArn"],
+ "primaryIdentifier" : [ "/properties/JobDefinitionArn" ],
+ "handlers": {
+ "create": {
+ "permissions": [
+ "sagemaker:CreateModelBiasJobDefinition",
+ "sagemaker:DescribeModelBiasJobDefinition",
+ "iam:PassRole"
+ ]
+ },
+ "delete": {
+ "permissions": [
+ "sagemaker:DeleteModelBiasJobDefinition"
+ ]
+ },
+ "read": {
+ "permissions": [
+ "sagemaker:DescribeModelBiasJobDefinition"
+ ]
+ },
+ "update": {
+ "permissions": []
+ }
+ },
+ "readOnlyProperties": [
+ "/properties/CreationTime",
+ "/properties/JobDefinitionArn"
+ ],
+ "createOnlyProperties": [
+ "/properties/JobDefinitionName",
+ "/properties/ModelBiasAppSpecification",
+ "/properties/ModelBiasBaselineConfig",
+ "/properties/ModelBiasJobInput",
+ "/properties/ModelBiasJobOutputConfig",
+ "/properties/JobResources",
+ "/properties/NetworkConfig",
+ "/properties/RoleArn",
+ "/properties/StoppingCondition",
+ "/properties/Tags"
+ ]
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/README.md b/aws-sagemaker-modelbiasjobdefinition/docs/README.md
new file mode 100644
index 0000000..0bbd3e4
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/README.md
@@ -0,0 +1,178 @@
+# AWS::SageMaker::ModelBiasJobDefinition
+
+Resource Type definition for AWS::SageMaker::ModelBiasJobDefinition
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Type" : "AWS::SageMaker::ModelBiasJobDefinition",
+ "Properties" : {
+ "JobDefinitionName " : String ,
+ "ModelBiasBaselineConfig " : ModelBiasBaselineConfig ,
+ "ModelBiasAppSpecification " : ModelBiasAppSpecification ,
+ "ModelBiasJobInput " : ModelBiasJobInput ,
+ "ModelBiasJobOutputConfig " : MonitoringOutputConfig ,
+ "JobResources " : MonitoringResources ,
+ "NetworkConfig " : NetworkConfig ,
+ "RoleArn " : String ,
+ "StoppingCondition " : StoppingCondition ,
+ "Tags " : [ Tag , ... ] ,
+ }
+}
+
+
+### YAML
+
+
+Type: AWS::SageMaker::ModelBiasJobDefinition
+Properties:
+ JobDefinitionName : String
+ ModelBiasBaselineConfig : ModelBiasBaselineConfig
+ ModelBiasAppSpecification : ModelBiasAppSpecification
+ ModelBiasJobInput : ModelBiasJobInput
+ ModelBiasJobOutputConfig : MonitoringOutputConfig
+ JobResources : MonitoringResources
+ NetworkConfig : NetworkConfig
+ RoleArn : String
+ StoppingCondition : StoppingCondition
+ Tags :
+ - Tag
+
+
+## Properties
+
+#### JobDefinitionName
+
+The name of the job definition.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+_Required_: No
+
+_Type_: ModelBiasBaselineConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasAppSpecification
+
+Container image configuration object for the monitoring job.
+
+_Required_: Yes
+
+_Type_: ModelBiasAppSpecification
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasJobInput
+
+The inputs for a monitoring job.
+
+_Required_: Yes
+
+_Type_: ModelBiasJobInput
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ModelBiasJobOutputConfig
+
+The output configuration for monitoring jobs.
+
+_Required_: Yes
+
+_Type_: MonitoringOutputConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### JobResources
+
+Identifies the resources to deploy for a monitoring job.
+
+_Required_: Yes
+
+_Type_: MonitoringResources
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+_Required_: No
+
+_Type_: NetworkConfig
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### RoleArn
+
+The Amazon Resource Name (ARN) of an IAM role that Amazon SageMaker can assume to perform tasks on your behalf.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 20
+
+_Maximum_: 2048
+
+_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+_Required_: No
+
+_Type_: StoppingCondition
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Tags
+
+An array of key-value pairs to apply to this resource.
+
+_Required_: No
+
+_Type_: List of Tag
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Ref
+
+When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the JobDefinitionArn.
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### CreationTime
+
+The time at which the job definition was created.
+
+#### JobDefinitionArn
+
+The Amazon Resource Name (ARN) of job definition.
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/clusterconfig.md b/aws-sagemaker-modelbiasjobdefinition/docs/clusterconfig.md
new file mode 100644
index 0000000..bbfcdde
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/clusterconfig.md
@@ -0,0 +1,70 @@
+# AWS::SageMaker::ModelBiasJobDefinition ClusterConfig
+
+Configuration for the cluster used to run model monitoring jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "InstanceCount " : Integer ,
+ "InstanceType " : String ,
+ "VolumeKmsKeyId " : String ,
+ "VolumeSizeInGB " : Integer
+}
+
+
+### YAML
+
+
+InstanceCount : Integer
+InstanceType : String
+VolumeKmsKeyId : String
+VolumeSizeInGB : Integer
+
+
+## Properties
+
+#### InstanceCount
+
+The number of ML compute instances to use in the model monitoring job. For distributed processing jobs, specify a value greater than 1. The default value is 1.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### InstanceType
+
+The ML compute instance type for the processing job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VolumeKmsKeyId
+
+The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt data on the storage volume attached to the ML compute instance(s) that run the model monitoring job.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VolumeSizeInGB
+
+The size of the ML storage volume, in gigabytes, that you want to provision. You must specify sufficient ML storage for your scenario.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/constraintsresource.md b/aws-sagemaker-modelbiasjobdefinition/docs/constraintsresource.md
new file mode 100644
index 0000000..251642b
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/constraintsresource.md
@@ -0,0 +1,38 @@
+# AWS::SageMaker::ModelBiasJobDefinition ConstraintsResource
+
+The baseline constraints resource for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+S3Uri : String
+
+
+## Properties
+
+#### S3Uri
+
+The Amazon S3 URI.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 1024
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/endpointinput.md b/aws-sagemaker-modelbiasjobdefinition/docs/endpointinput.md
new file mode 100644
index 0000000..7f65c2a
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/endpointinput.md
@@ -0,0 +1,170 @@
+# AWS::SageMaker::ModelBiasJobDefinition EndpointInput
+
+The endpoint for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EndpointName " : String ,
+ "LocalPath " : String ,
+ "S3DataDistributionType " : String ,
+ "S3InputMode " : String ,
+ "StartTimeOffset " : String ,
+ "EndTimeOffset " : String ,
+ "FeaturesAttribute " : String ,
+ "InferenceAttribute " : String ,
+ "ProbabilityAttribute " : String ,
+ "ProbabilityThresholdAttribute " : Double
+}
+
+
+### YAML
+
+
+EndpointName : String
+LocalPath : String
+S3DataDistributionType : String
+S3InputMode : String
+StartTimeOffset : String
+EndTimeOffset : String
+FeaturesAttribute : String
+InferenceAttribute : String
+ProbabilityAttribute : String
+ProbabilityThresholdAttribute : Double
+
+
+## Properties
+
+#### EndpointName
+
+The name of the endpoint used to run the monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### LocalPath
+
+Path to the filesystem where the endpoint data is available to the container.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3DataDistributionType
+
+Whether input data distributed in Amazon S3 is fully replicated or sharded by an S3 key. Defauts to FullyReplicated
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: FullyReplicated
| ShardedByS3Key
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3InputMode
+
+Whether the Pipe or File is used as the input mode for transfering data for the monitoring job. Pipe mode is recommended for large datasets. File mode is useful for small files that fit in memory. Defaults to File.
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: Pipe
| File
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### StartTimeOffset
+
+The time offsets in ISO duration format
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 15
+
+_Pattern_: ^.?P.*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### EndTimeOffset
+
+The time offsets in ISO duration format
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 15
+
+_Pattern_: ^.?P.*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### FeaturesAttribute
+
+JSONpath to locate features in JSONlines dataset
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### InferenceAttribute
+
+Index or JSONpath to locate predicted label(s)
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ProbabilityAttribute
+
+Index or JSONpath to locate probabilities
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ProbabilityThresholdAttribute
+
+_Required_: No
+
+_Type_: Double
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification-environment.md b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification-environment.md
new file mode 100644
index 0000000..9005f6a
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification-environment.md
@@ -0,0 +1,48 @@
+# AWS::SageMaker::ModelBiasJobDefinition ModelBiasAppSpecification Environment
+
+Sets the environment variables in the Docker container
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "[a-zA-Z_][a-zA-Z0-9_]* " : String ,
+ "[\S\s]* " : String
+}
+
+
+### YAML
+
+
+[a-zA-Z_][a-zA-Z0-9_]* : String
+[\S\s]* : String
+
+
+## Properties
+
+#### \[a-zA-Z_][a-zA-Z0-9_]*
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### \[\S\s]*
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 256
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification.md b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification.md
new file mode 100644
index 0000000..77caae4
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasappspecification.md
@@ -0,0 +1,66 @@
+# AWS::SageMaker::ModelBiasJobDefinition ModelBiasAppSpecification
+
+Container image configuration object for the monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "ImageUri " : String ,
+ "ConfigUri " : String ,
+ "Environment " : Environment
+}
+
+
+### YAML
+
+
+ImageUri : String
+ConfigUri : String
+Environment : Environment
+
+
+## Properties
+
+#### ImageUri
+
+The container image to be run by the monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 255
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ConfigUri
+
+The S3 URI to an analysis configuration file
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 255
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Environment
+
+Sets the environment variables in the Docker container
+
+_Required_: No
+
+_Type_: Environment
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasbaselineconfig.md b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasbaselineconfig.md
new file mode 100644
index 0000000..2564259
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasbaselineconfig.md
@@ -0,0 +1,52 @@
+# AWS::SageMaker::ModelBiasJobDefinition ModelBiasBaselineConfig
+
+Baseline configuration used to validate that the data conforms to the specified constraints and statistics.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "BaseliningJobName " : String ,
+ "ConstraintsResource " : ConstraintsResource
+}
+
+
+### YAML
+
+
+BaseliningJobName : String
+ConstraintsResource : ConstraintsResource
+
+
+## Properties
+
+#### BaseliningJobName
+
+The name of a processing job
+
+_Required_: No
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 63
+
+_Pattern_: ^[a-zA-Z0-9](-*[a-zA-Z0-9])*$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ConstraintsResource
+
+The baseline constraints resource for a monitoring job.
+
+_Required_: No
+
+_Type_: ConstraintsResource
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasjobinput.md b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasjobinput.md
new file mode 100644
index 0000000..149b233
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/modelbiasjobinput.md
@@ -0,0 +1,46 @@
+# AWS::SageMaker::ModelBiasJobDefinition ModelBiasJobInput
+
+The inputs for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EndpointInput " : EndpointInput ,
+ "GroundTruthS3Input " : MonitoringGroundTruthS3Input
+}
+
+
+### YAML
+
+
+EndpointInput : EndpointInput
+GroundTruthS3Input : MonitoringGroundTruthS3Input
+
+
+## Properties
+
+#### EndpointInput
+
+The endpoint for a monitoring job.
+
+_Required_: Yes
+
+_Type_: EndpointInput
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### GroundTruthS3Input
+
+Ground truth input provided in S3
+
+_Required_: Yes
+
+_Type_: MonitoringGroundTruthS3Input
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/monitoringgroundtruths3input.md b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringgroundtruths3input.md
new file mode 100644
index 0000000..4585b7b
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringgroundtruths3input.md
@@ -0,0 +1,38 @@
+# AWS::SageMaker::ModelBiasJobDefinition MonitoringGroundTruthS3Input
+
+Ground truth input provided in S3
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+S3Uri : String
+
+
+## Properties
+
+#### S3Uri
+
+A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 512
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutput.md b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutput.md
new file mode 100644
index 0000000..5ba89c1
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutput.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::ModelBiasJobDefinition MonitoringOutput
+
+The output object for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "S3Output " : S3Output
+}
+
+
+### YAML
+
+
+S3Output : S3Output
+
+
+## Properties
+
+#### S3Output
+
+Information about where and how to store the results of a monitoring job.
+
+_Required_: Yes
+
+_Type_: S3Output
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutputconfig.md b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutputconfig.md
new file mode 100644
index 0000000..0b975d4
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringoutputconfig.md
@@ -0,0 +1,55 @@
+# AWS::SageMaker::ModelBiasJobDefinition MonitoringOutputConfig
+
+The output configuration for monitoring jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "KmsKeyId " : String ,
+ "MonitoringOutputs " : [ MonitoringOutput , ... ]
+}
+
+
+### YAML
+
+
+KmsKeyId : String
+MonitoringOutputs :
+ - MonitoringOutput
+
+
+## Properties
+
+#### KmsKeyId
+
+The AWS Key Management Service (AWS KMS) key that Amazon SageMaker uses to encrypt the model artifacts at rest using Amazon S3 server-side encryption.
+
+_Required_: No
+
+_Type_: String
+
+_Maximum_: 2048
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### MonitoringOutputs
+
+Monitoring outputs for monitoring jobs. This is where the output of the periodic monitoring jobs is uploaded.
+
+_Required_: Yes
+
+_Type_: List of MonitoringOutput
+
+_Minimum_: 1
+
+_Maximum_: 1
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/monitoringresources.md b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringresources.md
new file mode 100644
index 0000000..2043ecf
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/monitoringresources.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::ModelBiasJobDefinition MonitoringResources
+
+Identifies the resources to deploy for a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "ClusterConfig " : ClusterConfig
+}
+
+
+### YAML
+
+
+ClusterConfig : ClusterConfig
+
+
+## Properties
+
+#### ClusterConfig
+
+Configuration for the cluster used to run model monitoring jobs.
+
+_Required_: Yes
+
+_Type_: ClusterConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/networkconfig.md b/aws-sagemaker-modelbiasjobdefinition/docs/networkconfig.md
new file mode 100644
index 0000000..74f763e
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/networkconfig.md
@@ -0,0 +1,58 @@
+# AWS::SageMaker::ModelBiasJobDefinition NetworkConfig
+
+Networking options for a job, such as network traffic encryption between containers, whether to allow inbound and outbound network calls to and from containers, and the VPC subnets and security groups to use for VPC-enabled jobs.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "EnableInterContainerTrafficEncryption " : Boolean ,
+ "EnableNetworkIsolation " : Boolean ,
+ "VpcConfig " : VpcConfig
+}
+
+
+### YAML
+
+
+EnableInterContainerTrafficEncryption : Boolean
+EnableNetworkIsolation : Boolean
+VpcConfig : VpcConfig
+
+
+## Properties
+
+#### EnableInterContainerTrafficEncryption
+
+Whether to encrypt all communications between distributed processing jobs. Choose True to encrypt communications. Encryption provides greater security for distributed processing jobs, but the processing might take longer.
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### EnableNetworkIsolation
+
+Whether to allow inbound and outbound network calls to and from the containers used for the processing job.
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### VpcConfig
+
+Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.
+
+_Required_: No
+
+_Type_: VpcConfig
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/s3output.md b/aws-sagemaker-modelbiasjobdefinition/docs/s3output.md
new file mode 100644
index 0000000..1a3570b
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/s3output.md
@@ -0,0 +1,68 @@
+# AWS::SageMaker::ModelBiasJobDefinition S3Output
+
+Information about where and how to store the results of a monitoring job.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "LocalPath " : String ,
+ "S3UploadMode " : String ,
+ "S3Uri " : String
+}
+
+
+### YAML
+
+
+LocalPath : String
+S3UploadMode : String
+S3Uri : String
+
+
+## Properties
+
+#### LocalPath
+
+The local path to the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job. LocalPath is an absolute path for the output data.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: .*
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3UploadMode
+
+Whether to upload the results of the monitoring job continuously or after the job completes.
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: Continuous
| EndOfJob
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### S3Uri
+
+A URI that identifies the Amazon S3 storage location where Amazon SageMaker saves the results of a monitoring job.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 512
+
+_Pattern_: ^(https|s3)://([^/]+)/?(.*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/stoppingcondition.md b/aws-sagemaker-modelbiasjobdefinition/docs/stoppingcondition.md
new file mode 100644
index 0000000..0c55cc8
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/stoppingcondition.md
@@ -0,0 +1,34 @@
+# AWS::SageMaker::ModelBiasJobDefinition StoppingCondition
+
+Specifies a time limit for how long the monitoring job is allowed to run.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "MaxRuntimeInSeconds " : Integer
+}
+
+
+### YAML
+
+
+MaxRuntimeInSeconds : Integer
+
+
+## Properties
+
+#### MaxRuntimeInSeconds
+
+The maximum runtime allowed in seconds.
+
+_Required_: Yes
+
+_Type_: Integer
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/tag.md b/aws-sagemaker-modelbiasjobdefinition/docs/tag.md
new file mode 100644
index 0000000..b0026f4
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/tag.md
@@ -0,0 +1,56 @@
+# AWS::SageMaker::ModelBiasJobDefinition Tag
+
+A key-value pair to associate with a resource.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "Key " : String ,
+ "Value " : String
+}
+
+
+### YAML
+
+
+Key : String
+Value : String
+
+
+## Properties
+
+#### Key
+
+The key name of the tag. You can specify a value that is 1 to 127 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: Yes
+
+_Type_: String
+
+_Minimum_: 1
+
+_Maximum_: 128
+
+_Pattern_: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Value
+
+The value for the tag. You can specify a value that is 1 to 255 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -.
+
+_Required_: Yes
+
+_Type_: String
+
+_Maximum_: 256
+
+_Pattern_: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/docs/vpcconfig.md b/aws-sagemaker-modelbiasjobdefinition/docs/vpcconfig.md
new file mode 100644
index 0000000..e9ef7c3
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/docs/vpcconfig.md
@@ -0,0 +1,48 @@
+# AWS::SageMaker::ModelBiasJobDefinition VpcConfig
+
+Specifies a VPC that your training jobs and hosted models have access to. Control access to and from your training and model containers by configuring the VPC.
+
+## Syntax
+
+To declare this entity in your AWS CloudFormation template, use the following syntax:
+
+### JSON
+
+
+{
+ "SecurityGroupIds " : [ String, ... ] ,
+ "Subnets " : [ String, ... ]
+}
+
+
+### YAML
+
+
+SecurityGroupIds :
+ - String
+Subnets :
+ - String
+
+
+## Properties
+
+#### SecurityGroupIds
+
+The VPC security group IDs, in the form sg-xxxxxxxx. Specify the security groups for the VPC that is specified in the Subnets field.
+
+_Required_: Yes
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Subnets
+
+The ID of the subnets in the VPC to which you want to connect to your monitoring jobs.
+
+_Required_: Yes
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
diff --git a/aws-sagemaker-modelbiasjobdefinition/lombok.config b/aws-sagemaker-modelbiasjobdefinition/lombok.config
new file mode 100644
index 0000000..7a21e88
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/aws-sagemaker-modelbiasjobdefinition/pom.xml b/aws-sagemaker-modelbiasjobdefinition/pom.xml
new file mode 100644
index 0000000..d94f1ef
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/pom.xml
@@ -0,0 +1,210 @@
+
+
+ 4.0.0
+
+ software.amazon.sagemaker.modelbiasjobdefinition
+ aws-sagemaker-modelbiasjobdefinition-handler
+ aws-sagemaker-modelbiasjobdefinition-handler
+ 1.0-SNAPSHOT
+ jar
+
+
+ 1.8
+ 1.8
+ UTF-8
+ UTF-8
+
+
+
+
+
+ software.amazon.awssdk
+ sagemaker
+ 2.15.50
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ [2.0.0, 3.0.0)
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.4
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ 3.12.2
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.5.0-M1
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 2.26.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 2.26.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ -Xlint:all,-options,-processing
+ -Werror
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.3
+
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+ generate
+ generate-sources
+
+ exec
+
+
+ cfn
+ generate
+ ${project.basedir}
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.basedir}/target/generated-sources/rpdk
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.4
+
+
+ maven-surefire-plugin
+ 3.0.0-M3
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.4
+
+
+ **/BaseConfiguration*
+ **/BaseHandler*
+ **/HandlerWrapper*
+ **/ResourceModel*
+
+
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+ jacoco-check
+
+ check
+
+
+
+
+ PACKAGE
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.4
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.4
+
+
+
+
+
+
+
+
+
+
+
+ ${project.basedir}
+
+ aws-sagemaker-modelbiasjobdefinition.json
+
+
+
+
+
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/resource-role.yaml b/aws-sagemaker-modelbiasjobdefinition/resource-role.yaml
new file mode 100644
index 0000000..4cf5e72
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/resource-role.yaml
@@ -0,0 +1,34 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+ This CloudFormation template creates a role assumed by CloudFormation
+ during CRUDL operations to mutate resources on behalf of the customer.
+
+Resources:
+ ExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ MaxSessionDuration: 8400
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: resources.cloudformation.amazonaws.com
+ Action: sts:AssumeRole
+ Path: "/"
+ Policies:
+ - PolicyName: ResourceTypePolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - "iam:PassRole"
+ - "sagemaker:CreateModelBiasJobDefinition"
+ - "sagemaker:DeleteModelBiasJobDefinition"
+ - "sagemaker:DescribeModelBiasJobDefinition"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/BaseHandlerStd.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/BaseHandlerStd.java
new file mode 100644
index 0000000..b3202d7
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/BaseHandlerStd.java
@@ -0,0 +1,38 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+/**
+ * Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers
+ */
+public abstract class BaseHandlerStd extends BaseHandler {
+
+ protected static final String MODEL_BIAS_ARN_SUBSTRING = ":model-bias-job-definition/";
+
+ @Override
+ public final ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+ return handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ proxy.newProxy(ClientBuilder::getClient),
+ logger
+ );
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger);
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CallbackContext.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CallbackContext.java
new file mode 100644
index 0000000..cfc91ca
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CallbackContext.java
@@ -0,0 +1,7 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.cloudformation.proxy.StdCallbackContext;
+
+@lombok.EqualsAndHashCode(callSuper = true)
+public class CallbackContext extends StdCallbackContext {
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ClientBuilder.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ClientBuilder.java
new file mode 100644
index 0000000..6a19ba1
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ClientBuilder.java
@@ -0,0 +1,12 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+
+/**
+ * Provides APIs to build service client.
+ */
+public class ClientBuilder {
+ public static SageMakerClient getClient() {
+ return SageMakerClient.builder().build();
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Configuration.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Configuration.java
new file mode 100644
index 0000000..03f1cd8
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Configuration.java
@@ -0,0 +1,7 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+class Configuration extends BaseConfiguration {
+ public Configuration() {
+ super("aws-sagemaker-modelbiasjobdefinition.json");
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandler.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandler.java
new file mode 100644
index 0000000..ce7ada9
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandler.java
@@ -0,0 +1,108 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.CreateModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.CreateModelBiasJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+import java.util.Random;
+
+
+public class CreateHandler extends BaseHandlerStd {
+ public static final int ALLOWED_JOB_DEFINITION_NAME_LENGTH = 20;
+ public static final String CFN_RESOURCE_NAME_PREFIX = "CFN";
+ public static final int GUID_LENGTH = 12;
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = generateParameterName(request.getLogicalResourceIdentifier(),
+ request.getClientRequestToken());
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate("AWS-SageMaker-ModelBiasJobDefinition::Create", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToCreateRequest)
+ .makeServiceCall(this::createResource)
+ .progress())
+ .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
+ }
+
+ /**
+ * Client invocation of the create request through the proxyClient, which is already initialised with
+ * caller credentials, region and retry settings
+ * @param awsRequest the aws service request to create a resource
+ * @param proxyClient the aws service client to make the call
+ * @return awsResponse create resource response
+ */
+ private CreateModelBiasJobDefinitionResponse createResource(
+ final CreateModelBiasJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ CreateModelBiasJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createModelBiasJobDefinition);
+ } catch (final ResourceInUseException e) {
+ throw new ResourceAlreadyExistsException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName());
+ } catch (final AwsServiceException e) {
+
+ // The exception thrown due to validation failure does not have error code set,
+ // hence we need to check it using error message
+ if(StringUtils.isNotBlank(e.getMessage()) && e.getMessage().contains("validation error detected")) {
+ throw new CfnInvalidRequestException(Action.CREATE.toString(), e);
+ }
+ Translator.throwCfnException(Action.CREATE.toString(), e);
+ }
+
+ return response;
+ }
+
+ // We support this special use case of auto-generating names only for CloudFormation.
+ // Name format: Prefix - logical resource id - randomString
+ private String generateParameterName(final String logicalResourceId, final String clientRequestToken) {
+ StringBuilder sb = new StringBuilder();
+ int endIndex = logicalResourceId.length() > ALLOWED_JOB_DEFINITION_NAME_LENGTH
+ ? ALLOWED_JOB_DEFINITION_NAME_LENGTH : logicalResourceId.length();
+
+ sb.append(CFN_RESOURCE_NAME_PREFIX);
+ sb.append("-");
+ sb.append(logicalResourceId.substring(0, endIndex));
+ sb.append("-");
+
+ sb.append(RandomStringUtils.random(
+ GUID_LENGTH,
+ 0,
+ 0,
+ true,
+ true,
+ null,
+ new Random(clientRequestToken.hashCode())));
+ return sb.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandler.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandler.java
new file mode 100644
index 0000000..c2bb99a
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandler.java
@@ -0,0 +1,73 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DeleteModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DeleteModelBiasJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class DeleteHandler extends BaseHandlerStd {
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = Utils.getResourceNameFromArn(model.getJobDefinitionArn(), MODEL_BIAS_ARN_SUBSTRING);
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return ProgressEvent.progress(model, callbackContext)
+ .then(progress ->
+ proxy.initiate("AWS-SageMaker-ModelBiasJobDefinition::Delete", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToDeleteRequest)
+ .makeServiceCall(this::deleteResource)
+ .done(awsResponse -> ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .build()));
+ }
+
+ /**
+ * Implement client invocation of the delete request through the proxyClient.
+ *
+ * @param awsRequest the aws service request to delete a resource
+ * @param proxyClient the aws service client to make the call
+ * @return delete resource response
+ */
+ private DeleteModelBiasJobDefinitionResponse deleteResource(
+ final DeleteModelBiasJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient) {
+
+ DeleteModelBiasJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteModelBiasJobDefinition);
+ } catch (ResourceNotFoundException e) {
+ // NotFound responded from Delete handler will be considered as success by CFN backend service.
+ // This is to handle out of stack resource deletion (https://sage.amazon.com/questions/896677)
+ throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName());
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.DELETE.toString(), e);
+ }
+
+ return response;
+ }
+}
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandler.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandler.java
new file mode 100644
index 0000000..73b4aec
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandler.java
@@ -0,0 +1,81 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException;
+import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class ReadHandler extends BaseHandlerStd {
+
+ private Logger logger;
+
+ protected ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger) {
+
+ this.logger = logger;
+
+ final ResourceModel model = request.getDesiredResourceState();
+
+ //Set job definition name if absent
+ String jobDefinitionName = model.getJobDefinitionName();
+ if(StringUtils.isEmpty(jobDefinitionName)){
+ jobDefinitionName = Utils.getResourceNameFromArn(model.getJobDefinitionArn(), MODEL_BIAS_ARN_SUBSTRING);
+ model.setJobDefinitionName(jobDefinitionName);
+ }
+
+ return proxy.initiate("AWS-SageMaker-ModelBiasJobDefinition::Read", proxyClient, model, callbackContext)
+ .translateToServiceRequest(TranslatorForRequest::translateToReadRequest)
+ .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest, sdkProxyClient, model))
+ .done(this::constructResourceModelFromResponse);
+ }
+
+ /**
+ * Client invocation of the read request through the proxyClient, which is already initialised with
+ * caller credentials, correct region and retry settings
+ * @param awsRequest the aws service request to describe a resource
+ * @param proxyClient the aws service client to make the call
+ * @return describe resource response
+ */
+ private DescribeModelBiasJobDefinitionResponse readResource(
+ final DescribeModelBiasJobDefinitionRequest awsRequest,
+ final ProxyClient proxyClient,
+ final ResourceModel model) {
+
+ DescribeModelBiasJobDefinitionResponse response = null;
+ try {
+ response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeModelBiasJobDefinition);
+ } catch (final ResourceNotFoundException e) {
+ throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.jobDefinitionName(), e);
+ } catch (final AwsServiceException e) {
+ Translator.throwCfnException(Action.READ.toString(), e);
+ }
+
+
+ return response;
+ }
+
+ /**
+ * Implement client invocation of the read request through the proxyClient, which is already
+ * initialised with caller credentials, correct region and retry settings
+ *
+ * @param awsResponse the aws service describe resource response
+ * @return progressEvent indicating success, in progress with delay callback or failed state
+ */
+ private ProgressEvent constructResourceModelFromResponse(
+ final DescribeModelBiasJobDefinitionResponse awsResponse) {
+ return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse));
+ }
+}
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Translator.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Translator.java
new file mode 100644
index 0000000..05f6726
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Translator.java
@@ -0,0 +1,61 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.cloudformation.exceptions.CfnAccessDeniedException;
+import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
+import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
+import software.amazon.cloudformation.exceptions.CfnNotFoundException;
+import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException;
+import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException;
+import software.amazon.cloudformation.exceptions.CfnThrottlingException;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * This class contains translation methods for object other than api request/response.
+ * It also contains common methods required by other translators.
+ */
+public class Translator {
+
+ /**
+ * Throws Cfn exception corresponding to error code of the given exception.
+ *
+ * @param operation
+ * @param e exception
+ */
+ public static void throwCfnException(final String operation, final AwsServiceException e) {
+ if(e.awsErrorDetails() != null && StringUtils.isNotBlank(e.awsErrorDetails().errorCode())) {
+ switch (e.awsErrorDetails().errorCode()) {
+ case "UnauthorizedOperation":
+ throw new CfnAccessDeniedException(operation, e);
+ case "InvalidParameter":
+ case "InvalidParameterValue":
+ case "ValidationError":
+ throw new CfnInvalidRequestException(operation, e);
+ case "InternalError":
+ case "ServiceUnavailable":
+ throw new CfnServiceInternalErrorException(operation, e);
+ case "ResourceLimitExceeded":
+ throw new CfnServiceLimitExceededException(e);
+ case "ResourceNotFound":
+ throw new CfnNotFoundException(e);
+ case "ThrottlingException":
+ throw new CfnThrottlingException(operation, e);
+ default:
+ throw new CfnGeneralServiceException(operation, e);
+ }
+ }
+
+ throw new CfnGeneralServiceException(operation, e);
+ }
+
+ public static Stream streamOfOrEmpty(final Collection collection) {
+ return Optional.ofNullable(collection)
+ .map(Collection::stream)
+ .orElseGet(Stream::empty);
+ }
+
+}
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForRequest.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForRequest.java
new file mode 100644
index 0000000..f0aca3a
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForRequest.java
@@ -0,0 +1,197 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.model.CreateModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasAppSpecification;
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasBaselineConfig;
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasJobInput;
+import software.amazon.awssdk.services.sagemaker.model.DeleteModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionRequest;
+import software.amazon.awssdk.services.sagemaker.model.EndpointInput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringNetworkConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutputConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringResources;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringS3Output;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringGroundTruthS3Input;
+import software.amazon.awssdk.services.sagemaker.model.Tag;
+import software.amazon.awssdk.services.sagemaker.model.VpcConfig;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * This class is a centralized placeholder for
+ * - api request construction
+ * - object translation to/from aws sdk
+ * - resource model construction for handlers like read/list
+ */
+final class TranslatorForRequest {
+
+ private TranslatorForRequest() {}
+
+ /**
+ * Request to create a resource
+ * @param model resource model
+ * @return createModelBiasJobDefinitionRequest - service request to create a resource
+ */
+ static CreateModelBiasJobDefinitionRequest translateToCreateRequest(final ResourceModel model) {
+ return CreateModelBiasJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .modelBiasAppSpecification(translate(model.getModelBiasAppSpecification()))
+ .modelBiasBaselineConfig(translate(model.getModelBiasBaselineConfig()))
+ .modelBiasJobInput(translate(model.getModelBiasJobInput()))
+ .modelBiasJobOutputConfig(translate(model.getModelBiasJobOutputConfig()))
+ .jobResources(translate(model.getJobResources()))
+ .networkConfig(translate(model.getNetworkConfig()))
+ .roleArn(model.getRoleArn())
+ .stoppingCondition(translate(model.getStoppingCondition()))
+ .tags(Translator.streamOfOrEmpty(model.getTags())
+ .map(curTag -> Tag.builder()
+ .key(curTag.getKey())
+ .value(curTag.getValue())
+ .build())
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ /**
+ * Request to read a resource
+ * @param model resource model
+ * @return describeModelBiasJobDefinitionRequest - the aws service request to describe a resource
+ */
+ static DescribeModelBiasJobDefinitionRequest translateToReadRequest(final ResourceModel model) {
+ return DescribeModelBiasJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .build();
+ }
+
+ /**
+ * Request to delete a resource
+ * @param model resource model
+ * @return deleteModelBiasJobDefinitionRequest the aws service request to delete a resource
+ */
+ static DeleteModelBiasJobDefinitionRequest translateToDeleteRequest(final ResourceModel model) {
+ return DeleteModelBiasJobDefinitionRequest.builder()
+ .jobDefinitionName(model.getJobDefinitionName())
+ .build();
+ }
+
+ static ModelBiasAppSpecification translate(final software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasAppSpecification appSpec) {
+ return appSpec == null ? null : ModelBiasAppSpecification.builder()
+ .imageUri(appSpec.getImageUri())
+ .configUri(appSpec.getConfigUri())
+ .environment(translateMapOfObjectsToMapOfStrings(appSpec.getEnvironment()))
+ .build();
+ }
+
+ static ModelBiasBaselineConfig translate(final software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig baselineConfig) {
+ return baselineConfig == null ? null : ModelBiasBaselineConfig.builder()
+ .baseliningJobName(baselineConfig.getBaseliningJobName())
+ .constraintsResource(translate(baselineConfig.getConstraintsResource()))
+ .build();
+ }
+
+ static MonitoringConstraintsResource translate(final software.amazon.sagemaker.modelbiasjobdefinition.ConstraintsResource constraintsResource) {
+ return constraintsResource == null ? null : MonitoringConstraintsResource.builder().s3Uri(constraintsResource.getS3Uri()).build();
+ }
+
+ static ModelBiasJobInput translate(final software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasJobInput jobInput) {
+ return jobInput == null ? null : ModelBiasJobInput.builder()
+ .endpointInput(translate(jobInput.getEndpointInput()))
+ .groundTruthS3Input(translate(jobInput.getGroundTruthS3Input()))
+ .build();
+ }
+ static EndpointInput translate(final software.amazon.sagemaker.modelbiasjobdefinition.EndpointInput endpointInput) {
+ return endpointInput == null ? null : EndpointInput.builder()
+ .endpointName(endpointInput.getEndpointName())
+ .localPath(endpointInput.getLocalPath())
+ .s3DataDistributionType(endpointInput.getS3DataDistributionType())
+ .s3InputMode(endpointInput.getS3InputMode())
+ .featuresAttribute(endpointInput.getFeaturesAttribute())
+ .inferenceAttribute(endpointInput.getInferenceAttribute())
+ .probabilityAttribute(endpointInput.getProbabilityAttribute())
+ .probabilityThresholdAttribute(endpointInput.getProbabilityThresholdAttribute())
+ .startTimeOffset(endpointInput.getStartTimeOffset())
+ .endTimeOffset(endpointInput.getEndTimeOffset())
+ .build();
+ }
+ static MonitoringOutputConfig translate(final software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutputConfig outputConfig) {
+ return outputConfig == null? null : MonitoringOutputConfig.builder()
+ .kmsKeyId(outputConfig.getKmsKeyId())
+ .monitoringOutputs(translateOutput(outputConfig.getMonitoringOutputs()))
+ .build();
+ }
+
+ static List translateOutput(final List monitoringOutputs) {
+ return monitoringOutputs == null ? null : monitoringOutputs.stream()
+ .map(monitoringOutput -> translate(monitoringOutput))
+ .collect(Collectors.toList());
+ }
+
+ static MonitoringOutput translate(final software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutput monitoringOutput) {
+ return monitoringOutput == null ? null : MonitoringOutput.builder()
+ .s3Output(translate(monitoringOutput.getS3Output()))
+ .build();
+ }
+
+ static MonitoringS3Output translate(final software.amazon.sagemaker.modelbiasjobdefinition.S3Output s3Output) {
+ return s3Output == null? null : MonitoringS3Output.builder()
+ .localPath(s3Output.getLocalPath())
+ .s3UploadMode(s3Output.getS3UploadMode())
+ .s3Uri(s3Output.getS3Uri())
+ .build();
+ }
+
+ static MonitoringResources translate(final software.amazon.sagemaker.modelbiasjobdefinition.MonitoringResources monitoringResources) {
+ return monitoringResources == null? null : MonitoringResources.builder()
+ .clusterConfig(translate(monitoringResources.getClusterConfig()))
+ .build();
+ }
+
+ static MonitoringClusterConfig translate(final software.amazon.sagemaker.modelbiasjobdefinition.ClusterConfig clusterConfig) {
+ return clusterConfig == null? null : MonitoringClusterConfig.builder()
+ .instanceCount(clusterConfig.getInstanceCount())
+ .instanceType(clusterConfig.getInstanceType())
+ .volumeKmsKeyId(clusterConfig.getVolumeKmsKeyId())
+ .volumeSizeInGB(clusterConfig.getVolumeSizeInGB())
+ .build();
+ }
+
+ static MonitoringNetworkConfig translate(final software.amazon.sagemaker.modelbiasjobdefinition.NetworkConfig networkConfig) {
+ return networkConfig == null? null : MonitoringNetworkConfig.builder()
+ .enableInterContainerTrafficEncryption(networkConfig.getEnableInterContainerTrafficEncryption())
+ .enableNetworkIsolation(networkConfig.getEnableNetworkIsolation())
+ .vpcConfig(translate(networkConfig.getVpcConfig()))
+ .build();
+ }
+
+ static VpcConfig translate(final software.amazon.sagemaker.modelbiasjobdefinition.VpcConfig vpcConfig) {
+ return vpcConfig == null? null : VpcConfig.builder()
+ .securityGroupIds(vpcConfig.getSecurityGroupIds())
+ .subnets(vpcConfig.getSubnets())
+ .build();
+ }
+
+ static MonitoringStoppingCondition translate(final software.amazon.sagemaker.modelbiasjobdefinition.StoppingCondition stoppingCondition) {
+ return stoppingCondition == null? null : MonitoringStoppingCondition.builder()
+ .maxRuntimeInSeconds(stoppingCondition.getMaxRuntimeInSeconds())
+ .build();
+ }
+
+ static MonitoringGroundTruthS3Input translate(final software.amazon.sagemaker.modelbiasjobdefinition.MonitoringGroundTruthS3Input s3Input) {
+ return s3Input == null? null : MonitoringGroundTruthS3Input.builder()
+ .s3Uri(s3Input.getS3Uri())
+ .build();
+ }
+
+ static Map translateMapOfObjectsToMapOfStrings(final Map mapOfObjects) {
+ return mapOfObjects == null ? null : mapOfObjects.entrySet().stream().collect(
+ Collectors.toMap(Map.Entry::getKey, e -> (String)e.getValue())
+ );
+ }
+
+}
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForResponse.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForResponse.java
new file mode 100644
index 0000000..9d467d6
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorForResponse.java
@@ -0,0 +1,173 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasAppSpecification;
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasBaselineConfig;
+import software.amazon.awssdk.services.sagemaker.model.ModelBiasJobInput;
+import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionResponse;
+import software.amazon.awssdk.services.sagemaker.model.EndpointInput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringNetworkConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutput;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringOutputConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringResources;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringS3Output;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition;
+import software.amazon.awssdk.services.sagemaker.model.VpcConfig;
+import software.amazon.awssdk.services.sagemaker.model.MonitoringGroundTruthS3Input;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class TranslatorForResponse {
+
+ private TranslatorForResponse() {
+ }
+
+ /**
+ * Translates resource object from sdk into a resource model
+ *
+ * @param awsResponse the aws service describe resource response
+ * @return model resource model
+ */
+ static ResourceModel translateFromReadResponse(final DescribeModelBiasJobDefinitionResponse awsResponse) {
+ return ResourceModel.builder()
+ .jobDefinitionArn(awsResponse.jobDefinitionArn())
+ .jobDefinitionName(awsResponse.jobDefinitionName())
+ .creationTime(awsResponse.creationTime().toString())
+ .modelBiasBaselineConfig(translate(awsResponse.modelBiasBaselineConfig()))
+ .modelBiasAppSpecification(translate(awsResponse.modelBiasAppSpecification()))
+ .modelBiasJobInput(translate(awsResponse.modelBiasJobInput()))
+ .modelBiasJobOutputConfig(translate(awsResponse.modelBiasJobOutputConfig()))
+ .jobResources(translate(awsResponse.jobResources()))
+ .networkConfig(translate(awsResponse.networkConfig()))
+ .roleArn(awsResponse.roleArn())
+ .stoppingCondition(translate(awsResponse.stoppingCondition()))
+ .build();
+ }
+
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig translate(
+ final ModelBiasBaselineConfig baselineConfig) {
+ return baselineConfig == null? null : software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig.builder()
+ .baseliningJobName(baselineConfig.baseliningJobName())
+ .constraintsResource(translate(baselineConfig.constraintsResource()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.ConstraintsResource translate(
+ final MonitoringConstraintsResource constraintsResource) {
+ return constraintsResource == null? null : software.amazon.sagemaker.modelbiasjobdefinition.ConstraintsResource.builder()
+ .s3Uri(constraintsResource.s3Uri())
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasAppSpecification translate(
+ final ModelBiasAppSpecification monitoringAppSpec) {
+ return monitoringAppSpec == null ? null : software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasAppSpecification.builder()
+ .imageUri(monitoringAppSpec.imageUri())
+ .configUri(monitoringAppSpec.configUri())
+ .environment(translateMapOfStringsMapOfObjects(monitoringAppSpec.environment()))
+ .build();
+ }
+
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasJobInput translate(final ModelBiasJobInput monitoringInput) {
+ return monitoringInput == null ? null : software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasJobInput.builder()
+ .endpointInput(translate(monitoringInput.endpointInput()))
+ .groundTruthS3Input(translate(monitoringInput.groundTruthS3Input()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.EndpointInput translate(final EndpointInput endpointInput) {
+ return endpointInput == null ? null : software.amazon.sagemaker.modelbiasjobdefinition.EndpointInput.builder()
+ .endpointName(endpointInput.endpointName())
+ .localPath(endpointInput.localPath())
+ .s3DataDistributionType(endpointInput.s3DataDistributionType().toString())
+ .s3InputMode(endpointInput.s3InputMode().toString())
+ .featuresAttribute(endpointInput.featuresAttribute())
+ .inferenceAttribute(endpointInput.inferenceAttribute())
+ .probabilityAttribute(endpointInput.probabilityAttribute())
+ .probabilityThresholdAttribute(endpointInput.probabilityThresholdAttribute())
+ .startTimeOffset(endpointInput.startTimeOffset())
+ .endTimeOffset(endpointInput.endTimeOffset())
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutputConfig translate(final MonitoringOutputConfig outputConfig) {
+ return outputConfig == null? null : software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutputConfig.builder()
+ .kmsKeyId(outputConfig.kmsKeyId())
+ .monitoringOutputs(translateOutput(outputConfig.monitoringOutputs()))
+ .build();
+ }
+
+ static List translateOutput(final List monitoringOutputs) {
+ return monitoringOutputs == null ? null : monitoringOutputs.stream()
+ .map(monitoringOutput -> translate(monitoringOutput))
+ .collect(Collectors.toList());
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutput translate(final MonitoringOutput monitoringOutput) {
+ return monitoringOutput == null ? null : software.amazon.sagemaker.modelbiasjobdefinition.MonitoringOutput.builder()
+ .s3Output(translate(monitoringOutput.s3Output()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.S3Output translate(final MonitoringS3Output s3Output) {
+ return s3Output == null? null : software.amazon.sagemaker.modelbiasjobdefinition.S3Output.builder()
+ .localPath(s3Output.localPath())
+ .s3UploadMode(s3Output.s3UploadMode().toString())
+ .s3Uri(s3Output.s3Uri())
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.MonitoringResources translate(final MonitoringResources monitoringResources) {
+ return monitoringResources == null? null : software.amazon.sagemaker.modelbiasjobdefinition.MonitoringResources.builder()
+ .clusterConfig(translate(monitoringResources.clusterConfig()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.ClusterConfig translate(final MonitoringClusterConfig clusterConfig) {
+ return clusterConfig == null? null : software.amazon.sagemaker.modelbiasjobdefinition.ClusterConfig.builder()
+ .instanceCount(clusterConfig.instanceCount())
+ .instanceType(clusterConfig.instanceType().toString())
+ .volumeKmsKeyId(clusterConfig.volumeKmsKeyId())
+ .volumeSizeInGB(clusterConfig.volumeSizeInGB())
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.NetworkConfig translate(final MonitoringNetworkConfig networkConfig) {
+ return networkConfig == null? null : software.amazon.sagemaker.modelbiasjobdefinition.NetworkConfig.builder()
+ .enableInterContainerTrafficEncryption(networkConfig.enableInterContainerTrafficEncryption())
+ .enableNetworkIsolation(networkConfig.enableNetworkIsolation())
+ .vpcConfig(translate(networkConfig.vpcConfig()))
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.VpcConfig translate(final VpcConfig vpcConfig) {
+ return vpcConfig == null? null : software.amazon.sagemaker.modelbiasjobdefinition.VpcConfig.builder()
+ .securityGroupIds(vpcConfig.securityGroupIds())
+ .subnets(vpcConfig.subnets())
+ .build();
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.StoppingCondition translate(final MonitoringStoppingCondition stoppingCondition) {
+ return stoppingCondition == null? null : software.amazon.sagemaker.modelbiasjobdefinition.StoppingCondition.builder()
+ .maxRuntimeInSeconds(stoppingCondition.maxRuntimeInSeconds())
+ .build();
+ }
+
+ static Map translateMapOfStringsMapOfObjects(final Map mapOfStrings) {
+ return mapOfStrings == null ? null : mapOfStrings.entrySet().stream().collect(
+ Collectors.toMap(Map.Entry::getKey, e -> (Object)e.getValue())
+ );
+ }
+
+ static software.amazon.sagemaker.modelbiasjobdefinition.MonitoringGroundTruthS3Input translate(final MonitoringGroundTruthS3Input s3Input) {
+ return s3Input == null? null : software.amazon.sagemaker.modelbiasjobdefinition.MonitoringGroundTruthS3Input.builder()
+ .s3Uri(s3Input.s3Uri())
+ .build();
+ }
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/UpdateHandler.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/UpdateHandler.java
new file mode 100644
index 0000000..c9e4fa8
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/UpdateHandler.java
@@ -0,0 +1,27 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+public class UpdateHandler extends BaseHandler {
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final ResourceHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger) {
+
+ logger.log(String.format("%s [%s] calling dummy update handler",
+ ResourceModel.TYPE_NAME, request.getDesiredResourceState()));
+
+ return ProgressEvent.builder()
+ .resourceModel(request.getDesiredResourceState())
+ .status(OperationStatus.SUCCESS)
+ .build();
+
+ }
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Utils.java b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Utils.java
new file mode 100644
index 0000000..456980f
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/main/java/software/amazon/sagemaker/modelbiasjobdefinition/Utils.java
@@ -0,0 +1,25 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+
+import org.apache.commons.lang3.StringUtils;
+
+public class Utils {
+
+ /**
+ * Get resource name from ARN.
+ *
+ * Since some resources use the physical id as the full arn, we need
+ * a way to go from that to the resource name; since we use just the name
+ * for all our api calls.
+ * @param resourceArn String representation of the Resource's ARN.
+ * @param substring The substring to partition on, that is followed
+ * by the resource name.
+ * @return The name portion of the ARN. Specifically the part that
+ * follows the first substring
+ */
+ public static String getResourceNameFromArn(final String resourceArn,
+ final String substring) {
+ return StringUtils.substringAfter(resourceArn, substring);
+ }
+
+}
\ No newline at end of file
diff --git a/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/AbstractTestBase.java b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/AbstractTestBase.java
new file mode 100644
index 0000000..4b36322
--- /dev/null
+++ b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/AbstractTestBase.java
@@ -0,0 +1,76 @@
+package software.amazon.sagemaker.modelbiasjobdefinition;
+
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.awssdk.awscore.AwsResponse;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.pagination.sync.SdkIterable;
+import software.amazon.awssdk.services.sagemaker.SageMakerClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.ProxyClient;
+
+import java.time.Instant;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
+public class AbstractTestBase {
+ protected static final String TEST_ENDPOINT_NAME = "testEndpointName";
+ protected static final String TEST_ENDPOINT_LOCAL_PATH = "/opt/ml/processing/endpointdata";
+ protected static final String TEST_IMAGE_URI = "012345678912.dkr.ecr.us-west-2.amazonaws.com/montecarloanalysiscontainer:latest";
+ protected static final String TEST_ARN = "sampleArn";
+ protected static final Instant TEST_TIME = Instant.now();
+ protected static final String TEST_JOB_DEFINITION_ARN = "arn:aws:sagemaker:us-west-2:1234567890:model-bias-job-definition/testJobDefinitionName";
+ protected static final String TEST_JOB_DEFINITION_NAME = "testJobDefinitionName";
+ protected static final String TEST_ERROR_MESSAGE = "test error message";
+ protected static final Credentials MOCK_CREDENTIALS;
+ protected static final LoggerProxy logger;
+
+ static {
+ MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token");
+ logger = new LoggerProxy();
+ }
+ static ProxyClient MOCK_PROXY(
+ final AmazonWebServicesClientProxy proxy,
+ final SageMakerClient sagemakerClient) {
+ return new ProxyClient() {
+ @Override
+ public ResponseT
+ injectCredentialsAndInvokeV2(RequestT request, Function requestFunction) {
+ return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
+ }
+
+ @Override
+ public
+ CompletableFuture
+ injectCredentialsAndInvokeV2Async(RequestT request, Function> requestFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public