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 > + 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-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandlerTest.java b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandlerTest.java new file mode 100644 index 0000000..0793076 --- /dev/null +++ b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/CreateHandlerTest.java @@ -0,0 +1,240 @@ +package software.amazon.sagemaker.modelbiasjobdefinition; + +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.CreateModelBiasJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelBiasJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionResponse; +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.modelbiasjobdefinition.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 DescribeModelBiasJobDefinitionResponse describeModelBiasJobDefinitionResponse = + DescribeModelBiasJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelBiasJobDefinitionResponse createModelBiasJobDefinitionResponse = CreateModelBiasJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class))) + .thenReturn(describeModelBiasJobDefinitionResponse); + when(proxyClient.client().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.class))) + .thenReturn(createModelBiasJobDefinitionResponse); + + 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 DescribeModelBiasJobDefinitionResponse describeModelBiasJobDefinitionResponse = + DescribeModelBiasJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelBiasJobDefinitionResponse createModelBiasJobDefinitionResponse = CreateModelBiasJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class))) + .thenReturn(describeModelBiasJobDefinitionResponse); + when(proxyClient.client().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.class))) + .thenReturn(createModelBiasJobDefinitionResponse); + + 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().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.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_ModelBiasJobDefinitionAlreadyExists_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.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().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.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().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.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().createModelBiasJobDefinition(any(CreateModelBiasJobDefinitionRequest.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-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandlerTest.java b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandlerTest.java new file mode 100644 index 0000000..570bb65 --- /dev/null +++ b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/DeleteHandlerTest.java @@ -0,0 +1,153 @@ +package software.amazon.sagemaker.modelbiasjobdefinition; + +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.DeleteModelBiasJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelBiasJobDefinitionResponse; +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.modelbiasjobdefinition.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 DeleteModelBiasJobDefinitionResponse deleteModelBiasJobDefinitionResponse = DeleteModelBiasJobDefinitionResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().deleteModelBiasJobDefinition(any(DeleteModelBiasJobDefinitionRequest.class))) + .thenReturn(deleteModelBiasJobDefinitionResponse); + + + 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 DeleteModelBiasJobDefinitionResponse deleteModelBiasJobDefinitionResponse = DeleteModelBiasJobDefinitionResponse.builder() + .build(); + ResourceModel resourceModel = ResourceModel.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(resourceModel) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + DeleteModelBiasJobDefinitionRequest.class); + + when(proxyClient.client().deleteModelBiasJobDefinition(any(DeleteModelBiasJobDefinitionRequest.class))) + .thenReturn(deleteModelBiasJobDefinitionResponse); + + final ProgressEvent response = invokeHandleRequest(request); + + verify(proxyClient.client(), times(1)).deleteModelBiasJobDefinition(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().deleteModelBiasJobDefinition(any(DeleteModelBiasJobDefinitionRequest.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_ModelBiasJobDefinitionDoesNotExists_Fails() { + when(proxyClient.client().deleteModelBiasJobDefinition(any(DeleteModelBiasJobDefinitionRequest.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-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandlerTest.java b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandlerTest.java new file mode 100644 index 0000000..4bd2b71 --- /dev/null +++ b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/ReadHandlerTest.java @@ -0,0 +1,222 @@ +package software.amazon.sagemaker.modelbiasjobdefinition; + +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.DescribeModelBiasJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelBiasJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelBiasBaselineConfig; +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.modelbiasjobdefinition.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() { + + ModelBiasBaselineConfig modelBiasBaselineConfig = ModelBiasBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelBiasJobDefinitionResponse describeModelBiasJobDefinitionResponse = + DescribeModelBiasJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelBiasBaselineConfig(modelBiasBaselineConfig) + .roleArn(TEST_ARN) + .build(); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class))) + .thenReturn(describeModelBiasJobDefinitionResponse); + + software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig.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) + .modelBiasBaselineConfig(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()).describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_WithoutJobDefinitionName_Success() { + + ModelBiasBaselineConfig modelBiasBaselineConfig = ModelBiasBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelBiasJobDefinitionResponse describeModelBiasJobDefinitionResponse = + DescribeModelBiasJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelBiasBaselineConfig(modelBiasBaselineConfig) + .roleArn(TEST_ARN) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + DescribeModelBiasJobDefinitionRequest.class); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class))) + .thenReturn(describeModelBiasJobDefinitionResponse); + + software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelbiasjobdefinition.ModelBiasBaselineConfig.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) + .modelBiasBaselineConfig(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)).describeModelBiasJobDefinition(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()).describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.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_ModelBiasJobDefinitionDoesNotExist_Fails() { + final AwsServiceException resourceNotFoundException = AwsServiceException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.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().describeModelBiasJobDefinition(any(DescribeModelBiasJobDefinitionRequest.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-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorTest.java b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorTest.java new file mode 100644 index 0000000..1379413 --- /dev/null +++ b/aws-sagemaker-modelbiasjobdefinition/src/test/java/software/amazon/sagemaker/modelbiasjobdefinition/TranslatorTest.java @@ -0,0 +1,145 @@ +package software.amazon.sagemaker.modelbiasjobdefinition; + +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-modelbiasjobdefinition/template.yml b/aws-sagemaker-modelbiasjobdefinition/template.yml new file mode 100644 index 0000000..d624fb4 --- /dev/null +++ b/aws-sagemaker-modelbiasjobdefinition/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::ModelBiasJobDefinition 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.modelbiasjobdefinition.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelbiasjobdefinition-1.0.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.modelbiasjobdefinition.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelbiasjobdefinition-1.0.jar diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/.rpdk-config b/aws-sagemaker-modelexplainabilityjobdefinition/.rpdk-config new file mode 100644 index 0000000..5547675 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::SageMaker::ModelExplainabilityJobDefinition", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.modelexplainabilityjobdefinition.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.modelexplainabilityjobdefinition.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "sagemaker", + "modelexplainabilityjobdefinition" + ], + "protocolVersion": "2.0.0" + } +} \ No newline at end of file diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/README.md b/aws-sagemaker-modelexplainabilityjobdefinition/README.md new file mode 100644 index 0000000..dfeed1d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/README.md @@ -0,0 +1,178 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition + +Resource Type definition for AWS::SageMaker::ModelExplainabilityJobDefinition + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelExplainabilityJobDefinition",
+    "Properties" : {
+        "JobDefinitionName" : String,
+        "ModelExplainabilityBaselineConfig" : ModelExplainabilityBaselineConfig,
+        "ModelExplainabilityAppSpecification" : ModelExplainabilityAppSpecification,
+        "ModelExplainabilityJobInput" : ModelExplainabilityJobInput,
+        "ModelExplainabilityJobOutputConfig" : MonitoringOutputConfig,
+        "JobResources" : MonitoringResources,
+        "NetworkConfig" : NetworkConfig,
+        "RoleArn" : String,
+        "StoppingCondition" : StoppingCondition,
+        "Tags" : [ Tag, ... ],
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelExplainabilityJobDefinition
+Properties:
+    JobDefinitionName: String
+    ModelExplainabilityBaselineConfig: ModelExplainabilityBaselineConfig
+    ModelExplainabilityAppSpecification: ModelExplainabilityAppSpecification
+    ModelExplainabilityJobInput: ModelExplainabilityJobInput
+    ModelExplainabilityJobOutputConfig: 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) + +#### ModelExplainabilityBaselineConfig + +Baseline configuration used to validate that the data conforms to the specified constraints and statistics. + +_Required_: No + +_Type_: ModelExplainabilityBaselineConfig + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityAppSpecification + +Container image configuration object for the monitoring job. + +_Required_: Yes + +_Type_: ModelExplainabilityAppSpecification + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityJobInput + +The inputs for a monitoring job. + +_Required_: Yes + +_Type_: ModelExplainabilityJobInput + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityJobOutputConfig + +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-modelexplainabilityjobdefinition/aws-sagemaker-modelexplainabilityjobdefinition.json b/aws-sagemaker-modelexplainabilityjobdefinition/aws-sagemaker-modelexplainabilityjobdefinition.json new file mode 100644 index 0000000..fd43c1a --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/aws-sagemaker-modelexplainabilityjobdefinition.json @@ -0,0 +1,438 @@ +{ + "typeName" : "AWS::SageMaker::ModelExplainabilityJobDefinition", + "description" : "Resource Type definition for AWS::SageMaker::ModelExplainabilityJobDefinition", + "additionalProperties" : false, + "properties" : { + "JobDefinitionArn" : { + "description": "The Amazon Resource Name (ARN) of job definition.", + "type" : "string", + "minLength": 1, + "maxLength": 256 + }, + "JobDefinitionName" : { + "$ref" : "#/definitions/JobDefinitionName" + }, + "ModelExplainabilityBaselineConfig": { + "$ref": "#/definitions/ModelExplainabilityBaselineConfig" + }, + "ModelExplainabilityAppSpecification": { + "$ref": "#/definitions/ModelExplainabilityAppSpecification" + }, + "ModelExplainabilityJobInput": { + "$ref": "#/definitions/ModelExplainabilityJobInput" + }, + "ModelExplainabilityJobOutputConfig": { + "$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" : { + "ModelExplainabilityBaselineConfig" : { + "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 + } + } + }, + "ModelExplainabilityAppSpecification" : { + "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" ] + }, + "ModelExplainabilityJobInput" : { + "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" + ] + }, + "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 + } + }, + "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 + } + }, + "required" : [ "ModelExplainabilityAppSpecification", "ModelExplainabilityJobInput", "ModelExplainabilityJobOutputConfig", "JobResources", "RoleArn"], + "primaryIdentifier" : [ "/properties/JobDefinitionArn" ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateModelExplainabilityJobDefinition", + "sagemaker:DescribeModelExplainabilityJobDefinition", + "iam:PassRole" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteModelExplainabilityJobDefinition" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeModelExplainabilityJobDefinition" + ] + }, + "update": { + "permissions": [] + } + }, + "readOnlyProperties": [ + "/properties/CreationTime", + "/properties/JobDefinitionArn" + ], + "createOnlyProperties": [ + "/properties/JobDefinitionName", + "/properties/ModelExplainabilityAppSpecification", + "/properties/ModelExplainabilityBaselineConfig", + "/properties/ModelExplainabilityJobInput", + "/properties/ModelExplainabilityJobOutputConfig", + "/properties/JobResources", + "/properties/NetworkConfig", + "/properties/RoleArn", + "/properties/StoppingCondition", + "/properties/Tags" + ] +} \ No newline at end of file diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/docs/README.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/README.md new file mode 100644 index 0000000..dfeed1d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/README.md @@ -0,0 +1,178 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition + +Resource Type definition for AWS::SageMaker::ModelExplainabilityJobDefinition + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelExplainabilityJobDefinition",
+    "Properties" : {
+        "JobDefinitionName" : String,
+        "ModelExplainabilityBaselineConfig" : ModelExplainabilityBaselineConfig,
+        "ModelExplainabilityAppSpecification" : ModelExplainabilityAppSpecification,
+        "ModelExplainabilityJobInput" : ModelExplainabilityJobInput,
+        "ModelExplainabilityJobOutputConfig" : MonitoringOutputConfig,
+        "JobResources" : MonitoringResources,
+        "NetworkConfig" : NetworkConfig,
+        "RoleArn" : String,
+        "StoppingCondition" : StoppingCondition,
+        "Tags" : [ Tag, ... ],
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelExplainabilityJobDefinition
+Properties:
+    JobDefinitionName: String
+    ModelExplainabilityBaselineConfig: ModelExplainabilityBaselineConfig
+    ModelExplainabilityAppSpecification: ModelExplainabilityAppSpecification
+    ModelExplainabilityJobInput: ModelExplainabilityJobInput
+    ModelExplainabilityJobOutputConfig: 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) + +#### ModelExplainabilityBaselineConfig + +Baseline configuration used to validate that the data conforms to the specified constraints and statistics. + +_Required_: No + +_Type_: ModelExplainabilityBaselineConfig + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityAppSpecification + +Container image configuration object for the monitoring job. + +_Required_: Yes + +_Type_: ModelExplainabilityAppSpecification + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityJobInput + +The inputs for a monitoring job. + +_Required_: Yes + +_Type_: ModelExplainabilityJobInput + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelExplainabilityJobOutputConfig + +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-modelexplainabilityjobdefinition/docs/clusterconfig.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/clusterconfig.md new file mode 100644 index 0000000..598e36c --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/clusterconfig.md @@ -0,0 +1,70 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/constraintsresource.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/constraintsresource.md new file mode 100644 index 0000000..6250f8d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/constraintsresource.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/endpointinput.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/endpointinput.md new file mode 100644 index 0000000..cdd7717 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/endpointinput.md @@ -0,0 +1,124 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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,
+    "FeaturesAttribute" : String,
+    "InferenceAttribute" : String,
+    "ProbabilityAttribute" : String
+}
+
+ +### YAML + +
+EndpointName: String
+LocalPath: String
+S3DataDistributionType: String
+S3InputMode: String
+FeaturesAttribute: String
+InferenceAttribute: String
+ProbabilityAttribute: 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) + +#### 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) + diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification-environment.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification-environment.md new file mode 100644 index 0000000..47f2c3c --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification-environment.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition ModelExplainabilityAppSpecification 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-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification.md new file mode 100644 index 0000000..9c10955 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityappspecification.md @@ -0,0 +1,66 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition ModelExplainabilityAppSpecification + +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-modelexplainabilityjobdefinition/docs/modelexplainabilitybaselineconfig.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilitybaselineconfig.md new file mode 100644 index 0000000..bf52316 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilitybaselineconfig.md @@ -0,0 +1,52 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition ModelExplainabilityBaselineConfig + +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-modelexplainabilityjobdefinition/docs/modelexplainabilityjobinput.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityjobinput.md new file mode 100644 index 0000000..99395e6 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/modelexplainabilityjobinput.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition ModelExplainabilityJobInput + +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-modelexplainabilityjobdefinition/docs/monitoringoutput.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringoutput.md new file mode 100644 index 0000000..837f82d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringoutput.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/monitoringoutputconfig.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringoutputconfig.md new file mode 100644 index 0000000..1993991 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringoutputconfig.md @@ -0,0 +1,55 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/monitoringresources.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringresources.md new file mode 100644 index 0000000..6640e9f --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/monitoringresources.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/networkconfig.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/networkconfig.md new file mode 100644 index 0000000..1341ecf --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/networkconfig.md @@ -0,0 +1,58 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/s3output.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/s3output.md new file mode 100644 index 0000000..f00457d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/s3output.md @@ -0,0 +1,68 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/stoppingcondition.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/stoppingcondition.md new file mode 100644 index 0000000..59abc91 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/stoppingcondition.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/tag.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/tag.md new file mode 100644 index 0000000..1459d36 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/tag.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/docs/vpcconfig.md b/aws-sagemaker-modelexplainabilityjobdefinition/docs/vpcconfig.md new file mode 100644 index 0000000..2d3db53 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/docs/vpcconfig.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::ModelExplainabilityJobDefinition 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-modelexplainabilityjobdefinition/lombok.config b/aws-sagemaker-modelexplainabilityjobdefinition/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/pom.xml b/aws-sagemaker-modelexplainabilityjobdefinition/pom.xml new file mode 100644 index 0000000..75b12e7 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + software.amazon.sagemaker.modelexplainabilityjobdefinition + aws-sagemaker-modelexplainabilityjobdefinition-handler + aws-sagemaker-modelexplainabilityjobdefinition-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-modelexplainabilityjobdefinition.json + + + + + \ No newline at end of file diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/resource-role.yaml b/aws-sagemaker-modelexplainabilityjobdefinition/resource-role.yaml new file mode 100644 index 0000000..fe19d5e --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/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:CreateModelExplainabilityJobDefinition" + - "sagemaker:DeleteModelExplainabilityJobDefinition" + - "sagemaker:DescribeModelExplainabilityJobDefinition" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/BaseHandlerStd.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/BaseHandlerStd.java new file mode 100644 index 0000000..364eed8 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/BaseHandlerStd.java @@ -0,0 +1,38 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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_EXPLAINABILITY_ARN_SUBSTRING = ":model-explainability-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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CallbackContext.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CallbackContext.java new file mode 100644 index 0000000..266ee7d --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CallbackContext.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ClientBuilder.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ClientBuilder.java new file mode 100644 index 0000000..2812cd7 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ClientBuilder.java @@ -0,0 +1,12 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Configuration.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Configuration.java new file mode 100644 index 0000000..d85b6b4 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Configuration.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +class Configuration extends BaseConfiguration { + public Configuration() { + super("aws-sagemaker-modelexplainabilityjobdefinition.json"); + } +} \ No newline at end of file diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandler.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandler.java new file mode 100644 index 0000000..f2b8a12 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandler.java @@ -0,0 +1,107 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.CreateModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelExplainabilityJobDefinitionResponse; +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-ModelExplainabilityJobDefinition::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 CreateModelExplainabilityJobDefinitionResponse createResource( + final CreateModelExplainabilityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient) { + + CreateModelExplainabilityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createModelExplainabilityJobDefinition); + } 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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandler.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandler.java new file mode 100644 index 0000000..2c7d293 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandler.java @@ -0,0 +1,75 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.DeleteModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelExplainabilityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +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.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; +import software.amazon.sagemaker.modelexplainabilityjobdefinition.Utils; + +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_EXPLAINABILITY_ARN_SUBSTRING); + model.setJobDefinitionName(jobDefinitionName); + } + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-ModelExplainabilityJobDefinition::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 DeleteModelExplainabilityJobDefinitionResponse deleteResource( + final DeleteModelExplainabilityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient) { + + DeleteModelExplainabilityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteModelExplainabilityJobDefinition); + } 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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandler.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandler.java new file mode 100644 index 0000000..86115a3 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandler.java @@ -0,0 +1,82 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.DescribeModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionResponse; +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; +import software.amazon.sagemaker.modelexplainabilityjobdefinition.Utils; + +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_EXPLAINABILITY_ARN_SUBSTRING); + model.setJobDefinitionName(jobDefinitionName); + } + + return proxy.initiate("AWS-SageMaker-ModelExplainabilityJobDefinition::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 DescribeModelExplainabilityJobDefinitionResponse readResource( + final DescribeModelExplainabilityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient, + final ResourceModel model) { + + DescribeModelExplainabilityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeModelExplainabilityJobDefinition); + } 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 DescribeModelExplainabilityJobDefinitionResponse awsResponse) { + return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse)); + } +} diff --git a/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Translator.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Translator.java new file mode 100644 index 0000000..d066537 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Translator.java @@ -0,0 +1,61 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForRequest.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForRequest.java new file mode 100644 index 0000000..de5a3da --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForRequest.java @@ -0,0 +1,187 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +import software.amazon.awssdk.services.sagemaker.model.CreateModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityJobInput; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionRequest; +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.ProblemType; +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 createModelExplainabilityJobDefinitionRequest - service request to create a resource + */ + static CreateModelExplainabilityJobDefinitionRequest translateToCreateRequest(final ResourceModel model) { + return CreateModelExplainabilityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .modelExplainabilityAppSpecification(translate(model.getModelExplainabilityAppSpecification())) + .modelExplainabilityBaselineConfig(translate(model.getModelExplainabilityBaselineConfig())) + .modelExplainabilityJobInput(translate(model.getModelExplainabilityJobInput())) + .modelExplainabilityJobOutputConfig(translate(model.getModelExplainabilityJobOutputConfig())) + .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 describeModelExplainabilityJobDefinitionRequest - the aws service request to describe a resource + */ + static DescribeModelExplainabilityJobDefinitionRequest translateToReadRequest(final ResourceModel model) { + return DescribeModelExplainabilityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .build(); + } + + /** + * Request to delete a resource + * @param model resource model + * @return deleteModelExplainabilityJobDefinitionRequest the aws service request to delete a resource + */ + static DeleteModelExplainabilityJobDefinitionRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteModelExplainabilityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .build(); + } + + static ModelExplainabilityAppSpecification translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityAppSpecification appSpec) { + return appSpec == null ? null : ModelExplainabilityAppSpecification.builder() + .imageUri(appSpec.getImageUri()) + .configUri(appSpec.getConfigUri()) + .environment(translateMapOfObjectsToMapOfStrings(appSpec.getEnvironment())) + .build(); + } + + static ModelExplainabilityBaselineConfig translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig baselineConfig) { + return baselineConfig == null ? null : ModelExplainabilityBaselineConfig.builder() + .baseliningJobName(baselineConfig.getBaseliningJobName()) + .constraintsResource(translate(baselineConfig.getConstraintsResource())) + .build(); + } + + static MonitoringConstraintsResource translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.ConstraintsResource constraintsResource) { + return constraintsResource == null ? null : MonitoringConstraintsResource.builder().s3Uri(constraintsResource.getS3Uri()).build(); + } + + static ModelExplainabilityJobInput translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityJobInput jobInput) { + return jobInput == null ? null : ModelExplainabilityJobInput.builder() + .endpointInput(translate(jobInput.getEndpointInput())) + .build(); + } + static EndpointInput translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.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()) + .build(); + } + static MonitoringOutputConfig translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.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.modelexplainabilityjobdefinition.MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.getS3Output())) + .build(); + } + + static MonitoringS3Output translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.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.modelexplainabilityjobdefinition.MonitoringResources monitoringResources) { + return monitoringResources == null? null : MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.getClusterConfig())) + .build(); + } + + static MonitoringClusterConfig translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.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.modelexplainabilityjobdefinition.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.modelexplainabilityjobdefinition.VpcConfig vpcConfig) { + return vpcConfig == null? null : VpcConfig.builder() + .securityGroupIds(vpcConfig.getSecurityGroupIds()) + .subnets(vpcConfig.getSubnets()) + .build(); + } + + static MonitoringStoppingCondition translate(final software.amazon.sagemaker.modelexplainabilityjobdefinition.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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForResponse.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForResponse.java new file mode 100644 index 0000000..8466d40 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorForResponse.java @@ -0,0 +1,161 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityJobInput; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionResponse; +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 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 DescribeModelExplainabilityJobDefinitionResponse awsResponse) { + return ResourceModel.builder() + .jobDefinitionArn(awsResponse.jobDefinitionArn()) + .jobDefinitionName(awsResponse.jobDefinitionName()) + .creationTime(awsResponse.creationTime().toString()) + .modelExplainabilityBaselineConfig(translate(awsResponse.modelExplainabilityBaselineConfig())) + .modelExplainabilityAppSpecification(translate(awsResponse.modelExplainabilityAppSpecification())) + .modelExplainabilityJobInput(translate(awsResponse.modelExplainabilityJobInput())) + .modelExplainabilityJobOutputConfig(translate(awsResponse.modelExplainabilityJobOutputConfig())) + .jobResources(translate(awsResponse.jobResources())) + .networkConfig(translate(awsResponse.networkConfig())) + .roleArn(awsResponse.roleArn()) + .stoppingCondition(translate(awsResponse.stoppingCondition())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig translate( + final ModelExplainabilityBaselineConfig baselineConfig) { + return baselineConfig == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig.builder() + .baseliningJobName(baselineConfig.baseliningJobName()) + .constraintsResource(translate(baselineConfig.constraintsResource())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.ConstraintsResource translate( + final MonitoringConstraintsResource constraintsResource) { + return constraintsResource == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.ConstraintsResource.builder() + .s3Uri(constraintsResource.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityAppSpecification translate( + final ModelExplainabilityAppSpecification monitoringAppSpec) { + return monitoringAppSpec == null ? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityAppSpecification.builder() + .imageUri(monitoringAppSpec.imageUri()) + .configUri(monitoringAppSpec.configUri()) + .environment(translateMapOfStringsMapOfObjects(monitoringAppSpec.environment())) + .build(); + } + + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityJobInput translate(final ModelExplainabilityJobInput monitoringInput) { + return monitoringInput == null ? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityJobInput.builder() + .endpointInput(translate(monitoringInput.endpointInput())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.EndpointInput translate(final EndpointInput endpointInput) { + return endpointInput == null ? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.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()) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.MonitoringOutputConfig translate(final MonitoringOutputConfig outputConfig) { + return outputConfig == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.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.modelexplainabilityjobdefinition.MonitoringOutput translate(final MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.s3Output())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.S3Output translate(final MonitoringS3Output s3Output) { + return s3Output == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.S3Output.builder() + .localPath(s3Output.localPath()) + .s3UploadMode(s3Output.s3UploadMode().toString()) + .s3Uri(s3Output.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.MonitoringResources translate(final MonitoringResources monitoringResources) { + return monitoringResources == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.clusterConfig())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.ClusterConfig translate(final MonitoringClusterConfig clusterConfig) { + return clusterConfig == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.ClusterConfig.builder() + .instanceCount(clusterConfig.instanceCount()) + .instanceType(clusterConfig.instanceType().toString()) + .volumeKmsKeyId(clusterConfig.volumeKmsKeyId()) + .volumeSizeInGB(clusterConfig.volumeSizeInGB()) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.NetworkConfig translate(final MonitoringNetworkConfig networkConfig) { + return networkConfig == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.NetworkConfig.builder() + .enableInterContainerTrafficEncryption(networkConfig.enableInterContainerTrafficEncryption()) + .enableNetworkIsolation(networkConfig.enableNetworkIsolation()) + .vpcConfig(translate(networkConfig.vpcConfig())) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.VpcConfig translate(final VpcConfig vpcConfig) { + return vpcConfig == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.VpcConfig.builder() + .securityGroupIds(vpcConfig.securityGroupIds()) + .subnets(vpcConfig.subnets()) + .build(); + } + + static software.amazon.sagemaker.modelexplainabilityjobdefinition.StoppingCondition translate(final MonitoringStoppingCondition stoppingCondition) { + return stoppingCondition == null? null : software.amazon.sagemaker.modelexplainabilityjobdefinition.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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/UpdateHandler.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/UpdateHandler.java new file mode 100644 index 0000000..45a777c --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/UpdateHandler.java @@ -0,0 +1,27 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Utils.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Utils.java new file mode 100644 index 0000000..d8b5cc2 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/main/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/Utils.java @@ -0,0 +1,25 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + + +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-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/AbstractTestBase.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/AbstractTestBase.java new file mode 100644 index 0000000..dc5b92c --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/AbstractTestBase.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-explainability-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-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandlerTest.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandlerTest.java new file mode 100644 index 0000000..0e257b5 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/CreateHandlerTest.java @@ -0,0 +1,240 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.CreateModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelExplainabilityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionResponse; +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.modelexplainabilityjobdefinition.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 DescribeModelExplainabilityJobDefinitionResponse describeModelExplainabilityJobDefinitionResponse = + DescribeModelExplainabilityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelExplainabilityJobDefinitionResponse createModelExplainabilityJobDefinitionResponse = CreateModelExplainabilityJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(describeModelExplainabilityJobDefinitionResponse); + when(proxyClient.client().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(createModelExplainabilityJobDefinitionResponse); + + 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 DescribeModelExplainabilityJobDefinitionResponse describeModelExplainabilityJobDefinitionResponse = + DescribeModelExplainabilityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelExplainabilityJobDefinitionResponse createModelExplainabilityJobDefinitionResponse = CreateModelExplainabilityJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(describeModelExplainabilityJobDefinitionResponse); + when(proxyClient.client().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(createModelExplainabilityJobDefinitionResponse); + + 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().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.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_ModelExplainabilityJobDefinitionAlreadyExists_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.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().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.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().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.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().createModelExplainabilityJobDefinition(any(CreateModelExplainabilityJobDefinitionRequest.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-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandlerTest.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandlerTest.java new file mode 100644 index 0000000..4051bd7 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/DeleteHandlerTest.java @@ -0,0 +1,155 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.Mockito; +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.DeleteModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelExplainabilityJobDefinitionResponse; +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.List; + +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.modelexplainabilityjobdefinition.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 DeleteModelExplainabilityJobDefinitionResponse deleteModelExplainabilityJobDefinitionResponse = DeleteModelExplainabilityJobDefinitionResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().deleteModelExplainabilityJobDefinition(any(DeleteModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(deleteModelExplainabilityJobDefinitionResponse); + + + 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 DeleteModelExplainabilityJobDefinitionResponse deleteModelExplainabilityJobDefinitionResponse = DeleteModelExplainabilityJobDefinitionResponse.builder() + .build(); + + ResourceModel resourceModel = ResourceModel.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(resourceModel) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeleteModelExplainabilityJobDefinitionRequest.class); + + when(proxyClient.client().deleteModelExplainabilityJobDefinition(any(DeleteModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(deleteModelExplainabilityJobDefinitionResponse); + + final ProgressEvent response = invokeHandleRequest(request); + + verify(proxyClient.client(), times(1)).deleteModelExplainabilityJobDefinition(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().deleteModelExplainabilityJobDefinition(any(DeleteModelExplainabilityJobDefinitionRequest.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_ModelExplainabilityJobDefinitionDoesNotExists_Fails() { + when(proxyClient.client().deleteModelExplainabilityJobDefinition(any(DeleteModelExplainabilityJobDefinitionRequest.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-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandlerTest.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandlerTest.java new file mode 100644 index 0000000..b71aadc --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/ReadHandlerTest.java @@ -0,0 +1,223 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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.DescribeModelExplainabilityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelExplainabilityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelExplainabilityBaselineConfig; +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.modelexplainabilityjobdefinition.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() { + + ModelExplainabilityBaselineConfig modelExplainabilityBaselineConfig = ModelExplainabilityBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelExplainabilityJobDefinitionResponse describeModelExplainabilityJobDefinitionResponse = + DescribeModelExplainabilityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelExplainabilityBaselineConfig(modelExplainabilityBaselineConfig) + .roleArn(TEST_ARN) + .build(); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(describeModelExplainabilityJobDefinitionResponse); + + software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig.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) + .modelExplainabilityBaselineConfig(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()).describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_WithoutJobDefinitionName_Success() { + + ModelExplainabilityBaselineConfig modelExplainabilityBaselineConfig = ModelExplainabilityBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelExplainabilityJobDefinitionResponse describeModelExplainabilityJobDefinitionResponse = + DescribeModelExplainabilityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelExplainabilityBaselineConfig(modelExplainabilityBaselineConfig) + .roleArn(TEST_ARN) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + DescribeModelExplainabilityJobDefinitionRequest.class); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class))) + .thenReturn(describeModelExplainabilityJobDefinitionResponse); + + software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelexplainabilityjobdefinition.ModelExplainabilityBaselineConfig.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) + .modelExplainabilityBaselineConfig(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)).describeModelExplainabilityJobDefinition(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()).describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.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_ModelExplainabilityJobDefinitionDoesNotExist_Fails() { + final AwsServiceException resourceNotFoundException = AwsServiceException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.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().describeModelExplainabilityJobDefinition(any(DescribeModelExplainabilityJobDefinitionRequest.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-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorTest.java b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorTest.java new file mode 100644 index 0000000..4850609 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/src/test/java/software/amazon/sagemaker/modelexplainabilityjobdefinition/TranslatorTest.java @@ -0,0 +1,145 @@ +package software.amazon.sagemaker.modelexplainabilityjobdefinition; + +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-modelexplainabilityjobdefinition/template.yml b/aws-sagemaker-modelexplainabilityjobdefinition/template.yml new file mode 100644 index 0000000..b066609 --- /dev/null +++ b/aws-sagemaker-modelexplainabilityjobdefinition/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::ModelExplainabilityJobDefinition 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.modelexplainabilityjobdefinition.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelexplainabilityjobdefinition-1.0.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.modelexplainabilityjobdefinition.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelexplainabilityjobdefinition-1.0.jar diff --git a/aws-sagemaker-modelpackagegroup/.rpdk-config b/aws-sagemaker-modelpackagegroup/.rpdk-config new file mode 100644 index 0000000..4c3e6d2 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::SageMaker::ModelPackageGroup", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.modelpackagegroup.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.modelpackagegroup.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "sagemaker", + "modelpackagegroup" + ], + "protocolVersion": "2.0.0" + } +} diff --git a/aws-sagemaker-modelpackagegroup/README.md b/aws-sagemaker-modelpackagegroup/README.md new file mode 100644 index 0000000..ab7b632 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/README.md @@ -0,0 +1,131 @@ +# AWS::SageMaker::ModelPackageGroup + +Resource Type definition for AWS::SageMaker::ModelPackageGroup + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelPackageGroup",
+    "Properties" : {
+        "Tags" : [ Tag, ... ],
+        "ModelPackageGroupArn" : String,
+        "ModelPackageGroupName" : String,
+        "ModelPackageGroupDescription" : String,
+        "ModelPackageGroupPolicy" : Map,
+        "CreationTime" : String,
+        "ModelPackageGroupStatus" : String
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelPackageGroup
+Properties:
+    Tags: 
+      - Tag
+    ModelPackageGroupArn: String
+    ModelPackageGroupName: String
+    ModelPackageGroupDescription: String
+    ModelPackageGroupPolicy: Map
+    CreationTime: String
+    ModelPackageGroupStatus: String
+
+ +## Properties + +#### Tags + +An array of key-value pairs to apply to this resource. + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupArn + +The Amazon Resource Name (ARN) of the model package group. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_Pattern_: arn:.* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupName + +The name of the model package group. + +_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) + +#### ModelPackageGroupDescription + +The description of the model package group. + +_Required_: No + +_Type_: String + +_Maximum_: 1024 + +_Pattern_: [\p{L}\p{M}\p{Z}\p{S}\p{N}\p{P}]* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupPolicy + +_Required_: No + +_Type_: Map + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### CreationTime + +The time at which the model package group was created. + +_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) + +#### ModelPackageGroupStatus + +The status of a modelpackage group job. + +_Required_: No + +_Type_: String + +_Allowed Values_: Pending | InProgress | Completed | Failed | Deleting | DeleteFailed + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the ModelPackageGroupArn. diff --git a/aws-sagemaker-modelpackagegroup/aws-sagemaker-modelpackagegroup.json b/aws-sagemaker-modelpackagegroup/aws-sagemaker-modelpackagegroup.json new file mode 100644 index 0000000..2511a66 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/aws-sagemaker-modelpackagegroup.json @@ -0,0 +1,141 @@ +{ + "typeName": "AWS::SageMaker::ModelPackageGroup", + "description": "Resource Type definition for AWS::SageMaker::ModelPackageGroup", + "additionalProperties": false, + "properties": { + "Tags" : { + "type" : "array", + "maxItems" : 50, + "description" : "An array of key-value pairs to apply to this resource.", + "items" : { + "$ref" : "#/definitions/Tag" + } + }, + "ModelPackageGroupArn": { + "$ref" : "#/definitions/ModelPackageGroupArn" + }, + "ModelPackageGroupName": { + "$ref" : "#/definitions/ModelPackageGroupName" + }, + "ModelPackageGroupDescription": { + "$ref" : "#/definitions/ModelPackageGroupDescription" + }, + "ModelPackageGroupPolicy": { + "type": ["object", "string"] + }, + "CreationTime": { + "description": "The time at which the model package group was created.", + "type": "string" + }, + "ModelPackageGroupStatus": { + "description": "The status of a modelpackage group job.", + "type": "string", + "enum": [ + "Pending", + "InProgress", + "Completed", + "Failed", + "Deleting", + "DeleteFailed" + ] + } + }, + "definitions": { + "Tag" : { + "description" : "A key-value pair to associate with a resource.", + "type" : "object", + "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" ] + }, + "ModelPackageGroupDescription": { + "type" : "string", + "description" : "The description of the model package group.", + "pattern": "[\\p{L}\\p{M}\\p{Z}\\p{S}\\p{N}\\p{P}]*", + "maxLength" : 1024 + }, + "ModelPackageGroupName": { + "type" : "string", + "description" : "The name of the model package group.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "maxLength" : 63 + }, + "ModelPackageGroupArn": { + "description": "The Amazon Resource Name (ARN) of the model package group.", + "type" : "string", + "minLength": 1, + "maxLength": 256, + "pattern": "arn:.*" + } + }, + "required": [ + "ModelPackageGroupName" + ], + "primaryIdentifier": [ + "/properties/ModelPackageGroupArn" + ], + "readOnlyProperties": [ + "/properties/ModelPackageGroupArn", + "/properties/CreationTime", + "/properties/ModelPackageGroupStatus" + ], + "createOnlyProperties": [ + "/properties/ModelPackageGroupName", + "/properties/ModelPackageGroupDescription" + ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateModelPackageGroup", + "sagemaker:DescribeModelPackageGroup", + "sagemaker:GetModelPackageGroupPolicy", + "sagemaker:PutModelPackageGroupPolicy" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteModelPackageGroup", + "sagemaker:DescribeModelPackageGroup", + "sagemaker:GetModelPackageGroupPolicy", + "sagemaker:DeleteModelPackageGroupPolicy" + ] + }, + "list": { + "permissions": [ + "sagemaker:ListModelPackageGroups" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeModelPackageGroup", + "sagemaker:GetModelPackageGroupPolicy", + "sagemaker:PutModelPackageGroupPolicy", + "sagemaker:ListTags" + ] + }, + "update": { + "permissions": [ + "sagemaker:DescribeModelPackageGroup", + "sagemaker:GetModelPackageGroupPolicy", + "sagemaker:DeleteModelPackageGroupPolicy", + "sagemaker:PutModelPackageGroupPolicy", + "sagemaker:ListTags", + "sagemaker:AddTags", + "sagemaker:DeleteTags" + ] + } + } +} \ No newline at end of file diff --git a/aws-sagemaker-modelpackagegroup/docs/README.md b/aws-sagemaker-modelpackagegroup/docs/README.md new file mode 100644 index 0000000..ab7b632 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/docs/README.md @@ -0,0 +1,131 @@ +# AWS::SageMaker::ModelPackageGroup + +Resource Type definition for AWS::SageMaker::ModelPackageGroup + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelPackageGroup",
+    "Properties" : {
+        "Tags" : [ Tag, ... ],
+        "ModelPackageGroupArn" : String,
+        "ModelPackageGroupName" : String,
+        "ModelPackageGroupDescription" : String,
+        "ModelPackageGroupPolicy" : Map,
+        "CreationTime" : String,
+        "ModelPackageGroupStatus" : String
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelPackageGroup
+Properties:
+    Tags: 
+      - Tag
+    ModelPackageGroupArn: String
+    ModelPackageGroupName: String
+    ModelPackageGroupDescription: String
+    ModelPackageGroupPolicy: Map
+    CreationTime: String
+    ModelPackageGroupStatus: String
+
+ +## Properties + +#### Tags + +An array of key-value pairs to apply to this resource. + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupArn + +The Amazon Resource Name (ARN) of the model package group. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_Pattern_: arn:.* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupName + +The name of the model package group. + +_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) + +#### ModelPackageGroupDescription + +The description of the model package group. + +_Required_: No + +_Type_: String + +_Maximum_: 1024 + +_Pattern_: [\p{L}\p{M}\p{Z}\p{S}\p{N}\p{P}]* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ModelPackageGroupPolicy + +_Required_: No + +_Type_: Map + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### CreationTime + +The time at which the model package group was created. + +_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) + +#### ModelPackageGroupStatus + +The status of a modelpackage group job. + +_Required_: No + +_Type_: String + +_Allowed Values_: Pending | InProgress | Completed | Failed | Deleting | DeleteFailed + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the ModelPackageGroupArn. diff --git a/aws-sagemaker-modelpackagegroup/docs/tag.md b/aws-sagemaker-modelpackagegroup/docs/tag.md new file mode 100644 index 0000000..f9e99fd --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/docs/tag.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::ModelPackageGroup 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-modelpackagegroup/lombok.config b/aws-sagemaker-modelpackagegroup/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-modelpackagegroup/pom.xml b/aws-sagemaker-modelpackagegroup/pom.xml new file mode 100644 index 0000000..a9a91b2 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + software.amazon.sagemaker.modelpackagegroup + aws-sagemaker-modelpackagegroup-handler + aws-sagemaker-modelpackagegroup-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.7 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-sagemaker-modelpackagegroup.json + + + + + diff --git a/aws-sagemaker-modelpackagegroup/resource-role.yaml b/aws-sagemaker-modelpackagegroup/resource-role.yaml new file mode 100644 index 0000000..1fa85a8 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/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: + - "sagemaker:AddTags" + - "sagemaker:CreateModelPackageGroup" + - "sagemaker:DeleteModelPackageGroup" + - "sagemaker:DeleteModelPackageGroupPolicy" + - "sagemaker:DeleteTags" + - "sagemaker:DescribeModelPackageGroup" + - "sagemaker:GetModelPackageGroupPolicy" + - "sagemaker:ListModelPackageGroups" + - "sagemaker:ListTags" + - "sagemaker:PutModelPackageGroupPolicy" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/BaseHandlerStd.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/BaseHandlerStd.java new file mode 100644 index 0000000..53662ca --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/BaseHandlerStd.java @@ -0,0 +1,36 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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); +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CallbackContext.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CallbackContext.java new file mode 100644 index 0000000..3c0325f --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CallbackContext.java @@ -0,0 +1,10 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ClientBuilder.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ClientBuilder.java new file mode 100644 index 0000000..02a297f --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ClientBuilder.java @@ -0,0 +1,12 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.services.sagemaker.SageMakerClient; + +/** + * Provides APIs to build service client. + */ +public class ClientBuilder { + public static SageMakerClient getClient() { + return SageMakerClient.builder().build(); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Configuration.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Configuration.java new file mode 100644 index 0000000..0a6e1af --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Configuration.java @@ -0,0 +1,8 @@ +package software.amazon.sagemaker.modelpackagegroup; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-sagemaker-modelpackagegroup.json"); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CreateHandler.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CreateHandler.java new file mode 100644 index 0000000..43838d1 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/CreateHandler.java @@ -0,0 +1,128 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.CreateModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.CreateModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +import software.amazon.cloudformation.Action; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProxyClient; + +public class CreateHandler 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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-ModelPackageGroup::Create", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToCreateRequest) + .makeServiceCall(this::createResource) + .stabilize(this::stabilizedOnCreate) + .progress() + ) + .then(progress -> putResourcePolicy(proxyClient, model, callbackContext)) + .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 createModelPackageGroupRequest the aws service request to create a model package group + * @param proxyClient the aws service client to make the call + * @return awsResponse create model package resource response + */ + private CreateModelPackageGroupResponse createResource( + final CreateModelPackageGroupRequest createModelPackageGroupRequest, + final ProxyClient proxyClient) { + CreateModelPackageGroupResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2( + createModelPackageGroupRequest, proxyClient.client()::createModelPackageGroup); + } catch (final ResourceInUseException e) { + throw new ResourceAlreadyExistsException(ResourceModel.TYPE_NAME, createModelPackageGroupRequest.modelPackageGroupName()); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.CREATE.toString(), ResourceModel.TYPE_NAME, createModelPackageGroupRequest.modelPackageGroupName(), e); + } + return response; + } + + /** + * Client invocation of the put policy request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent putResourcePolicy( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + if (model.getModelPackageGroupPolicy() != null) { + proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToPutModelPackageGroupPolicyRequest(model), proxyClient.client()::putModelPackageGroupPolicy); + } + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.CREATE.toString(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + /** + * This is used to ensure model package group resource has moved from Pending to Scheduled/Failed state. + * @param createModelPackageGroupRequest the aws service request to create a model package group + * @param createModelPackageGroupResponse the aws service response on creating a model package group + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return boolean state of stabilized or not + */ + private boolean stabilizedOnCreate( + final CreateModelPackageGroupRequest createModelPackageGroupRequest, + final CreateModelPackageGroupResponse createModelPackageGroupResponse, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + + if(model.getModelPackageGroupArn() == null){ + model.setModelPackageGroupArn(createModelPackageGroupResponse.modelPackageGroupArn()); + } + + final DescribeModelPackageGroupResponse response = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToReadRequest(model), + proxyClient.client()::describeModelPackageGroup); + final ModelPackageGroupStatus modelPackageGroupStatus = response.modelPackageGroupStatus(); + + switch (modelPackageGroupStatus) { + case COMPLETED: + case FAILED: + logger.log(String.format("%s [%s] has been stabilized with status %s.", ResourceModel.TYPE_NAME, + model.getPrimaryIdentifier(), modelPackageGroupStatus)); + return true; + case PENDING: + case IN_PROGRESS: + logger.log(String.format("%s [%s] is stabilizing.", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier())); + return false; + default: + throw new CfnGeneralServiceException( + "Stabilizing of " + model.getPrimaryIdentifier() + + " failed with unexpected status " + modelPackageGroupStatus); + } + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandler.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandler.java new file mode 100644 index 0000000..b546df5 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandler.java @@ -0,0 +1,108 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +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.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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-ModelPackageGroup::Delete", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::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 deleteModelPackageGroupRequest the aws service request to delete a model package group + * @param proxyClient the aws service client to make the call + * @return delete model package group response + */ + private DeleteModelPackageGroupResponse deleteResource( + final DeleteModelPackageGroupRequest deleteModelPackageGroupRequest, + final ProxyClient proxyClient) { + + DeleteModelPackageGroupResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(deleteModelPackageGroupRequest, proxyClient.client()::deleteModelPackageGroup); + } 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 + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, deleteModelPackageGroupRequest.modelPackageGroupName()); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.DELETE.toString(), ResourceModel.TYPE_NAME, deleteModelPackageGroupRequest.modelPackageGroupName(), e); + } + + return response; + } + + /** + * Sync delete API moves resource in PENDING state and actual deletion happens asynchronously. + * Stabilization is required to ensure model package group resource deletion has been completed. + * @param deleteModelPackageGroupRequest the aws service request to delete a model package group + * @param deleteModelPackageGroupResult the aws service response on deleting a model package group + * @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 DeleteModelPackageGroupRequest deleteModelPackageGroupRequest, + final DeleteModelPackageGroupResponse deleteModelPackageGroupResult, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + DescribeModelPackageGroupResponse response = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToReadRequest(model), + proxyClient.client()::describeModelPackageGroup); + final ModelPackageGroupStatus modelPackageGroupStatus = response.modelPackageGroupStatus(); + + switch (modelPackageGroupStatus) { + case DELETING: + logger.log(String.format("%s with name [%s] is stabilizing while delete.", ResourceModel.TYPE_NAME, model.getModelPackageGroupName())); + return false; + default: + throw new CfnGeneralServiceException("Delete stabilizing of model package group: " + model.getModelPackageGroupName()); + } + } catch (final ResourceNotFoundException e) { + return true; + } catch (final SageMakerException e) { + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().matches(".*ModelPackageGroup .* does not exist.*")) { + return true; + } + throw e; + } + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ExceptionMapper.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ExceptionMapper.java new file mode 100644 index 0000000..3f12a8a --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ExceptionMapper.java @@ -0,0 +1,62 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException; + +public class ExceptionMapper { + /** + * Throws Cfn exception corresponding to error code of the given exception. + * + * @param operation + * @param e exception + */ + static void throwCfnException(final String operation, 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(operation, 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); + } + + static void throwCfnException(final String operation, final String resourceType, final String resourceName, final AwsServiceException e) { + if (StringUtils.isNotBlank(e.getMessage()) + && (e.getMessage().matches(".*Cannot find Model Package Group:.*") + || e.getMessage().matches(".*ModelPackageGroup .* does not exist.*"))) { + throw new CfnNotFoundException(resourceType, resourceName, e); + } + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().matches(".*Model Package Group already exists.*")) { + throw new ResourceAlreadyExistsException(resourceType, resourceName, e); + } + throwCfnException(operation, e); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ListHandler.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ListHandler.java new file mode 100644 index 0000000..fdd6be9 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ListHandler.java @@ -0,0 +1,70 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsResponse; +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 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("AWS-SageMaker-ModelPackageGroup::List", proxyClient, model, callbackContext) + .translateToServiceRequest(resourceModel -> Translator.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 listModelPackageGroupsRequest the aws service request to list model package groups + * @param proxyClient the aws service client to make the call + * @return list model package group response + */ + private ListModelPackageGroupsResponse listResources( + final ListModelPackageGroupsRequest listModelPackageGroupsRequest, + final ProxyClient proxyClient) { + + ListModelPackageGroupsResponse listModelPackageGroupsResponse = null; + try { + listModelPackageGroupsResponse = proxyClient.injectCredentialsAndInvokeV2(listModelPackageGroupsRequest, + proxyClient.client()::listModelPackageGroups); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.LIST.toString(), e); + } + + return listModelPackageGroupsResponse; + } + + /** + * Build the Progress Event object from the SageMaker ListModelPackageGroupsResult. + * @param listResponse the aws service list resource response + * @return progressEvent indicating success, in progress with delay callback or failed state + */ + private ProgressEvent constructResourceModelFromResponse( + final ListModelPackageGroupsResponse listResponse) { + return ProgressEvent.builder() + .nextToken(listResponse.nextToken()) + .resourceModels(Translator.translateFromListResponse(listResponse)) + .status(OperationStatus.SUCCESS) + .build(); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ReadHandler.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ReadHandler.java new file mode 100644 index 0000000..7d37fcf --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/ReadHandler.java @@ -0,0 +1,107 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +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; + +import java.util.List; + +public class ReadHandler extends BaseHandlerStd { + + private Logger logger; + private ProxyClient proxyClient; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + this.logger = logger; + this.proxyClient = proxyClient; + + final ResourceModel model = request.getDesiredResourceState(); + + return proxy.initiate("AWS-SageMaker-ModelPackageGroup::Read", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest)) + .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 describeModelPackageGroupRequest the aws service request to describe model package group + * @return describe model package group response + */ + private DescribeModelPackageGroupResponse readResource( + final DescribeModelPackageGroupRequest describeModelPackageGroupRequest) { + DescribeModelPackageGroupResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(describeModelPackageGroupRequest, proxyClient.client()::describeModelPackageGroup); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, describeModelPackageGroupRequest.modelPackageGroupName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.READ.toString(), ResourceModel.TYPE_NAME, describeModelPackageGroupRequest.modelPackageGroupName(), e); + } + return response; + } + + /** + * Construction of resource model from the read response, add resource policy and tags to model + * + * @param awsResponse the aws service describe model package group response + * @return progressEvent indicating success, in progress with delay callback or failed state + */ + private ProgressEvent constructResourceModelFromResponse( + final DescribeModelPackageGroupResponse awsResponse) { + ResourceModel model = Translator.translateFromReadResponse(awsResponse); + addResourcePolicyToModel(model); + addTagsToModel(model); + return ProgressEvent.defaultSuccessHandler(model); + } + + /** + * Add resource policy to the model after fetching it + * + * @param model the resource model to which policy has to be added + */ + private void addResourcePolicyToModel(ResourceModel model) { + try { + GetModelPackageGroupPolicyResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToGetModelPackageGroupPolicyRequest(model), proxyClient.client()::getModelPackageGroupPolicy); + if (response.resourcePolicy() != null) { + model.setModelPackageGroupPolicy(response.resourcePolicy()); + } + } catch (AwsServiceException e) { + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().matches(".*Cannot find resource policy.*")) { + // there's no policy available, default null value is used + } else { + throw e; + } + } + } + + /** + * Add tags to the model after fetching it + * + * @param model the resource model to which tags have to be added + */ + private void addTagsToModel(ResourceModel model) { + ListTagsResponse tagsResponse = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToListTagsRequest(model), proxyClient.client()::listTags); + List tags = Translator.sdkTagsToCfnTags(tagsResponse.tags()); + model.setTags(tags); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Translator.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Translator.java new file mode 100644 index 0000000..1e55dbb --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/Translator.java @@ -0,0 +1,261 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import software.amazon.awssdk.services.sagemaker.model.CreateModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.PutModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.AddTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This class is a centralized placeholder for + * - api request construction + * - object translation to/from aws sdk + * - resource model construction for read/list handlers + */ + +public class Translator { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Translates the model to Request to create a model package group + * @param model resource model + * @return awsRequest the aws service request to create a model package group + */ + static CreateModelPackageGroupRequest translateToCreateRequest(final ResourceModel model) { + List tags = cfnTagsToSdkTags(model.getTags()); + CreateModelPackageGroupRequest request; + if (!tags.isEmpty()) { + request = CreateModelPackageGroupRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName()) + .modelPackageGroupDescription(model.getModelPackageGroupDescription()) + .tags(tags) + .build(); + } + else { + request = CreateModelPackageGroupRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName()) + .modelPackageGroupDescription(model.getModelPackageGroupDescription()) + .build(); + } + return request; + } + + + /** + * Translates the model to Request to put a model package group policy + * @param model resource model + * @return awsRequest the aws service request to put a model package group policy + */ + static PutModelPackageGroupPolicyRequest translateToPutModelPackageGroupPolicyRequest(final ResourceModel model) { + PutModelPackageGroupPolicyRequest putModelPackageGroupPolicyRequest; + try { + String policy; + if (model.getModelPackageGroupPolicy() instanceof String) { + policy = model.getModelPackageGroupPolicy().toString(); + } + else { + policy = MAPPER.writeValueAsString(model.getModelPackageGroupPolicy()); + } + putModelPackageGroupPolicyRequest = PutModelPackageGroupPolicyRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName()) + .resourcePolicy(policy) + .build(); + } catch (JsonProcessingException e) { + throw new CfnInvalidRequestException(e); + } + return putModelPackageGroupPolicyRequest; + } + + /** + * Translates the model to Request to delete a model package group policy + * @param model resource model + * @return awsRequest the aws service request to delete a model package group policy + */ + static DeleteModelPackageGroupPolicyRequest translateToDeleteModelPackageGroupPolicyRequest(final ResourceModel model) { + return DeleteModelPackageGroupPolicyRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName()) + .build(); + } + + /** + * Translates the model to Request to get a model package group policy + * @param model resource model + * @return awsRequest the aws service request to get a model package group policy + */ + static GetModelPackageGroupPolicyRequest translateToGetModelPackageGroupPolicyRequest(final ResourceModel model) { + return GetModelPackageGroupPolicyRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName()) + .build(); + } + + /** + * Translates the model to Request to read a model package group + * @param model resource model + * @return awsRequest the aws service request to describe a model package group + */ + static DescribeModelPackageGroupRequest translateToReadRequest(final ResourceModel model) { + return DescribeModelPackageGroupRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupName() != null + ? model.getModelPackageGroupName() : model.getModelPackageGroupArn()).build(); + } + + /** + * 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 DescribeModelPackageGroupResponse awsResponse) { + // e.g. https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs/blob/2077c92299aeb9a68ae8f4418b5e932b12a8b186/aws-logs-loggroup/src/main/java/com/aws/logs/loggroup/Translator.java#L58-L73 + return ResourceModel.builder() + .creationTime(awsResponse.creationTime().toString()) + .modelPackageGroupArn(awsResponse.modelPackageGroupArn()) + .modelPackageGroupName(awsResponse.modelPackageGroupName()) + .modelPackageGroupDescription(awsResponse.modelPackageGroupDescription()) + .modelPackageGroupStatus(awsResponse.modelPackageGroupStatus().toString()) + .build(); + } + + /** + * Translates the model to Request to delete a model package group + * @param model resource model + * @return awsRequest the aws service request to delete a model package group + */ + static DeleteModelPackageGroupRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteModelPackageGroupRequest.builder() + .modelPackageGroupName(model.getModelPackageGroupArn() != null + ? model.getModelPackageGroupArn() : model.getModelPackageGroupName()) + .build(); + } + + /** + * Translates next token to Request to list model package groups + * @param nextToken token passed to the aws service list resources request + * @return awsRequest the aws service request to list model package groups within aws account + */ + static ListModelPackageGroupsRequest translateToListRequest(final String nextToken) { + return ListModelPackageGroupsRequest.builder() + .nextToken(nextToken).build(); + } + + /** + * Translates resource objects from sdk into list of resource models + * @param awsResponse the aws service list resource response + * @return list of resource models + */ + static List translateFromListResponse(final ListModelPackageGroupsResponse awsResponse) { + return Translator.streamOfOrEmpty(awsResponse.modelPackageGroupSummaryList()) + .map(summary -> ResourceModel.builder() + .creationTime(summary.creationTime().toString()) + .modelPackageGroupName(summary.modelPackageGroupName()) + .modelPackageGroupArn(summary.modelPackageGroupArn()) + .modelPackageGroupDescription(summary.modelPackageGroupDescription()) + .modelPackageGroupStatus(summary.modelPackageGroupStatus().toString()) + .build()) + .collect(Collectors.toList()); + } + + private static Stream streamOfOrEmpty(final Collection collection) { + return Optional.ofNullable(collection) + .map(Collection::stream) + .orElseGet(Stream::empty); + } + + /** + * Translates tag objects from sdk into list of tag objects in resource model + * @param tags, resource model tags + * @return list of sdk tags + */ + static List cfnTagsToSdkTags(final List tags) { + if (tags == null) { + return new ArrayList<>(); + } + for (final software.amazon.sagemaker.modelpackagegroup.Tag tag : tags) { + if (tag.getKey() == null) { + throw new CfnInvalidRequestException("Tags cannot have a null key"); + } + if (tag.getValue() == null) { + throw new CfnInvalidRequestException("Tags cannot have a null value"); + } + } + return tags.stream() + .map(e -> Tag.builder() + .key(e.getKey()) + .value(e.getValue()) + .build()) + .collect(Collectors.toList()); + } + + /** + * Translates tag objects from resource model into list of tag objects of sdk + * @param tags, sdk tags got from the aws service response + * @return list of resource model tags + */ + static List sdkTagsToCfnTags(final List tags) { + if (tags == null || tags.isEmpty()) { + return null; + } + final List cfnTags = + tags.stream() + .map(e -> software.amazon.sagemaker.modelpackagegroup.Tag.builder() + .key(e.key()) + .value(e.value()) + .build()) + .collect(Collectors.toList()); + return cfnTags; + } + + /** + * Translates the model to request to list tags of model package group + * @param model resource model + * @return awsRequest the aws service to list tags of model package group + */ + static ListTagsRequest translateToListTagsRequest(final ResourceModel model) { + return ListTagsRequest.builder() + .resourceArn(model.getModelPackageGroupArn()) + .build(); + } + + /** + * Construct add tags request from list of tags and model package group arn + * @param tagsToAdd, list of tags to be added to the model package group + * @param arn, arn of the model package group to which tags have to be added. + * @return awsRequest the aws service request to add tags to model package group + */ + static AddTagsRequest translateToAddTagsRequest(final List tagsToAdd, String arn) { + return AddTagsRequest.builder() + .resourceArn(arn) + .tags(tagsToAdd) + .build(); + } + + /** + * Construct delete tags request from list of tags and model package group arn + * @param tagsToDelete, list of tags to be deleted from the model package group + * @param arn, arn of the model package group from which tags have to be deleted. + * @return awsRequest the aws service request to add tags to model package group + */ + static DeleteTagsRequest translateToDeleteTagsRequest(final List tagsToDelete, String arn) { + return DeleteTagsRequest.builder() + .resourceArn(arn) + .tagKeys(tagsToDelete) + .build(); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandler.java b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandler.java new file mode 100644 index 0000000..6f03ec2 --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/main/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandler.java @@ -0,0 +1,156 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import com.amazonaws.util.CollectionUtils; +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.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.cloudformation.Action; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProxyClient; +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.stream.Collectors; + +public class UpdateHandler 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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> addModelPackageGroupArnIfNotAvailable(proxyClient, model, callbackContext)) + .then(progress -> updateResourcePolicy(proxyClient, model, callbackContext)) + .then(progress -> updateTags(proxyClient, model, callbackContext)) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + /** + * Adding the model package group arn if not available in the model + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent addModelPackageGroupArnIfNotAvailable( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + if (model.getModelPackageGroupArn() == null) { + DescribeModelPackageGroupResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToReadRequest(model), proxyClient.client()::describeModelPackageGroup); + model.setModelPackageGroupArn(response.modelPackageGroupArn()); + } + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + /** + * Client invocation of the update resource policy request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent updateResourcePolicy( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + if (model.getModelPackageGroupPolicy() != null) { + proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToPutModelPackageGroupPolicyRequest(model), proxyClient.client()::putModelPackageGroupPolicy); + } else { + try { + proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToDeleteModelPackageGroupPolicyRequest(model), proxyClient.client()::deleteModelPackageGroupPolicy); + } catch (AwsServiceException e) { + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().matches(".*Cannot find resource policy.*")) { + // policy already deleted or not available + } + else { + throw e; + } + } + } + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + /** + * Client invocation of the update tags request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent updateTags( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + handleTagging(proxyClient, model); + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, model.getModelPackageGroupName(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + /** + * Find the tag difference between existing model and updated model and update the diff tags to the model package group + * @param proxyClient the aws service client to make the call + * @param model the resource model + */ + private void handleTagging(final ProxyClient proxyClient, + final ResourceModel model) { + final Set newTags = new HashSet<>(Translator.cfnTagsToSdkTags(model.getTags())); + final Set existingTags + = new HashSet<>(proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToListTagsRequest(model), proxyClient.client()::listTags).tags()); + final List tagsToRemove = existingTags.stream() + .filter(tag -> !newTags.contains(tag)) + .map(tag -> tag.key()) + .collect(Collectors.toList()); + final List tagsToAdd = newTags.stream() + .filter(tag -> !existingTags.contains(tag)) + .collect(Collectors.toList()); + + if (!CollectionUtils.isNullOrEmpty(tagsToRemove)) { + proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToDeleteTagsRequest(tagsToRemove, model.getModelPackageGroupArn()), + proxyClient.client()::deleteTags); + } + if (!CollectionUtils.isNullOrEmpty(tagsToAdd)) { + proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToAddTagsRequest(tagsToAdd, model.getModelPackageGroupArn()), + proxyClient.client()::addTags); + } + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/AbstractTestBase.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/AbstractTestBase.java new file mode 100644 index 0000000..83b8f1a --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/AbstractTestBase.java @@ -0,0 +1,87 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import com.google.common.collect.ImmutableList; +import org.json.JSONObject; +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.awssdk.services.sagemaker.model.Tag; +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.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class AbstractTestBase { + protected static final Instant TEST_CREATION_TIME = Instant.now(); + protected static final String TEST_MODEL_PACKAGE_GROUP_ARN = "testModelPackageGroupArn"; + protected static final String TEST_MODEL_PACKAGE_GROUP_NAME = "testModelPackageGroupName"; + protected static final String TEST_ERROR_MESSAGE = "test error message"; + protected static final String MODEL_PACKAGE_GROUP_ALREADY_EXISTS_ERROR_MESSAGE = "Model Package Group already exists: sample_arn"; + protected static final String MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE = "ModelPackageGroup sample_arn does not exist."; + protected static final String CANNOT_FIND_MODEL_PACKAGE_GROUP_ERROR_MESSAGE = "Cannot find Model Package Group: sample_arn"; + protected static final String CANNOT_FIND_MODEL_PACKAGE_GROUP_POLICY_ERROR_MESSAGE = "Cannot find resource policy for: sample_arn"; + protected static final String TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT = "{\"policy\": \"test model package group policy\"}"; + protected static final List TEST_SDK_TAGS = ImmutableList.of(Tag.builder().key("key1").value("value1").build()); + protected static final List TEST_CFN_MODEL_TAGS + = ImmutableList.of(software.amazon.sagemaker.modelpackagegroup.Tag.builder() + .key("key1").value("value1").build()); + 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; + } + }; + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/CreateHandlerTest.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/CreateHandlerTest.java new file mode 100644 index 0000000..338c98e --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/CreateHandlerTest.java @@ -0,0 +1,530 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.CreateModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyResponse; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ResourceLimitExceededException; +import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException; +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.ProxyClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +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.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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 { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SageMakerClient sdkClient; + + private final ResourceModel requestModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + private final ResourceModel requestModelWithTags = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + private final ResourceModel requestModelWithResourcePolicy = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + private final ResourceModel requestModelWithTagsAndResourcePolicy = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + @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 DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(null) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_withTags() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(null) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModelWithTags) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_withResourcePolicy() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModelWithResourcePolicy) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_withTagsAndResourcePolicy() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModelWithResourcePolicy) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(serviceInternalException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_ModelPackageGroupAlreadyExists_Fails() { + final AwsServiceException resourceExistexception = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_ALREADY_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(resourceExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.AlreadyExists.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testCreateHandler_ResourceInUseException_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(resourceInUseException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.AlreadyExists.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testCreateHandler_ResourceLimitExceededException() { + final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(resourceLimitExceededException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_ValidationFailure() { + final AwsServiceException validationFailureException = SageMakerException.builder() + .message("1 validation error detected: Value null at 'modelPackageGroupName' " + + "failed to satisfy constraint: Member must not be null") + .statusCode(400) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(validationFailureException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnInvalidRequestException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.InvalidRequest.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_NoExceptionMessage() { + final AwsServiceException someException = SageMakerException.builder() + .statusCode(400) + .build(); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenThrow(someException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_VerifyStabilization_CompletedStatus() { + final DescribeModelPackageGroupResponse firstDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.PENDING) + .build(); + + final DescribeModelPackageGroupResponse secondDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(null) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse).thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse).thenReturn(listTagsResponse); + + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_VerifyStabilization_FailedStatus() { + final DescribeModelPackageGroupResponse firstDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.PENDING) + .build(); + + final DescribeModelPackageGroupResponse secondDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.DELETE_FAILED) + .build(); + + final CreateModelPackageGroupResponse createModelPackageGroupResponse = CreateModelPackageGroupResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().createModelPackageGroup(any(CreateModelPackageGroupRequest.class))) + .thenReturn(createModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + "Stabilizing of {\"/properties/ModelPackageGroupArn\":\"testModelPackageGroupArn\"} failed with unexpected status DeleteFailed"), + exception.getMessage()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.modelpackagegroup.CreateHandler handler = new software.amazon.sagemaker.modelpackagegroup.CreateHandler(); + return handler.handleRequest(proxy, request, new software.amazon.sagemaker.modelpackagegroup.CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandlerTest.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandlerTest.java new file mode 100644 index 0000000..4327c3b --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/DeleteHandlerTest.java @@ -0,0 +1,248 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +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.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +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 static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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.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 DeleteModelPackageGroupResponse deleteModelPackageGroupResponse = DeleteModelPackageGroupResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenReturn(deleteModelPackageGroupResponse); + + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + assertNull(response.getResourceModel()); + } + + @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().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenThrow(serviceInternalException); + + Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.DELETE), exception.getMessage()); + } + + @Test + public void testDeleteHandler_ModelPackageGroupDoesNotExists_Fails() { + final AwsServiceException resourceNotExistexception = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenThrow(resourceNotExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testDeleteHandler_ResourceNotFoundException_Fails() { + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete_WithResourceNotFoundException() { + final DescribeModelPackageGroupResponse firstDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.DELETING) + .build(); + + final DeleteModelPackageGroupResponse deleteModelPackageGroupResponse = DeleteModelPackageGroupResponse.builder() + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(firstDescribeResponse).thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenReturn(deleteModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete_WithModelPackageGroupNotExist() { + final DescribeModelPackageGroupResponse firstDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.DELETING) + .build(); + + final AwsServiceException resourceNotExistexception = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + final DeleteModelPackageGroupResponse deleteModelPackageGroupResponse = DeleteModelPackageGroupResponse.builder() + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(firstDescribeResponse).thenThrow(resourceNotExistexception); + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenReturn(deleteModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_ResourceNotDeleted() { + final DescribeModelPackageGroupResponse firstDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.PENDING) + .build(); + + final DescribeModelPackageGroupResponse secondDescribeResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.DELETE_FAILED) + .build(); + + final DeleteModelPackageGroupResponse deleteModelPackageGroupResponse = DeleteModelPackageGroupResponse.builder() + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().deleteModelPackageGroup(any(DeleteModelPackageGroupRequest.class))) + .thenReturn(deleteModelPackageGroupResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + "Delete stabilizing of model package group: " + TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .build(); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.modelpackagegroup.DeleteHandler handler = new DeleteHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ListHandlerTest.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ListHandlerTest.java new file mode 100644 index 0000000..6ce223c --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ListHandlerTest.java @@ -0,0 +1,154 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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.ModelPackageGroupSummary; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListModelPackageGroupsRequest; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.cloudformation.Action; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +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.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.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 ModelPackageGroupSummary modelPackageGroupSummary = ModelPackageGroupSummary.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + + final ListModelPackageGroupsResponse listModelPackageGroupsResponse = + ListModelPackageGroupsResponse.builder() + .modelPackageGroupSummaryList(modelPackageGroupSummary) + .nextToken(TEST_TOKEN) + .build(); + + when(proxyClient.client().listModelPackageGroups(any(ListModelPackageGroupsRequest.class))) + .thenReturn(listModelPackageGroupsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + List expectedModels = new ArrayList(); + expectedModels.add(expectedResourceModel); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getResourceModel()); + assertEquals(expectedModels, response.getResourceModels()); + assertEquals(TEST_TOKEN, response.getNextToken()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testListHandler_SimpleSuccess_NoModelPackageGroupExist() { + final ListModelPackageGroupsResponse listModelPackageGroupsResponse = + ListModelPackageGroupsResponse.builder() + .modelPackageGroupSummaryList(Collections.emptyList()) + .nextToken(null) + .build(); + + when(proxyClient.client().listModelPackageGroups(any(ListModelPackageGroupsRequest.class))) + .thenReturn(listModelPackageGroupsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getResourceModel()); + assertEquals(Collections.emptyList(), response.getResourceModels()); + assertNull(response.getNextToken()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testListHandler_ServiceInternalException() { + AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InternalError").build(); + AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build(); + + when(proxyClient.client().listModelPackageGroups(any(ListModelPackageGroupsRequest.class))) + .thenThrow(ex); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.ServiceInternalError.getMessage(), + Action.LIST.toString()), exception.getMessage()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.modelpackagegroup.ListHandler handler = new ListHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder().build(); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ReadHandlerTest.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ReadHandlerTest.java new file mode 100644 index 0000000..0595b1f --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/ReadHandlerTest.java @@ -0,0 +1,319 @@ +package software.amazon.sagemaker.modelpackagegroup; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyResponse; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.cloudformation.Action; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +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.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.verify; +import static org.mockito.Mockito.when; + +@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() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class)); + } + + @Test + public void testReadHandler_WithEmptyTags() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class)); + } + + @Test + public void testReadHandler_WithNullResourcePolicy() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = + GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(null) + .build(); + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class)); + } + + @Test + public void testReadHandler_WithCannotFindResourcePolicy() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final AwsServiceException resourcePolicyNotFoundException = SageMakerException.builder() + .message(CANNOT_FIND_MODEL_PACKAGE_GROUP_POLICY_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenThrow(resourcePolicyNotFoundException); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenThrow(serviceInternalException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.READ), exception.getMessage()); + } + + @Test + public void testReadHandler_ModelPackageGroupNotExist_Fails() { + final AwsServiceException resourceNotExistexception = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenThrow(resourceNotExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + + @Test + public void testReadHandler_ResourceNotFoundException() { + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.modelpackagegroup.ReadHandler handler = new ReadHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .build(); + } +} diff --git a/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandlerTest.java b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandlerTest.java new file mode 100644 index 0000000..eb0e6fb --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/src/test/java/software/amazon/sagemaker/modelpackagegroup/UpdateHandlerTest.java @@ -0,0 +1,545 @@ +package software.amazon.sagemaker.modelpackagegroup; + +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.DescribeModelPackageGroupResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelPackageGroupStatus; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelPackageGroupRequest; +import software.amazon.awssdk.services.sagemaker.model.PutModelPackageGroupPolicyResponse; +import software.amazon.awssdk.services.sagemaker.model.PutModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyResponse; +import software.amazon.awssdk.services.sagemaker.model.GetModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelPackageGroupPolicyRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.AddTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.AddTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +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.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import java.time.Duration; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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) +class UpdateHandlerTest 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 testUpdateHandler_SimpleSuccess() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testUpdateHandler_SimpleSuccess_AddTags() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponseWithTags = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + final ListTagsResponse listTagsResponseWithoutTags = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponseWithoutTags).thenReturn(listTagsResponseWithTags); + + final AddTagsResponse addTagsResponse = + AddTagsResponse.builder().build(); + when(proxyClient.client().addTags(any(AddTagsRequest.class))) + .thenReturn(addTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithBothTagsAndResourcePolicy()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testUpdateHandler_SimpleSuccess_DeleteTags() { + final DescribeModelPackageGroupResponse describeModelPackageGroupResponse = + DescribeModelPackageGroupResponse.builder() + .creationTime(TEST_CREATION_TIME) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED) + .build(); + when(proxyClient.client().describeModelPackageGroup(any(DescribeModelPackageGroupRequest.class))) + .thenReturn(describeModelPackageGroupResponse); + + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final GetModelPackageGroupPolicyResponse getModelPackageGroupPolicyResponse = GetModelPackageGroupPolicyResponse.builder() + .resourcePolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .build(); + + when(proxyClient.client().getModelPackageGroupPolicy(any(GetModelPackageGroupPolicyRequest.class))) + .thenReturn(getModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponseWithTags = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + final ListTagsResponse listTagsResponseWithoutTags = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponseWithTags).thenReturn(listTagsResponseWithoutTags); + + final DeleteTagsResponse deleteTagsResponse = + DeleteTagsResponse.builder().build(); + when(proxyClient.client().deleteTags(any(DeleteTagsRequest.class))) + .thenReturn(deleteTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testUpdateHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenThrow(serviceInternalException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.UPDATE), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingResourcePolicy_PutPolicy() { + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingResourcePolicy_DeletePolicy() { + when(proxyClient.client().deleteModelPackageGroupPolicy(any(DeleteModelPackageGroupPolicyRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ModelpackagegroupNotFound_UpdatingResourcePolicy_PutPolicy() { + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(CANNOT_FIND_MODEL_PACKAGE_GROUP_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ModelpackagegroupNotFound_UpdatingResourcePolicy_DeletePolicy() { + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(CANNOT_FIND_MODEL_PACKAGE_GROUP_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().deleteModelPackageGroupPolicy(any(DeleteModelPackageGroupPolicyRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithBothTagsAndResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingTags_AddTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + when(proxyClient.client().addTags(any(AddTagsRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithBothTagsAndResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingTags_DeleteTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + when(proxyClient.client().deleteTags(any(DeleteTagsRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ModelPackageGroupNotExists_UpdatingTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ModelPackageGroupNotExists_UpdatingTags_AddTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().addTags(any(AddTagsRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithBothTagsAndResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ModelPackageGroupNotExists_UpdatingTags_DeleteTags() { + final PutModelPackageGroupPolicyResponse putModelPackageGroupPolicyResponse = PutModelPackageGroupPolicyResponse.builder() + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .build(); + + when(proxyClient.client().putModelPackageGroupPolicy(any(PutModelPackageGroupPolicyRequest.class))) + .thenReturn(putModelPackageGroupPolicyResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(MODEL_PACKAGE_GROUP_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().deleteTags(any(DeleteTagsRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithResourcePolicy()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.modelpackagegroup.ResourceModel.TYPE_NAME, TEST_MODEL_PACKAGE_GROUP_NAME), exception.getMessage()); + } + + private ResourceModel getRequestResourceModelWithResourcePolicy() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + } + + private ResourceModel getRequestResourceModelWithBothTagsAndResourcePolicy() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupPolicy(TEST_MODEL_PACKAGE_GROUP_POLICY_TEXT) + .tags(TEST_CFN_MODEL_TAGS) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .modelPackageGroupName(TEST_MODEL_PACKAGE_GROUP_NAME) + .modelPackageGroupArn(TEST_MODEL_PACKAGE_GROUP_ARN) + .modelPackageGroupStatus(ModelPackageGroupStatus.COMPLETED.toString()) + .build(); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.modelpackagegroup.UpdateHandler handler = new UpdateHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-modelpackagegroup/template.yml b/aws-sagemaker-modelpackagegroup/template.yml new file mode 100644 index 0000000..f23e4aa --- /dev/null +++ b/aws-sagemaker-modelpackagegroup/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::ModelPackageGroup 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.modelpackagegroup.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelpackagegroup-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.modelpackagegroup.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelpackagegroup-handler-1.0-SNAPSHOT.jar diff --git a/aws-sagemaker-modelqualityjobdefinition/.rpdk-config b/aws-sagemaker-modelqualityjobdefinition/.rpdk-config new file mode 100644 index 0000000..178d4a0 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::SageMaker::ModelQualityJobDefinition", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.modelqualityjobdefinition.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.modelqualityjobdefinition.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "sagemaker", + "modelqualityjobdefinition" + ], + "protocolVersion": "2.0.0" + } +} \ No newline at end of file diff --git a/aws-sagemaker-modelqualityjobdefinition/README.md b/aws-sagemaker-modelqualityjobdefinition/README.md new file mode 100644 index 0000000..bb17e29 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/README.md @@ -0,0 +1,178 @@ +# AWS::SageMaker::ModelQualityJobDefinition + +Resource Type definition for AWS::SageMaker::ModelQualityJobDefinition + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelQualityJobDefinition",
+    "Properties" : {
+        "JobDefinitionName" : String,
+        "ModelQualityBaselineConfig" : ModelQualityBaselineConfig,
+        "ModelQualityAppSpecification" : ModelQualityAppSpecification,
+        "ModelQualityJobInput" : ModelQualityJobInput,
+        "ModelQualityJobOutputConfig" : MonitoringOutputConfig,
+        "JobResources" : MonitoringResources,
+        "NetworkConfig" : NetworkConfig,
+        "RoleArn" : String,
+        "StoppingCondition" : StoppingCondition,
+        "Tags" : [ Tag, ... ],
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelQualityJobDefinition
+Properties:
+    JobDefinitionName: String
+    ModelQualityBaselineConfig: ModelQualityBaselineConfig
+    ModelQualityAppSpecification: ModelQualityAppSpecification
+    ModelQualityJobInput: ModelQualityJobInput
+    ModelQualityJobOutputConfig: 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) + +#### ModelQualityBaselineConfig + +Baseline configuration used to validate that the data conforms to the specified constraints and statistics. + +_Required_: No + +_Type_: ModelQualityBaselineConfig + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityAppSpecification + +Container image configuration object for the monitoring job. + +_Required_: Yes + +_Type_: ModelQualityAppSpecification + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityJobInput + +The inputs for a monitoring job. + +_Required_: Yes + +_Type_: ModelQualityJobInput + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityJobOutputConfig + +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-modelqualityjobdefinition/aws-sagemaker-modelqualityjobdefinition.json b/aws-sagemaker-modelqualityjobdefinition/aws-sagemaker-modelqualityjobdefinition.json new file mode 100644 index 0000000..f1803e7 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/aws-sagemaker-modelqualityjobdefinition.json @@ -0,0 +1,496 @@ +{ + "typeName" : "AWS::SageMaker::ModelQualityJobDefinition", + "description" : "Resource Type definition for AWS::SageMaker::ModelQualityJobDefinition", + "additionalProperties" : false, + "properties" : { + "JobDefinitionArn" : { + "description": "The Amazon Resource Name (ARN) of job definition.", + "type" : "string", + "minLength": 1, + "maxLength": 256 + }, + "JobDefinitionName" : { + "$ref" : "#/definitions/JobDefinitionName" + }, + "ModelQualityBaselineConfig": { + "$ref": "#/definitions/ModelQualityBaselineConfig" + }, + "ModelQualityAppSpecification": { + "$ref": "#/definitions/ModelQualityAppSpecification" + }, + "ModelQualityJobInput": { + "$ref": "#/definitions/ModelQualityJobInput" + }, + "ModelQualityJobOutputConfig": { + "$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" : { + "ModelQualityBaselineConfig" : { + "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 + } + } + }, + "ModelQualityAppSpecification" : { + "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" + }, + "ProblemType": { + "$ref": "#/definitions/ProblemType" + } + }, + "required" : [ "ImageUri", "ProblemType" ] + }, + "ModelQualityJobInput" : { + "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" + }, + "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 + }, + "ProblemType": { + "description": "The status of the monitoring job.", + "type": "string", + "enum": [ + "BinaryClassification", + "MulticlassClassification", + "Regression" + ] + }, + "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" : [ "ModelQualityAppSpecification", "ModelQualityJobInput", "ModelQualityJobOutputConfig", "JobResources", "RoleArn"], + "primaryIdentifier" : [ "/properties/JobDefinitionArn" ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateModelQualityJobDefinition", + "sagemaker:DescribeModelQualityJobDefinition", + "iam:PassRole" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteModelQualityJobDefinition" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeModelQualityJobDefinition" + ] + }, + "update": { + "permissions": [] + } + }, + "readOnlyProperties": [ + "/properties/CreationTime", + "/properties/JobDefinitionArn" + ], + "createOnlyProperties": [ + "/properties/JobDefinitionName", + "/properties/ModelQualityAppSpecification", + "/properties/ModelQualityBaselineConfig", + "/properties/ModelQualityJobInput", + "/properties/ModelQualityJobOutputConfig", + "/properties/JobResources", + "/properties/NetworkConfig", + "/properties/RoleArn", + "/properties/StoppingCondition", + "/properties/Tags" + ] +} \ No newline at end of file diff --git a/aws-sagemaker-modelqualityjobdefinition/docs/README.md b/aws-sagemaker-modelqualityjobdefinition/docs/README.md new file mode 100644 index 0000000..bb17e29 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/README.md @@ -0,0 +1,178 @@ +# AWS::SageMaker::ModelQualityJobDefinition + +Resource Type definition for AWS::SageMaker::ModelQualityJobDefinition + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::ModelQualityJobDefinition",
+    "Properties" : {
+        "JobDefinitionName" : String,
+        "ModelQualityBaselineConfig" : ModelQualityBaselineConfig,
+        "ModelQualityAppSpecification" : ModelQualityAppSpecification,
+        "ModelQualityJobInput" : ModelQualityJobInput,
+        "ModelQualityJobOutputConfig" : MonitoringOutputConfig,
+        "JobResources" : MonitoringResources,
+        "NetworkConfig" : NetworkConfig,
+        "RoleArn" : String,
+        "StoppingCondition" : StoppingCondition,
+        "Tags" : [ Tag, ... ],
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::ModelQualityJobDefinition
+Properties:
+    JobDefinitionName: String
+    ModelQualityBaselineConfig: ModelQualityBaselineConfig
+    ModelQualityAppSpecification: ModelQualityAppSpecification
+    ModelQualityJobInput: ModelQualityJobInput
+    ModelQualityJobOutputConfig: 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) + +#### ModelQualityBaselineConfig + +Baseline configuration used to validate that the data conforms to the specified constraints and statistics. + +_Required_: No + +_Type_: ModelQualityBaselineConfig + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityAppSpecification + +Container image configuration object for the monitoring job. + +_Required_: Yes + +_Type_: ModelQualityAppSpecification + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityJobInput + +The inputs for a monitoring job. + +_Required_: Yes + +_Type_: ModelQualityJobInput + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ModelQualityJobOutputConfig + +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-modelqualityjobdefinition/docs/clusterconfig.md b/aws-sagemaker-modelqualityjobdefinition/docs/clusterconfig.md new file mode 100644 index 0000000..16015ab --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/clusterconfig.md @@ -0,0 +1,70 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/constraintsresource.md b/aws-sagemaker-modelqualityjobdefinition/docs/constraintsresource.md new file mode 100644 index 0000000..34721b2 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/constraintsresource.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/endpointinput.md b/aws-sagemaker-modelqualityjobdefinition/docs/endpointinput.md new file mode 100644 index 0000000..4186096 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/endpointinput.md @@ -0,0 +1,156 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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,
+    "InferenceAttribute" : String,
+    "ProbabilityAttribute" : String,
+    "ProbabilityThresholdAttribute" : Double
+}
+
+ +### YAML + +
+EndpointName: String
+LocalPath: String
+S3DataDistributionType: String
+S3InputMode: String
+StartTimeOffset: String
+EndTimeOffset: 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) + +#### 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-modelqualityjobdefinition/docs/modelqualityappspecification-environment.md b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityappspecification-environment.md new file mode 100644 index 0000000..2e31d07 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityappspecification-environment.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::ModelQualityJobDefinition ModelQualityAppSpecification 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-modelqualityjobdefinition/docs/modelqualityappspecification.md b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityappspecification.md new file mode 100644 index 0000000..4e83ba0 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityappspecification.md @@ -0,0 +1,122 @@ +# AWS::SageMaker::ModelQualityJobDefinition ModelQualityAppSpecification + +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,
+    "ProblemType" : String
+}
+
+ +### YAML + +
+ContainerArguments: 
+      - String
+ContainerEntrypoint: 
+      - String
+ImageUri: String
+PostAnalyticsProcessorSourceUri: String
+RecordPreprocessorSourceUri: String
+Environment: Environment
+ProblemType: String
+
+ +## 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) + +#### ProblemType + +The status of the monitoring job. + +_Required_: Yes + +_Type_: String + +_Allowed Values_: BinaryClassification | MulticlassClassification | Regression + +_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-modelqualityjobdefinition/docs/modelqualitybaselineconfig.md b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualitybaselineconfig.md new file mode 100644 index 0000000..bb82903 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualitybaselineconfig.md @@ -0,0 +1,52 @@ +# AWS::SageMaker::ModelQualityJobDefinition ModelQualityBaselineConfig + +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-modelqualityjobdefinition/docs/modelqualityjobinput.md b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityjobinput.md new file mode 100644 index 0000000..f2df566 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/modelqualityjobinput.md @@ -0,0 +1,46 @@ +# AWS::SageMaker::ModelQualityJobDefinition ModelQualityJobInput + +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-modelqualityjobdefinition/docs/monitoringgroundtruths3input.md b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringgroundtruths3input.md new file mode 100644 index 0000000..f19b282 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringgroundtruths3input.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/monitoringoutput.md b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringoutput.md new file mode 100644 index 0000000..c6be608 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringoutput.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/monitoringoutputconfig.md b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringoutputconfig.md new file mode 100644 index 0000000..ea45eb1 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringoutputconfig.md @@ -0,0 +1,55 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/monitoringresources.md b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringresources.md new file mode 100644 index 0000000..d31b3cd --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/monitoringresources.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/networkconfig.md b/aws-sagemaker-modelqualityjobdefinition/docs/networkconfig.md new file mode 100644 index 0000000..a52d368 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/networkconfig.md @@ -0,0 +1,58 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/s3output.md b/aws-sagemaker-modelqualityjobdefinition/docs/s3output.md new file mode 100644 index 0000000..022b1e6 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/s3output.md @@ -0,0 +1,68 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/stoppingcondition.md b/aws-sagemaker-modelqualityjobdefinition/docs/stoppingcondition.md new file mode 100644 index 0000000..55ac11a --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/stoppingcondition.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/tag.md b/aws-sagemaker-modelqualityjobdefinition/docs/tag.md new file mode 100644 index 0000000..2c46d7d --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/tag.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/docs/vpcconfig.md b/aws-sagemaker-modelqualityjobdefinition/docs/vpcconfig.md new file mode 100644 index 0000000..b934b5c --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/docs/vpcconfig.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::ModelQualityJobDefinition 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-modelqualityjobdefinition/lombok.config b/aws-sagemaker-modelqualityjobdefinition/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-modelqualityjobdefinition/pom.xml b/aws-sagemaker-modelqualityjobdefinition/pom.xml new file mode 100644 index 0000000..0d874a2 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + software.amazon.sagemaker.modelqualityjobdefinition + aws-sagemaker-modelqualityjobdefinition-handler + aws-sagemaker-modelqualityjobdefinition-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-modelqualityjobdefinition.json + + + + + \ No newline at end of file diff --git a/aws-sagemaker-modelqualityjobdefinition/resource-role.yaml b/aws-sagemaker-modelqualityjobdefinition/resource-role.yaml new file mode 100644 index 0000000..0dab45d --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/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:CreateModelQualityJobDefinition" + - "sagemaker:DeleteModelQualityJobDefinition" + - "sagemaker:DescribeModelQualityJobDefinition" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/BaseHandlerStd.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/BaseHandlerStd.java new file mode 100644 index 0000000..1ac8a3c --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/BaseHandlerStd.java @@ -0,0 +1,38 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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_QUALITY_ARN_SUBSTRING = ":model-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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CallbackContext.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CallbackContext.java new file mode 100644 index 0000000..283abd8 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CallbackContext.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ClientBuilder.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ClientBuilder.java new file mode 100644 index 0000000..d2e4eec --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ClientBuilder.java @@ -0,0 +1,12 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Configuration.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Configuration.java new file mode 100644 index 0000000..e80d5b9 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Configuration.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +class Configuration extends BaseConfiguration { + public Configuration() { + super("aws-sagemaker-modelqualityjobdefinition.json"); + } +} \ No newline at end of file diff --git a/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandler.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandler.java new file mode 100644 index 0000000..ba231af --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandler.java @@ -0,0 +1,108 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.CreateModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelQualityJobDefinitionResponse; +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-ModelQualityJobDefinition::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 CreateModelQualityJobDefinitionResponse createResource( + final CreateModelQualityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient) { + + CreateModelQualityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createModelQualityJobDefinition); + } 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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandler.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandler.java new file mode 100644 index 0000000..5eba453 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandler.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.DeleteModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelQualityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +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.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; +import software.amazon.sagemaker.modelqualityjobdefinition.Utils; + +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_QUALITY_ARN_SUBSTRING); + model.setJobDefinitionName(jobDefinitionName); + } + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-ModelQualityJobDefinition::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 DeleteModelQualityJobDefinitionResponse deleteResource( + final DeleteModelQualityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient) { + + DeleteModelQualityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deleteModelQualityJobDefinition); + } 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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandler.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandler.java new file mode 100644 index 0000000..2c10734 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandler.java @@ -0,0 +1,82 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.DescribeModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionResponse; +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; +import software.amazon.sagemaker.modelqualityjobdefinition.Utils; + +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_QUALITY_ARN_SUBSTRING); + model.setJobDefinitionName(jobDefinitionName); + } + + return proxy.initiate("AWS-SageMaker-ModelQualityJobDefinition::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 DescribeModelQualityJobDefinitionResponse readResource( + final DescribeModelQualityJobDefinitionRequest awsRequest, + final ProxyClient proxyClient, + final ResourceModel model) { + + DescribeModelQualityJobDefinitionResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeModelQualityJobDefinition); + } 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 DescribeModelQualityJobDefinitionResponse awsResponse) { + return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse)); + } +} diff --git a/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Translator.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Translator.java new file mode 100644 index 0000000..f76e6a1 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Translator.java @@ -0,0 +1,61 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForRequest.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForRequest.java new file mode 100644 index 0000000..0cd1f69 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForRequest.java @@ -0,0 +1,201 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +import software.amazon.awssdk.services.sagemaker.model.CreateModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityJobInput; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionRequest; +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.ProblemType; +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 createModelQualityJobDefinitionRequest - service request to create a resource + */ + static CreateModelQualityJobDefinitionRequest translateToCreateRequest(final ResourceModel model) { + return CreateModelQualityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .modelQualityAppSpecification(translate(model.getModelQualityAppSpecification())) + .modelQualityBaselineConfig(translate(model.getModelQualityBaselineConfig())) + .modelQualityJobInput(translate(model.getModelQualityJobInput())) + .modelQualityJobOutputConfig(translate(model.getModelQualityJobOutputConfig())) + .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 describeModelQualityJobDefinitionRequest - the aws service request to describe a resource + */ + static DescribeModelQualityJobDefinitionRequest translateToReadRequest(final ResourceModel model) { + return DescribeModelQualityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .build(); + } + + /** + * Request to delete a resource + * @param model resource model + * @return deleteModelQualityJobDefinitionRequest the aws service request to delete a resource + */ + static DeleteModelQualityJobDefinitionRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteModelQualityJobDefinitionRequest.builder() + .jobDefinitionName(model.getJobDefinitionName()) + .build(); + } + + static ModelQualityAppSpecification translate(final software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityAppSpecification appSpec) { + return appSpec == null ? null : ModelQualityAppSpecification.builder() + .containerArguments(appSpec.getContainerArguments()) + .containerEntrypoint(appSpec.getContainerEntrypoint()) + .imageUri(appSpec.getImageUri()) + .postAnalyticsProcessorSourceUri(appSpec.getPostAnalyticsProcessorSourceUri()) + .recordPreprocessorSourceUri(appSpec.getRecordPreprocessorSourceUri()) + .problemType(appSpec.getProblemType()) + .environment(translateMapOfObjectsToMapOfStrings(appSpec.getEnvironment())) + .build(); + } + + static ModelQualityBaselineConfig translate(final software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig baselineConfig) { + return baselineConfig == null ? null : ModelQualityBaselineConfig.builder() + .baseliningJobName(baselineConfig.getBaseliningJobName()) + .constraintsResource(translate(baselineConfig.getConstraintsResource())) + .build(); + } + + static MonitoringConstraintsResource translate(final software.amazon.sagemaker.modelqualityjobdefinition.ConstraintsResource constraintsResource) { + return constraintsResource == null ? null : MonitoringConstraintsResource.builder().s3Uri(constraintsResource.getS3Uri()).build(); + } + + static ModelQualityJobInput translate(final software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityJobInput jobInput) { + return jobInput == null ? null : ModelQualityJobInput.builder() + .endpointInput(translate(jobInput.getEndpointInput())) + .groundTruthS3Input(translate(jobInput.getGroundTruthS3Input())) + .build(); + } + static EndpointInput translate(final software.amazon.sagemaker.modelqualityjobdefinition.EndpointInput endpointInput) { + return endpointInput == null ? null : EndpointInput.builder() + .endpointName(endpointInput.getEndpointName()) + .localPath(endpointInput.getLocalPath()) + .s3DataDistributionType(endpointInput.getS3DataDistributionType()) + .s3InputMode(endpointInput.getS3InputMode()) + .inferenceAttribute(endpointInput.getInferenceAttribute()) + .probabilityAttribute(endpointInput.getProbabilityAttribute()) + .probabilityThresholdAttribute(endpointInput.getProbabilityThresholdAttribute()) + .startTimeOffset(endpointInput.getStartTimeOffset()) + .endTimeOffset(endpointInput.getEndTimeOffset()) + .build(); + } + static MonitoringOutputConfig translate(final software.amazon.sagemaker.modelqualityjobdefinition.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.modelqualityjobdefinition.MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.getS3Output())) + .build(); + } + + static MonitoringS3Output translate(final software.amazon.sagemaker.modelqualityjobdefinition.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.modelqualityjobdefinition.MonitoringResources monitoringResources) { + return monitoringResources == null? null : MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.getClusterConfig())) + .build(); + } + + static MonitoringClusterConfig translate(final software.amazon.sagemaker.modelqualityjobdefinition.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.modelqualityjobdefinition.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.modelqualityjobdefinition.VpcConfig vpcConfig) { + return vpcConfig == null? null : VpcConfig.builder() + .securityGroupIds(vpcConfig.getSecurityGroupIds()) + .subnets(vpcConfig.getSubnets()) + .build(); + } + + static MonitoringStoppingCondition translate(final software.amazon.sagemaker.modelqualityjobdefinition.StoppingCondition stoppingCondition) { + return stoppingCondition == null? null : MonitoringStoppingCondition.builder() + .maxRuntimeInSeconds(stoppingCondition.getMaxRuntimeInSeconds()) + .build(); + } + + static MonitoringGroundTruthS3Input translate(final software.amazon.sagemaker.modelqualityjobdefinition.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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForResponse.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForResponse.java new file mode 100644 index 0000000..235978a --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorForResponse.java @@ -0,0 +1,176 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +import software.amazon.awssdk.services.sagemaker.model.ModelQualityAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityJobInput; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionResponse; +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 DescribeModelQualityJobDefinitionResponse awsResponse) { + return ResourceModel.builder() + .jobDefinitionArn(awsResponse.jobDefinitionArn()) + .jobDefinitionName(awsResponse.jobDefinitionName()) + .creationTime(awsResponse.creationTime().toString()) + .modelQualityBaselineConfig(translate(awsResponse.modelQualityBaselineConfig())) + .modelQualityAppSpecification(translate(awsResponse.modelQualityAppSpecification())) + .modelQualityJobInput(translate(awsResponse.modelQualityJobInput())) + .modelQualityJobOutputConfig(translate(awsResponse.modelQualityJobOutputConfig())) + .jobResources(translate(awsResponse.jobResources())) + .networkConfig(translate(awsResponse.networkConfig())) + .roleArn(awsResponse.roleArn()) + .stoppingCondition(translate(awsResponse.stoppingCondition())) + .build(); + } + + + static software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig translate( + final ModelQualityBaselineConfig baselineConfig) { + return baselineConfig == null? null : software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig.builder() + .baseliningJobName(baselineConfig.baseliningJobName()) + .constraintsResource(translate(baselineConfig.constraintsResource())) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.ConstraintsResource translate( + final MonitoringConstraintsResource constraintsResource) { + return constraintsResource == null? null : software.amazon.sagemaker.modelqualityjobdefinition.ConstraintsResource.builder() + .s3Uri(constraintsResource.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityAppSpecification translate( + final ModelQualityAppSpecification monitoringAppSpec) { + return monitoringAppSpec == null ? null : software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityAppSpecification.builder() + .containerArguments(monitoringAppSpec.containerArguments()) + .containerEntrypoint(monitoringAppSpec.containerEntrypoint()) + .imageUri(monitoringAppSpec.imageUri()) + .postAnalyticsProcessorSourceUri(monitoringAppSpec.postAnalyticsProcessorSourceUri()) + .recordPreprocessorSourceUri(monitoringAppSpec.recordPreprocessorSourceUri()) + .problemType(monitoringAppSpec.problemType().toString()) + .environment(translateMapOfStringsMapOfObjects(monitoringAppSpec.environment())) + .build(); + } + + + static software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityJobInput translate(final ModelQualityJobInput monitoringInput) { + return monitoringInput == null ? null : software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityJobInput.builder() + .endpointInput(translate(monitoringInput.endpointInput())) + .groundTruthS3Input(translate(monitoringInput.groundTruthS3Input())) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.EndpointInput translate(final EndpointInput endpointInput) { + return endpointInput == null ? null : software.amazon.sagemaker.modelqualityjobdefinition.EndpointInput.builder() + .endpointName(endpointInput.endpointName()) + .localPath(endpointInput.localPath()) + .s3DataDistributionType(endpointInput.s3DataDistributionType().toString()) + .s3InputMode(endpointInput.s3InputMode().toString()) + .inferenceAttribute(endpointInput.inferenceAttribute()) + .probabilityAttribute(endpointInput.probabilityAttribute()) + .probabilityThresholdAttribute(endpointInput.probabilityThresholdAttribute()) + .startTimeOffset(endpointInput.startTimeOffset()) + .endTimeOffset(endpointInput.endTimeOffset()) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.MonitoringOutputConfig translate(final MonitoringOutputConfig outputConfig) { + return outputConfig == null? null : software.amazon.sagemaker.modelqualityjobdefinition.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.modelqualityjobdefinition.MonitoringOutput translate(final MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : software.amazon.sagemaker.modelqualityjobdefinition.MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.s3Output())) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.S3Output translate(final MonitoringS3Output s3Output) { + return s3Output == null? null : software.amazon.sagemaker.modelqualityjobdefinition.S3Output.builder() + .localPath(s3Output.localPath()) + .s3UploadMode(s3Output.s3UploadMode().toString()) + .s3Uri(s3Output.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.MonitoringResources translate(final MonitoringResources monitoringResources) { + return monitoringResources == null? null : software.amazon.sagemaker.modelqualityjobdefinition.MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.clusterConfig())) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.ClusterConfig translate(final MonitoringClusterConfig clusterConfig) { + return clusterConfig == null? null : software.amazon.sagemaker.modelqualityjobdefinition.ClusterConfig.builder() + .instanceCount(clusterConfig.instanceCount()) + .instanceType(clusterConfig.instanceType().toString()) + .volumeKmsKeyId(clusterConfig.volumeKmsKeyId()) + .volumeSizeInGB(clusterConfig.volumeSizeInGB()) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.NetworkConfig translate(final MonitoringNetworkConfig networkConfig) { + return networkConfig == null? null : software.amazon.sagemaker.modelqualityjobdefinition.NetworkConfig.builder() + .enableInterContainerTrafficEncryption(networkConfig.enableInterContainerTrafficEncryption()) + .enableNetworkIsolation(networkConfig.enableNetworkIsolation()) + .vpcConfig(translate(networkConfig.vpcConfig())) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.VpcConfig translate(final VpcConfig vpcConfig) { + return vpcConfig == null? null : software.amazon.sagemaker.modelqualityjobdefinition.VpcConfig.builder() + .securityGroupIds(vpcConfig.securityGroupIds()) + .subnets(vpcConfig.subnets()) + .build(); + } + + static software.amazon.sagemaker.modelqualityjobdefinition.StoppingCondition translate(final MonitoringStoppingCondition stoppingCondition) { + return stoppingCondition == null? null : software.amazon.sagemaker.modelqualityjobdefinition.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.modelqualityjobdefinition.MonitoringGroundTruthS3Input translate(final MonitoringGroundTruthS3Input s3Input) { + return s3Input == null? null : software.amazon.sagemaker.modelqualityjobdefinition.MonitoringGroundTruthS3Input.builder() + .s3Uri(s3Input.s3Uri()) + .build(); + } + +} \ No newline at end of file diff --git a/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/UpdateHandler.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/UpdateHandler.java new file mode 100644 index 0000000..23fb70a --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/UpdateHandler.java @@ -0,0 +1,27 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Utils.java b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Utils.java new file mode 100644 index 0000000..ae3b321 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/main/java/software/amazon/sagemaker/modelqualityjobdefinition/Utils.java @@ -0,0 +1,25 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + + +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-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/AbstractTestBase.java b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/AbstractTestBase.java new file mode 100644 index 0000000..6d86199 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/AbstractTestBase.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-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-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandlerTest.java b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandlerTest.java new file mode 100644 index 0000000..71a96b1 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/CreateHandlerTest.java @@ -0,0 +1,240 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.CreateModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateModelQualityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionResponse; +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.modelqualityjobdefinition.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 DescribeModelQualityJobDefinitionResponse describeModelQualityJobDefinitionResponse = + DescribeModelQualityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelQualityJobDefinitionResponse createModelQualityJobDefinitionResponse = CreateModelQualityJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class))) + .thenReturn(describeModelQualityJobDefinitionResponse); + when(proxyClient.client().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.class))) + .thenReturn(createModelQualityJobDefinitionResponse); + + 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 DescribeModelQualityJobDefinitionResponse describeModelQualityJobDefinitionResponse = + DescribeModelQualityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final CreateModelQualityJobDefinitionResponse createModelQualityJobDefinitionResponse = CreateModelQualityJobDefinitionResponse.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class))) + .thenReturn(describeModelQualityJobDefinitionResponse); + when(proxyClient.client().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.class))) + .thenReturn(createModelQualityJobDefinitionResponse); + + 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().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.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_ModelQualityJobDefinitionAlreadyExists_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.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().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.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().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.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().createModelQualityJobDefinition(any(CreateModelQualityJobDefinitionRequest.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-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandlerTest.java b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandlerTest.java new file mode 100644 index 0000000..2efb273 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/DeleteHandlerTest.java @@ -0,0 +1,154 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.DeleteModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteModelQualityJobDefinitionResponse; +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.modelqualityjobdefinition.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 DeleteModelQualityJobDefinitionResponse deleteModelQualityJobDefinitionResponse = DeleteModelQualityJobDefinitionResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().deleteModelQualityJobDefinition(any(DeleteModelQualityJobDefinitionRequest.class))) + .thenReturn(deleteModelQualityJobDefinitionResponse); + + 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 DeleteModelQualityJobDefinitionResponse deleteModelQualityJobDefinitionResponse = DeleteModelQualityJobDefinitionResponse.builder() + .build(); + + ResourceModel resourceModel = ResourceModel.builder() + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(resourceModel) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + DeleteModelQualityJobDefinitionRequest.class); + + + when(proxyClient.client().deleteModelQualityJobDefinition(any(DeleteModelQualityJobDefinitionRequest.class))) + .thenReturn(deleteModelQualityJobDefinitionResponse); + + final ProgressEvent response = invokeHandleRequest(request); + + verify(proxyClient.client(), times(1)).deleteModelQualityJobDefinition(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().deleteModelQualityJobDefinition(any(DeleteModelQualityJobDefinitionRequest.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_ModelQualityJobDefinitionDoesNotExists_Fails() { + when(proxyClient.client().deleteModelQualityJobDefinition(any(DeleteModelQualityJobDefinitionRequest.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-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandlerTest.java b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandlerTest.java new file mode 100644 index 0000000..4cc0a26 --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/ReadHandlerTest.java @@ -0,0 +1,223 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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.DescribeModelQualityJobDefinitionRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeModelQualityJobDefinitionResponse; +import software.amazon.awssdk.services.sagemaker.model.ModelQualityBaselineConfig; +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.modelqualityjobdefinition.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() { + + ModelQualityBaselineConfig modelQualityBaselineConfig = ModelQualityBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelQualityJobDefinitionResponse describeModelQualityJobDefinitionResponse = + DescribeModelQualityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelQualityBaselineConfig(modelQualityBaselineConfig) + .roleArn(TEST_ARN) + .build(); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class))) + .thenReturn(describeModelQualityJobDefinitionResponse); + + software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig.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) + .modelQualityBaselineConfig(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()).describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_WithoutJobDefinitionName_Success() { + + ModelQualityBaselineConfig modelQualityBaselineConfig = ModelQualityBaselineConfig.builder() + .baseliningJobName(TEST_PROCESSING_JOB_NAME).build(); + + final DescribeModelQualityJobDefinitionResponse describeModelQualityJobDefinitionResponse = + DescribeModelQualityJobDefinitionResponse.builder() + .creationTime(TEST_TIME) + .jobDefinitionName(TEST_JOB_DEFINITION_NAME) + .jobDefinitionArn(TEST_JOB_DEFINITION_ARN) + .modelQualityBaselineConfig(modelQualityBaselineConfig) + .roleArn(TEST_ARN) + .build(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + DescribeModelQualityJobDefinitionRequest.class); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class))) + .thenReturn(describeModelQualityJobDefinitionResponse); + + software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig resourceBaselineConfig = + software.amazon.sagemaker.modelqualityjobdefinition.ModelQualityBaselineConfig.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) + .modelQualityBaselineConfig(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)).describeModelQualityJobDefinition(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()).describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.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_ModelQualityJobDefinitionDoesNotExist_Fails() { + final AwsServiceException resourceNotFoundException = AwsServiceException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.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().describeModelQualityJobDefinition(any(DescribeModelQualityJobDefinitionRequest.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-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorTest.java b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorTest.java new file mode 100644 index 0000000..8d1041b --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/src/test/java/software/amazon/sagemaker/modelqualityjobdefinition/TranslatorTest.java @@ -0,0 +1,145 @@ +package software.amazon.sagemaker.modelqualityjobdefinition; + +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-modelqualityjobdefinition/template.yml b/aws-sagemaker-modelqualityjobdefinition/template.yml new file mode 100644 index 0000000..2fd9f6a --- /dev/null +++ b/aws-sagemaker-modelqualityjobdefinition/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::ModelQualityJobDefinition 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.modelqualityjobdefinition.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelqualityjobdefinition-1.0.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.modelqualityjobdefinition.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-modelqualityjobdefinition-1.0.jar diff --git a/aws-sagemaker-monitoringschedule/.rpdk-config b/aws-sagemaker-monitoringschedule/.rpdk-config new file mode 100644 index 0000000..6344e8a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::SageMaker::MonitoringSchedule", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.monitoringschedule.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.monitoringschedule.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "sagemaker", + "monitoringschedule" + ], + "protocolVersion": "2.0.0" + } +} diff --git a/aws-sagemaker-monitoringschedule/README.md b/aws-sagemaker-monitoringschedule/README.md new file mode 100644 index 0000000..f6c739a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/README.md @@ -0,0 +1,150 @@ +# AWS::SageMaker::MonitoringSchedule + +Resource Type definition for AWS::SageMaker::MonitoringSchedule + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::MonitoringSchedule",
+    "Properties" : {
+        "MonitoringScheduleName" : String,
+        "MonitoringScheduleConfig" : MonitoringScheduleConfig,
+        "Tags" : [ Tag, ... ],
+        "EndpointName" : String,
+        "FailureReason" : String,
+        "LastMonitoringExecutionSummary" : MonitoringExecutionSummary,
+        "MonitoringScheduleStatus" : String
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::MonitoringSchedule
+Properties:
+    MonitoringScheduleName: String
+    MonitoringScheduleConfig: MonitoringScheduleConfig
+    Tags: 
+      - Tag
+    EndpointName: String
+    FailureReason: String
+    LastMonitoringExecutionSummary: MonitoringExecutionSummary
+    MonitoringScheduleStatus: String
+
+ +## Properties + +#### MonitoringScheduleName + +The name of the monitoring schedule. + +_Required_: Yes + +_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) + +#### MonitoringScheduleConfig + +The configuration object that specifies the monitoring schedule and defines the monitoring job. + +_Required_: Yes + +_Type_: MonitoringScheduleConfig + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tags + +An array of key-value pairs to apply to this resource. + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### EndpointName + +The name of the endpoint used to run the monitoring job. + +_Required_: No + +_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) + +#### FailureReason + +Contains the reason a monitoring job failed, if it failed. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 1024 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### LastMonitoringExecutionSummary + +Summary of information about monitoring job + +_Required_: No + +_Type_: MonitoringExecutionSummary + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringScheduleStatus + +The status of a schedule job. + +_Required_: No + +_Type_: String + +_Allowed Values_: Pending | Failed | Scheduled | Stopped + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the MonitoringScheduleArn. + +### 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). + +#### MonitoringScheduleArn + +The Amazon Resource Name (ARN) of the monitoring schedule. + +#### CreationTime + +The time at which the schedule was created. + +#### LastModifiedTime + +A timestamp that indicates the last time the monitoring job was modified. + diff --git a/aws-sagemaker-monitoringschedule/aws-sagemaker-monitoringschedule.json b/aws-sagemaker-monitoringschedule/aws-sagemaker-monitoringschedule.json new file mode 100644 index 0000000..d396a28 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/aws-sagemaker-monitoringschedule.json @@ -0,0 +1,590 @@ +{ + "typeName" : "AWS::SageMaker::MonitoringSchedule", + "description" : "Resource Type definition for AWS::SageMaker::MonitoringSchedule", + "additionalProperties" : false, + "properties" : { + "MonitoringScheduleArn" : { + "description": "The Amazon Resource Name (ARN) of the monitoring schedule.", + "type" : "string", + "minLength": 1, + "maxLength": 256 + }, + "MonitoringScheduleName" : { + "$ref" : "#/definitions/MonitoringScheduleName" + }, + "MonitoringScheduleConfig": { + "$ref": "#/definitions/MonitoringScheduleConfig" + }, + "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 schedule was created.", + "type": "string" + }, + "EndpointName": { + "$ref" : "#/definitions/EndpointName" + }, + "FailureReason" : { + "type" : "string", + "description" : "Contains the reason a monitoring job failed, if it failed.", + "minLength" : 1, + "maxLength" : 1024 + }, + "LastModifiedTime": { + "description": "A timestamp that indicates the last time the monitoring job was modified.", + "type": "string" + }, + "LastMonitoringExecutionSummary" : { + "description" : "Describes metadata on the last execution to run, if there was one.", + "$ref" : "#/definitions/MonitoringExecutionSummary" + }, + "MonitoringScheduleStatus": { + "description": "The status of a schedule job.", + "type": "string", + "enum": [ + "Pending", + "Failed", + "Scheduled", + "Stopped" + ] + } + }, + "definitions" : { + "MonitoringScheduleConfig" : { + "type" : "object", + "additionalProperties" : false, + "description": "The configuration object that specifies the monitoring schedule and defines the monitoring job.", + "properties" : { + "MonitoringJobDefinition": { + "$ref": "#/definitions/MonitoringJobDefinition" + }, + "MonitoringJobDefinitionName": { + "description": "Name of the job definition", + "type" : "string", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "minLength": 1, + "maxLength": 63 + }, + "MonitoringType": { + "$ref": "#/definitions/MonitoringType" + }, + "ScheduleConfig": { + "$ref": "#/definitions/ScheduleConfig" + } + } + }, + "MonitoringType": { + "description": "The type of monitoring job.", + "type": "string", + "enum": [ + "DataQuality", + "ModelQuality", + "ModelBias", + "ModelExplainability" + ] + }, + "MonitoringJobDefinition" : { + "type" : "object", + "additionalProperties" : false, + "description": "Defines the monitoring job.", + "properties" : { + "BaselineConfig": { + "$ref": "#/definitions/BaselineConfig" + }, + "Environment": { + "$ref": "#/definitions/Environment" + }, + "MonitoringAppSpecification": { + "$ref": "#/definitions/MonitoringAppSpecification" + }, + "MonitoringInputs": { + "$ref": "#/definitions/MonitoringInputs" + }, + "MonitoringOutputConfig": { + "$ref": "#/definitions/MonitoringOutputConfig" + }, + "MonitoringResources": { + "$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" + } + }, + "required" : [ "MonitoringAppSpecification", "MonitoringInputs", "MonitoringOutputConfig", "MonitoringResources", "RoleArn" ] + }, + "BaselineConfig" : { + "type" : "object", + "additionalProperties" : false, + "description": "Baseline configuration used to validate that the data conforms to the specified constraints and statistics.", + "properties" : { + "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", + "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 + } + } + }, + "MonitoringAppSpecification" : { + "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" : { + "$ref" : "#/definitions/ContainerArgument" + } + }, + "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" + } + }, + "required" : [ "ImageUri" ] + }, + "ContainerArgument" : { + "type" : "string", + "additionalProperties" : false, + "description": "Arguments for the container used to run the monitoring job.", + "minLength" : 1, + "maxLength": 256 + }, + "MonitoringInputs" : { + "type" : "array", + "additionalProperties" : false, + "description" : "The array of inputs for the monitoring job.", + "minItems" : 1, + "maxItems" : 1, + "items" : { + "$ref" : "#/definitions/MonitoringInput" + } + }, + "MonitoringInput" : { + "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" ] + }, + "ScheduleConfig" : { + "type" : "object", + "additionalProperties" : false, + "description": "Configuration details about the monitoring schedule.", + "properties" : { + "ScheduleExpression": { + "description": "A cron expression that describes details about the monitoring schedule.", + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required" : [ "ScheduleExpression" ] + }, + "Tag" : { + "description" : "A key-value pair to associate with a resource.", + "type" : "object", + "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" ] + }, + "MonitoringExecutionSummary" : { + "description" : "Summary of information about monitoring job", + "type" : "object", + "properties" : { + "CreationTime": { + "description": "The time at which the monitoring job was created.", + "type": "string" + }, + "EndpointName": { + "$ref" : "#/definitions/EndpointName" + }, + "FailureReason" : { + "type" : "string", + "description" : "Contains the reason a monitoring job failed, if it failed.", + "maxLength" : 1024 + }, + "LastModifiedTime": { + "description": "A timestamp that indicates the last time the monitoring job was modified.", + "type": "string" + }, + "MonitoringExecutionStatus": { + "description": "The status of the monitoring job.", + "type": "string", + "enum": [ + "Pending", + "Completed", + "CompletedWithViolations", + "InProgress", + "Failed", + "Stopping", + "Stopped" + ] + }, + "MonitoringScheduleName": { + "$ref" : "#/definitions/MonitoringScheduleName" + }, + "ProcessingJobArn" : { + "description": "The Amazon Resource Name (ARN) of the monitoring job.", + "type" : "string", + "pattern": "aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:processing-job/.*", + "maxLength": 256 + }, + "ScheduledTime": { + "description": "The time the monitoring job was scheduled.", + "type": "string" + } + }, + "required" : [ "CreationTime", "LastModifiedTime", "MonitoringExecutionStatus", "MonitoringScheduleName", "ScheduledTime" ] + }, + "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 + }, + "MonitoringScheduleName": { + "type" : "string", + "description" : "The name of the monitoring schedule.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "maxLength" : 63 + } + }, + "required" : [ "MonitoringScheduleConfig", "MonitoringScheduleName" ], + "primaryIdentifier" : [ "/properties/MonitoringScheduleArn" ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateMonitoringSchedule", + "sagemaker:DescribeMonitoringSchedule", + "iam:PassRole" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteMonitoringSchedule", + "sagemaker:DescribeMonitoringSchedule" + ] + }, + "list": { + "permissions": [ + "sagemaker:ListMonitoringSchedule" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeMonitoringSchedule" + ] + }, + "update": { + "permissions": [ + "sagemaker:UpdateMonitoringSchedule", + "sagemaker:DescribeMonitoringSchedule" + ] + } + }, + "readOnlyProperties": [ + "/properties/MonitoringScheduleArn", + "/properties/CreationTime", + "/properties/LastModifiedTime" + ], + "createOnlyProperties": [ + "/properties/MonitoringScheduleName" + ] +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/docs/README.md b/aws-sagemaker-monitoringschedule/docs/README.md new file mode 100644 index 0000000..f6c739a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/README.md @@ -0,0 +1,150 @@ +# AWS::SageMaker::MonitoringSchedule + +Resource Type definition for AWS::SageMaker::MonitoringSchedule + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::MonitoringSchedule",
+    "Properties" : {
+        "MonitoringScheduleName" : String,
+        "MonitoringScheduleConfig" : MonitoringScheduleConfig,
+        "Tags" : [ Tag, ... ],
+        "EndpointName" : String,
+        "FailureReason" : String,
+        "LastMonitoringExecutionSummary" : MonitoringExecutionSummary,
+        "MonitoringScheduleStatus" : String
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::MonitoringSchedule
+Properties:
+    MonitoringScheduleName: String
+    MonitoringScheduleConfig: MonitoringScheduleConfig
+    Tags: 
+      - Tag
+    EndpointName: String
+    FailureReason: String
+    LastMonitoringExecutionSummary: MonitoringExecutionSummary
+    MonitoringScheduleStatus: String
+
+ +## Properties + +#### MonitoringScheduleName + +The name of the monitoring schedule. + +_Required_: Yes + +_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) + +#### MonitoringScheduleConfig + +The configuration object that specifies the monitoring schedule and defines the monitoring job. + +_Required_: Yes + +_Type_: MonitoringScheduleConfig + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tags + +An array of key-value pairs to apply to this resource. + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### EndpointName + +The name of the endpoint used to run the monitoring job. + +_Required_: No + +_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) + +#### FailureReason + +Contains the reason a monitoring job failed, if it failed. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 1024 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### LastMonitoringExecutionSummary + +Summary of information about monitoring job + +_Required_: No + +_Type_: MonitoringExecutionSummary + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringScheduleStatus + +The status of a schedule job. + +_Required_: No + +_Type_: String + +_Allowed Values_: Pending | Failed | Scheduled | Stopped + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the MonitoringScheduleArn. + +### 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). + +#### MonitoringScheduleArn + +The Amazon Resource Name (ARN) of the monitoring schedule. + +#### CreationTime + +The time at which the schedule was created. + +#### LastModifiedTime + +A timestamp that indicates the last time the monitoring job was modified. + diff --git a/aws-sagemaker-monitoringschedule/docs/baselineconfig.md b/aws-sagemaker-monitoringschedule/docs/baselineconfig.md new file mode 100644 index 0000000..e5e11f2 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/baselineconfig.md @@ -0,0 +1,46 @@ +# AWS::SageMaker::MonitoringSchedule BaselineConfig + +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 + +
+{
+    "ConstraintsResource" : ConstraintsResource,
+    "StatisticsResource" : StatisticsResource
+}
+
+ +### YAML + +
+ConstraintsResource: ConstraintsResource
+StatisticsResource: StatisticsResource
+
+ +## Properties + +#### 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-monitoringschedule/docs/clusterconfig.md b/aws-sagemaker-monitoringschedule/docs/clusterconfig.md new file mode 100644 index 0000000..d566c08 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/clusterconfig.md @@ -0,0 +1,70 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/constraintsresource.md b/aws-sagemaker-monitoringschedule/docs/constraintsresource.md new file mode 100644 index 0000000..314ee10 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/constraintsresource.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/endpointinput.md b/aws-sagemaker-monitoringschedule/docs/endpointinput.md new file mode 100644 index 0000000..d56cde1 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/endpointinput.md @@ -0,0 +1,82 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/monitoringappspecification.md b/aws-sagemaker-monitoringschedule/docs/monitoringappspecification.md new file mode 100644 index 0000000..6a31aa2 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringappspecification.md @@ -0,0 +1,96 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringAppSpecification + +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
+}
+
+ +### YAML + +
+ContainerArguments: 
+      - String
+ContainerEntrypoint: 
+      - String
+ImageUri: String
+PostAnalyticsProcessorSourceUri: String
+RecordPreprocessorSourceUri: String
+
+ +## 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) + diff --git a/aws-sagemaker-monitoringschedule/docs/monitoringexecutionsummary.md b/aws-sagemaker-monitoringschedule/docs/monitoringexecutionsummary.md new file mode 100644 index 0000000..a6f4890 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringexecutionsummary.md @@ -0,0 +1,134 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringExecutionSummary + +Summary of information about monitoring job + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "CreationTime" : String,
+    "EndpointName" : String,
+    "FailureReason" : String,
+    "LastModifiedTime" : String,
+    "MonitoringExecutionStatus" : String,
+    "MonitoringScheduleName" : String,
+    "ProcessingJobArn" : String,
+    "ScheduledTime" : String
+}
+
+ +### YAML + +
+CreationTime: String
+EndpointName: String
+FailureReason: String
+LastModifiedTime: String
+MonitoringExecutionStatus: String
+MonitoringScheduleName: String
+ProcessingJobArn: String
+ScheduledTime: String
+
+ +## Properties + +#### CreationTime + +The time at which the monitoring job was created. + +_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) + +#### EndpointName + +The name of the endpoint used to run the monitoring job. + +_Required_: No + +_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) + +#### FailureReason + +Contains the reason a monitoring job failed, if it failed. + +_Required_: No + +_Type_: String + +_Maximum_: 1024 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### LastModifiedTime + +A timestamp that indicates the last time the monitoring job was modified. + +_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) + +#### MonitoringExecutionStatus + +The status of the monitoring job. + +_Required_: Yes + +_Type_: String + +_Allowed Values_: Pending | Completed | CompletedWithViolations | InProgress | Failed | Stopping | Stopped + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringScheduleName + +The name of the monitoring schedule. + +_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) + +#### ProcessingJobArn + +The Amazon Resource Name (ARN) of the monitoring job. + +_Required_: No + +_Type_: String + +_Maximum_: 256 + +_Pattern_: aws[a-z\-]*:sagemaker:[a-z0-9\-]*:[0-9]{12}:processing-job/.* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ScheduledTime + +The time the monitoring job was scheduled. + +_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-monitoringschedule/docs/monitoringinput.md b/aws-sagemaker-monitoringschedule/docs/monitoringinput.md new file mode 100644 index 0000000..63124eb --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringinput.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringInput + +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-monitoringschedule/docs/monitoringjobdefinition-environment.md b/aws-sagemaker-monitoringschedule/docs/monitoringjobdefinition-environment.md new file mode 100644 index 0000000..8941b0a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringjobdefinition-environment.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringJobDefinition 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-monitoringschedule/docs/monitoringjobdefinition.md b/aws-sagemaker-monitoringschedule/docs/monitoringjobdefinition.md new file mode 100644 index 0000000..14e17c8 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringjobdefinition.md @@ -0,0 +1,137 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringJobDefinition + +Defines the monitoring job. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "BaselineConfig" : BaselineConfig,
+    "Environment" : Environment,
+    "MonitoringAppSpecification" : MonitoringAppSpecification,
+    "MonitoringInputs" : [ MonitoringInput, ... ],
+    "MonitoringOutputConfig" : MonitoringOutputConfig,
+    "MonitoringResources" : MonitoringResources,
+    "NetworkConfig" : NetworkConfig,
+    "RoleArn" : String,
+    "StoppingCondition" : StoppingCondition
+}
+
+ +### YAML + +
+BaselineConfig: BaselineConfig
+Environment: Environment
+MonitoringAppSpecification: MonitoringAppSpecification
+MonitoringInputs: 
+      - MonitoringInput
+MonitoringOutputConfig: MonitoringOutputConfig
+MonitoringResources: MonitoringResources
+NetworkConfig: NetworkConfig
+RoleArn: String
+StoppingCondition: StoppingCondition
+
+ +## Properties + +#### BaselineConfig + +Baseline configuration used to validate that the data conforms to the specified constraints and statistics. + +_Required_: No + +_Type_: BaselineConfig + +_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) + +#### MonitoringAppSpecification + +Container image configuration object for the monitoring job. + +_Required_: Yes + +_Type_: MonitoringAppSpecification + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringInputs + +The array of inputs for the monitoring job. + +_Required_: Yes + +_Type_: List of MonitoringInput + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringOutputConfig + +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) + +#### MonitoringResources + +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_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### 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_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### StoppingCondition + +Specifies a time limit for how long the monitoring job is allowed to run. + +_Required_: No + +_Type_: StoppingCondition + +_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-monitoringschedule/docs/monitoringoutput.md b/aws-sagemaker-monitoringschedule/docs/monitoringoutput.md new file mode 100644 index 0000000..baa6bf7 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringoutput.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/monitoringoutputconfig.md b/aws-sagemaker-monitoringschedule/docs/monitoringoutputconfig.md new file mode 100644 index 0000000..f19ea5c --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringoutputconfig.md @@ -0,0 +1,55 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/monitoringresources.md b/aws-sagemaker-monitoringschedule/docs/monitoringresources.md new file mode 100644 index 0000000..a6634ec --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringresources.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/monitoringscheduleconfig.md b/aws-sagemaker-monitoringschedule/docs/monitoringscheduleconfig.md new file mode 100644 index 0000000..a8bda21 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/monitoringscheduleconfig.md @@ -0,0 +1,78 @@ +# AWS::SageMaker::MonitoringSchedule MonitoringScheduleConfig + +The configuration object that specifies the monitoring schedule and defines the monitoring job. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "MonitoringJobDefinition" : MonitoringJobDefinition,
+    "MonitoringJobDefinitionName" : String,
+    "MonitoringType" : String,
+    "ScheduleConfig" : ScheduleConfig
+}
+
+ +### YAML + +
+MonitoringJobDefinition: MonitoringJobDefinition
+MonitoringJobDefinitionName: String
+MonitoringType: String
+ScheduleConfig: ScheduleConfig
+
+ +## Properties + +#### MonitoringJobDefinition + +Defines the monitoring job. + +_Required_: No + +_Type_: MonitoringJobDefinition + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MonitoringJobDefinitionName + +Name of the job definition + +_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) + +#### MonitoringType + +The type of monitoring job. + +_Required_: No + +_Type_: String + +_Allowed Values_: DataQuality | ModelQuality | ModelBias | ModelExplainability + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ScheduleConfig + +Configuration details about the monitoring schedule. + +_Required_: No + +_Type_: ScheduleConfig + +_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-monitoringschedule/docs/networkconfig.md b/aws-sagemaker-monitoringschedule/docs/networkconfig.md new file mode 100644 index 0000000..a117c9c --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/networkconfig.md @@ -0,0 +1,58 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/s3output.md b/aws-sagemaker-monitoringschedule/docs/s3output.md new file mode 100644 index 0000000..3cda351 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/s3output.md @@ -0,0 +1,68 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/scheduleconfig.md b/aws-sagemaker-monitoringschedule/docs/scheduleconfig.md new file mode 100644 index 0000000..a944180 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/scheduleconfig.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::MonitoringSchedule ScheduleConfig + +Configuration details about the monitoring schedule. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "ScheduleExpression" : String
+}
+
+ +### YAML + +
+ScheduleExpression: String
+
+ +## Properties + +#### ScheduleExpression + +A cron expression that describes details about the monitoring schedule. + +_Required_: Yes + +_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) + diff --git a/aws-sagemaker-monitoringschedule/docs/statisticsresource.md b/aws-sagemaker-monitoringschedule/docs/statisticsresource.md new file mode 100644 index 0000000..01cf089 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/statisticsresource.md @@ -0,0 +1,38 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/stoppingcondition.md b/aws-sagemaker-monitoringschedule/docs/stoppingcondition.md new file mode 100644 index 0000000..ecbc995 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/stoppingcondition.md @@ -0,0 +1,34 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/tag.md b/aws-sagemaker-monitoringschedule/docs/tag.md new file mode 100644 index 0000000..2fa1fa8 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/tag.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/docs/vpcconfig.md b/aws-sagemaker-monitoringschedule/docs/vpcconfig.md new file mode 100644 index 0000000..f89fc6d --- /dev/null +++ b/aws-sagemaker-monitoringschedule/docs/vpcconfig.md @@ -0,0 +1,48 @@ +# AWS::SageMaker::MonitoringSchedule 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-monitoringschedule/lombok.config b/aws-sagemaker-monitoringschedule/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-monitoringschedule/pom.xml b/aws-sagemaker-monitoringschedule/pom.xml new file mode 100644 index 0000000..a8682b6 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + software.amazon.sagemaker.monitoringschedule + aws-sagemaker-monitoringschedule-handler + aws-sagemaker-monitoringschedule-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.3 + + + INSTRUCTION + COVEREDRATIO + 0.5 + + + + + + + + + + + + ${project.basedir} + + aws-sagemaker-monitoringschedule.json + + + + + \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/resource-role.yaml b/aws-sagemaker-monitoringschedule/resource-role.yaml new file mode 100644 index 0000000..54b1ee7 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/resource-role.yaml @@ -0,0 +1,36 @@ +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:CreateMonitoringSchedule" + - "sagemaker:DeleteMonitoringSchedule" + - "sagemaker:DescribeMonitoringSchedule" + - "sagemaker:ListMonitoringSchedule" + - "sagemaker:UpdateMonitoringSchedule" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/BaseHandlerStd.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/BaseHandlerStd.java new file mode 100644 index 0000000..33b1a4c --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/BaseHandlerStd.java @@ -0,0 +1,36 @@ +package software.amazon.sagemaker.monitoringschedule; + +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-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CallbackContext.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CallbackContext.java new file mode 100644 index 0000000..6fa6bb9 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CallbackContext.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.monitoringschedule; + +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-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ClientBuilder.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ClientBuilder.java new file mode 100644 index 0000000..f6ca328 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ClientBuilder.java @@ -0,0 +1,12 @@ +package software.amazon.sagemaker.monitoringschedule; + +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-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Configuration.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Configuration.java new file mode 100644 index 0000000..865ffbb --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Configuration.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.monitoringschedule; + +class Configuration extends BaseConfiguration { + public Configuration() { + super("aws-sagemaker-monitoringschedule.json"); + } +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CreateHandler.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CreateHandler.java new file mode 100644 index 0000000..29fafef --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/CreateHandler.java @@ -0,0 +1,108 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.CreateMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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.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 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("AWS-SageMaker-MonitoringSchedule::Create", 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 CreateMonitoringScheduleResponse createResource( + final CreateMonitoringScheduleRequest awsRequest, + final ProxyClient proxyClient) { + + CreateMonitoringScheduleResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createMonitoringSchedule); + } catch (final ResourceInUseException e) { + throw new ResourceAlreadyExistsException(ResourceModel.TYPE_NAME, awsRequest.monitoringScheduleName()); + } 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; + } + + /** + * This is used to ensure MonitoringSchedule resource has moved from Pending to Scheduled/Failed state. + * @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 boolean stabilizedOnCreate( + final CreateMonitoringScheduleRequest createMonitoringScheduleRequest, + final CreateMonitoringScheduleResponse createMonitoringScheduleResponse, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + + if(model.getMonitoringScheduleArn() == null){ + model.setMonitoringScheduleArn(createMonitoringScheduleResponse.monitoringScheduleArn()); + } + + final ScheduleStatus monitoringScheduleState = proxyClient.injectCredentialsAndInvokeV2(TranslatorForRequest.translateToReadRequest(model), + proxyClient.client()::describeMonitoringSchedule).monitoringScheduleStatus(); + + switch (monitoringScheduleState) { + case SCHEDULED: + case STOPPED: + logger.log(String.format("%s [%s] has been stabilized with status %s.", ResourceModel.TYPE_NAME, + model.getPrimaryIdentifier(), monitoringScheduleState)); + return true; + case PENDING: + logger.log(String.format("%s [%s] is stabilizing.", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier())); + return false; + default: + throw new CfnGeneralServiceException("Stabilizing of " + model.getPrimaryIdentifier()); + + } + } + +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/DeleteHandler.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/DeleteHandler.java new file mode 100644 index 0000000..a20a9a6 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/DeleteHandler.java @@ -0,0 +1,100 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.CreateMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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.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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-MonitoringSchedule::Delete", 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 deleteMonitoringScheduleRequest the aws service request to delete a resource + * @param proxyClient the aws service client to make the call + * @return delete resource response + */ + private DeleteMonitoringScheduleResponse deleteResource( + final DeleteMonitoringScheduleRequest deleteMonitoringScheduleRequest, + final ProxyClient proxyClient) { + + DeleteMonitoringScheduleResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(deleteMonitoringScheduleRequest, proxyClient.client()::deleteMonitoringSchedule); + } 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, deleteMonitoringScheduleRequest.monitoringScheduleName()); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.DELETE.toString(), e); + } + + return response; + } + + /** + * Sync delete API moves resource in PENDING state and actual deletion happens asynchronously. + * Stabilization is required to ensure MonitoringSchedule resource deletion has been completed. + * @param deleteMonitoringScheduleRequest the aws service request to delete a resource + * @param deleteMonitoringScheduleResponse 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 DeleteMonitoringScheduleRequest deleteMonitoringScheduleRequest, + final DeleteMonitoringScheduleResponse deleteMonitoringScheduleResponse, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + final ScheduleStatus monitoringScheduleState = proxyClient.injectCredentialsAndInvokeV2(TranslatorForRequest.translateToReadRequest(model), + proxyClient.client()::describeMonitoringSchedule).monitoringScheduleStatus(); + + switch (monitoringScheduleState) { + case PENDING: + logger.log(String.format("%s with name [%s] is stabilizing while delete.", ResourceModel.TYPE_NAME, model.getMonitoringScheduleName())); + return false; + default: + throw new CfnGeneralServiceException("Delete stabilizing of monitoring schedule: " + model.getMonitoringScheduleName()); + } + } catch (ResourceNotFoundException e) { + return true; + } + } +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ListHandler.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ListHandler.java new file mode 100644 index 0000000..fc9911a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ListHandler.java @@ -0,0 +1,70 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ListMonitoringSchedulesRequest; +import software.amazon.awssdk.services.sagemaker.model.ListMonitoringSchedulesResponse; +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 = "SageMaker::ListMonitoringSchedule"; + 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("AWS-SageMaker-MonitoringSchedule::List", 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 listMonitoringScheduleRequest the aws service request to describe a resource + * @param proxyClient the aws service client to make the call + * @return listMonitoringScheduleResponse + */ + private ListMonitoringSchedulesResponse listResources( + final ListMonitoringSchedulesRequest listMonitoringScheduleRequest, + final ProxyClient proxyClient) { + + ListMonitoringSchedulesResponse listMonitoringSchedulesResponse = null; + try { + listMonitoringSchedulesResponse = proxyClient.injectCredentialsAndInvokeV2(listMonitoringScheduleRequest, + proxyClient.client()::listMonitoringSchedules); + } catch (final AwsServiceException e) { + Translator.throwCfnException(OPERATION, e); + } + + return listMonitoringSchedulesResponse; + } + + /** + * 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 ListMonitoringSchedulesResponse 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-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ReadHandler.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ReadHandler.java new file mode 100644 index 0000000..2833d61 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/ReadHandler.java @@ -0,0 +1,73 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +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.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(); + + return proxy.initiate("AWS-SageMaker-MonitoringSchedule::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 DescribeMonitoringScheduleResponse readResource( + final DescribeMonitoringScheduleRequest awsRequest, + final ProxyClient proxyClient, + final ResourceModel model) { + + DescribeMonitoringScheduleResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeMonitoringSchedule); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.monitoringScheduleName(), 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 DescribeMonitoringScheduleResponse awsResponse) { + return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse)); + } +} diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Translator.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Translator.java new file mode 100644 index 0000000..b8c432a --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/Translator.java @@ -0,0 +1,61 @@ +package software.amazon.sagemaker.monitoringschedule; + +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 + */ + 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); + } + + static Stream streamOfOrEmpty(final Collection collection) { + return Optional.ofNullable(collection) + .map(Collection::stream) + .orElseGet(Stream::empty); + } + +} diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForRequest.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForRequest.java new file mode 100644 index 0000000..a1f09f4 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForRequest.java @@ -0,0 +1,252 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.services.sagemaker.model.CreateMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.EndpointInput; +import software.amazon.awssdk.services.sagemaker.model.ListMonitoringSchedulesRequest; +import software.amazon.awssdk.services.sagemaker.model.MonitoringAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.MonitoringBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource; +import software.amazon.awssdk.services.sagemaker.model.MonitoringInput; +import software.amazon.awssdk.services.sagemaker.model.MonitoringJobDefinition; +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.MonitoringScheduleConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringStatisticsResource; +import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition; +import software.amazon.awssdk.services.sagemaker.model.NetworkConfig; +import software.amazon.awssdk.services.sagemaker.model.ScheduleConfig; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.awssdk.services.sagemaker.model.UpdateMonitoringScheduleRequest; +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 createMonitoringScheduleRequest - service request to create a resource + */ + static CreateMonitoringScheduleRequest translateToCreateRequest(final ResourceModel model) { + return CreateMonitoringScheduleRequest.builder() + .monitoringScheduleName(model.getMonitoringScheduleName()) + .monitoringScheduleConfig(translate(model.getMonitoringScheduleConfig())) + .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 describeMonitoringScheduleRequest - the aws service request to describe a resource + */ + static DescribeMonitoringScheduleRequest translateToReadRequest(final ResourceModel model) { + return DescribeMonitoringScheduleRequest.builder() + .monitoringScheduleName(model.getMonitoringScheduleName()) + .build(); + } + + /** + * Request to delete a resource + * @param model resource model + * @return deleteMonitoringScheduleRequest the aws service request to delete a resource + */ + static DeleteMonitoringScheduleRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteMonitoringScheduleRequest.builder() + .monitoringScheduleName(model.getMonitoringScheduleName()) + .build(); + } + + /** + * Request to update properties of a previously created resource + * @param model resource model + * @return updateMonitoringScheduleRequest the aws service request to modify a resource + */ + static UpdateMonitoringScheduleRequest translateToUpdateRequest(final ResourceModel model) { + return UpdateMonitoringScheduleRequest.builder() + .monitoringScheduleName(model.getMonitoringScheduleName()) + .monitoringScheduleConfig(translate(model.getMonitoringScheduleConfig())) + .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 ListMonitoringSchedulesRequest translateToListRequest(final String nextToken) { + return ListMonitoringSchedulesRequest.builder() + .nextToken(nextToken) + .build(); + } + + /** + * Converts MonitoringScheduleConfig from a CFN resource model to a Sagemaker SDK object. + * @param config config from CFN resource provider. + * @return Sagemaker MonitoringScheduleConfig object. + */ + static MonitoringScheduleConfig translate(final software.amazon.sagemaker.monitoringschedule.MonitoringScheduleConfig config) { + return config == null ? null : MonitoringScheduleConfig.builder() + .monitoringJobDefinition(translate(config.getMonitoringJobDefinition())) + .monitoringJobDefinitionName(config.getMonitoringJobDefinitionName()) + .monitoringType(config.getMonitoringType()) + .scheduleConfig(translate(config.getScheduleConfig())) + .build(); + } + + static ScheduleConfig translate(final software.amazon.sagemaker.monitoringschedule.ScheduleConfig config) { + return config == null ? null : ScheduleConfig.builder().scheduleExpression(config.getScheduleExpression()).build(); + } + + static MonitoringJobDefinition translate(final software.amazon.sagemaker.monitoringschedule.MonitoringJobDefinition jobDefinition) { + return jobDefinition == null ? null : MonitoringJobDefinition.builder() + .baselineConfig(translate(jobDefinition.getBaselineConfig())) + .environment(translateMapOfObjectsToMapOfStrings(jobDefinition.getEnvironment())) + .monitoringAppSpecification(translate(jobDefinition.getMonitoringAppSpecification())) + .monitoringInputs(translate(jobDefinition.getMonitoringInputs())) + .monitoringOutputConfig(translate(jobDefinition.getMonitoringOutputConfig())) + .monitoringResources(translate(jobDefinition.getMonitoringResources())) + .networkConfig(translate(jobDefinition.getNetworkConfig())) + .roleArn(jobDefinition.getRoleArn()) + .stoppingCondition(translate(jobDefinition.getStoppingCondition())) + .build(); + } + + static MonitoringBaselineConfig translate(final software.amazon.sagemaker.monitoringschedule.BaselineConfig baselineConfig) { + return baselineConfig == null ? null : MonitoringBaselineConfig.builder() + .constraintsResource(translate(baselineConfig.getConstraintsResource())) + .statisticsResource(translate(baselineConfig.getStatisticsResource())) + .build(); + } + + static MonitoringConstraintsResource translate(final software.amazon.sagemaker.monitoringschedule.ConstraintsResource constraintsResource) { + return constraintsResource == null ? null : MonitoringConstraintsResource.builder().s3Uri(constraintsResource.getS3Uri()).build(); + } + + static MonitoringStatisticsResource translate(final software.amazon.sagemaker.monitoringschedule.StatisticsResource statisticsResource) { + return statisticsResource == null ? null : MonitoringStatisticsResource.builder().s3Uri(statisticsResource.getS3Uri()).build(); + } + + static MonitoringAppSpecification translate(final software.amazon.sagemaker.monitoringschedule.MonitoringAppSpecification monitoringAppSpec) { + return monitoringAppSpec == null ? null : MonitoringAppSpecification.builder() + .containerArguments(monitoringAppSpec.getContainerArguments()) + .containerEntrypoint(monitoringAppSpec.getContainerEntrypoint()) + .imageUri(monitoringAppSpec.getImageUri()) + .postAnalyticsProcessorSourceUri(monitoringAppSpec.getPostAnalyticsProcessorSourceUri()) + .recordPreprocessorSourceUri(monitoringAppSpec.getRecordPreprocessorSourceUri()) + .build(); + } + + static List translate(final List monitoringInputs) { + return monitoringInputs == null ? null : monitoringInputs.stream() + .map(monitoringInput -> translate(monitoringInput)) + .collect(Collectors.toList()); + } + + static MonitoringInput translate(final software.amazon.sagemaker.monitoringschedule.MonitoringInput monitoringInput) { + return monitoringInput == null ? null : MonitoringInput.builder() + .endpointInput(translate(monitoringInput.getEndpointInput())) + .build(); + } + + static EndpointInput translate(final software.amazon.sagemaker.monitoringschedule.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.monitoringschedule.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.monitoringschedule.MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.getS3Output())) + .build(); + } + + static MonitoringS3Output translate(final software.amazon.sagemaker.monitoringschedule.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.monitoringschedule.MonitoringResources monitoringResources) { + return monitoringResources == null? null : MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.getClusterConfig())) + .build(); + } + + static MonitoringClusterConfig translate(final software.amazon.sagemaker.monitoringschedule.ClusterConfig clusterConfig) { + return clusterConfig == null? null : MonitoringClusterConfig.builder() + .instanceCount(clusterConfig.getInstanceCount()) + .instanceType(clusterConfig.getInstanceType()) + .volumeKmsKeyId(clusterConfig.getVolumeKmsKeyId()) + .volumeSizeInGB(clusterConfig.getVolumeSizeInGB()) + .build(); + } + + static NetworkConfig translate(final software.amazon.sagemaker.monitoringschedule.NetworkConfig networkConfig) { + return networkConfig == null? null : NetworkConfig.builder() + .enableInterContainerTrafficEncryption(networkConfig.getEnableInterContainerTrafficEncryption()) + .enableNetworkIsolation(networkConfig.getEnableNetworkIsolation()) + .vpcConfig(translate(networkConfig.getVpcConfig())) + .build(); + } + + static VpcConfig translate(final software.amazon.sagemaker.monitoringschedule.VpcConfig vpcConfig) { + return vpcConfig == null? null : VpcConfig.builder() + .securityGroupIds(vpcConfig.getSecurityGroupIds()) + .subnets(vpcConfig.getSubnets()) + .build(); + } + + static MonitoringStoppingCondition translate(final software.amazon.sagemaker.monitoringschedule.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()) + ); + } + +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForResponse.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForResponse.java new file mode 100644 index 0000000..5314c8d --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/TranslatorForResponse.java @@ -0,0 +1,236 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.EndpointInput; +import software.amazon.awssdk.services.sagemaker.model.ListMonitoringSchedulesResponse; +import software.amazon.awssdk.services.sagemaker.model.MonitoringAppSpecification; +import software.amazon.awssdk.services.sagemaker.model.MonitoringBaselineConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringClusterConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringConstraintsResource; +import software.amazon.awssdk.services.sagemaker.model.MonitoringExecutionSummary; +import software.amazon.awssdk.services.sagemaker.model.MonitoringInput; +import software.amazon.awssdk.services.sagemaker.model.MonitoringJobDefinition; +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.MonitoringScheduleConfig; +import software.amazon.awssdk.services.sagemaker.model.MonitoringStatisticsResource; +import software.amazon.awssdk.services.sagemaker.model.MonitoringStoppingCondition; +import software.amazon.awssdk.services.sagemaker.model.NetworkConfig; +import software.amazon.awssdk.services.sagemaker.model.ScheduleConfig; +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 DescribeMonitoringScheduleResponse awsResponse) { + return ResourceModel.builder() + .creationTime(awsResponse.creationTime().toString()) + .endpointName(awsResponse.endpointName()) + .failureReason(awsResponse.failureReason()) + .lastModifiedTime(awsResponse.lastModifiedTime().toString()) + .lastMonitoringExecutionSummary(translate(awsResponse.lastMonitoringExecutionSummary())) + .monitoringScheduleArn(awsResponse.monitoringScheduleArn()) + .monitoringScheduleConfig(translate(awsResponse.monitoringScheduleConfig())) + .monitoringScheduleName(awsResponse.monitoringScheduleName()) + .monitoringScheduleStatus(awsResponse.monitoringScheduleStatus().toString()) + .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 ListMonitoringSchedulesResponse awsResponse) { + return Translator.streamOfOrEmpty(awsResponse.monitoringScheduleSummaries()) + .map(summary -> ResourceModel.builder() + .creationTime(summary.creationTime().toString()) + .endpointName(summary.endpointName()) + .monitoringScheduleArn(summary.monitoringScheduleArn()) + .monitoringScheduleName(summary.monitoringScheduleName()) + .monitoringScheduleStatus(summary.monitoringScheduleStatus().toString()) + .lastModifiedTime(summary.lastModifiedTime().toString()) + .build()) + .collect(Collectors.toList()); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringExecutionSummary translate( + final MonitoringExecutionSummary monitoringExecutionSummary) { + return monitoringExecutionSummary == null? null : software.amazon.sagemaker.monitoringschedule.MonitoringExecutionSummary.builder() + .creationTime(monitoringExecutionSummary.creationTime().toString()) + .endpointName(monitoringExecutionSummary.endpointName()) + .failureReason(monitoringExecutionSummary.failureReason()) + .lastModifiedTime(monitoringExecutionSummary.lastModifiedTime().toString()) + .monitoringExecutionStatus(monitoringExecutionSummary.monitoringExecutionStatus().toString()) + .monitoringScheduleName(monitoringExecutionSummary.monitoringScheduleName()) + .processingJobArn(monitoringExecutionSummary.processingJobArn()) + .scheduledTime(monitoringExecutionSummary.scheduledTime().toString()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringScheduleConfig translate( + final MonitoringScheduleConfig monitoringScheduleConfig) { + return monitoringScheduleConfig == null? null : software.amazon.sagemaker.monitoringschedule.MonitoringScheduleConfig.builder() + .monitoringJobDefinition(translate(monitoringScheduleConfig.monitoringJobDefinition())) + .monitoringJobDefinitionName(monitoringScheduleConfig.monitoringJobDefinitionName()) + .monitoringType(monitoringScheduleConfig.monitoringType().toString()) + .scheduleConfig(translate(monitoringScheduleConfig.scheduleConfig())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.ScheduleConfig translate(final ScheduleConfig config) { + return config == null ? null : software.amazon.sagemaker.monitoringschedule.ScheduleConfig.builder() + .scheduleExpression(config.scheduleExpression()).build(); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringJobDefinition translate( + final MonitoringJobDefinition monitoringJobDefinition) { + return monitoringJobDefinition == null? null : software.amazon.sagemaker.monitoringschedule.MonitoringJobDefinition.builder() + .baselineConfig(translate(monitoringJobDefinition.baselineConfig())) + .environment(translateMapOfStringsMapOfObjects(monitoringJobDefinition.environment())) + .monitoringAppSpecification(translate(monitoringJobDefinition.monitoringAppSpecification())) + .monitoringInputs(translateInput(monitoringJobDefinition.monitoringInputs())) + .monitoringOutputConfig(translate(monitoringJobDefinition.monitoringOutputConfig())) + .monitoringResources(translate(monitoringJobDefinition.monitoringResources())) + .networkConfig(translate(monitoringJobDefinition.networkConfig())) + .roleArn(monitoringJobDefinition.roleArn()) + .stoppingCondition(translate(monitoringJobDefinition.stoppingCondition())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.BaselineConfig translate( + final MonitoringBaselineConfig monitoringBaselineConfig) { + return monitoringBaselineConfig == null? null : software.amazon.sagemaker.monitoringschedule.BaselineConfig.builder() + .constraintsResource(translate(monitoringBaselineConfig.constraintsResource())) + .statisticsResource(translate(monitoringBaselineConfig.statisticsResource())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.ConstraintsResource translate( + final MonitoringConstraintsResource constraintsResource) { + return constraintsResource == null? null : software.amazon.sagemaker.monitoringschedule.ConstraintsResource.builder() + .s3Uri(constraintsResource.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.StatisticsResource translate( + final MonitoringStatisticsResource statisticsResource) { + return statisticsResource == null? null : software.amazon.sagemaker.monitoringschedule.StatisticsResource.builder() + .s3Uri(statisticsResource.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringAppSpecification translate( + final MonitoringAppSpecification monitoringAppSpec) { + return monitoringAppSpec == null ? null : software.amazon.sagemaker.monitoringschedule.MonitoringAppSpecification.builder() + .containerArguments(monitoringAppSpec.containerArguments()) + .containerEntrypoint(monitoringAppSpec.containerEntrypoint()) + .imageUri(monitoringAppSpec.imageUri()) + .postAnalyticsProcessorSourceUri(monitoringAppSpec.postAnalyticsProcessorSourceUri()) + .recordPreprocessorSourceUri(monitoringAppSpec.recordPreprocessorSourceUri()) + .build(); + } + + static List translateInput(final List monitoringInputs) { + return monitoringInputs == null ? null : monitoringInputs.stream() + .map(monitoringInput -> translate(monitoringInput)) + .collect(Collectors.toList()); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringInput translate(final MonitoringInput monitoringInput) { + return monitoringInput == null ? null : software.amazon.sagemaker.monitoringschedule.MonitoringInput.builder() + .endpointInput(translate(monitoringInput.endpointInput())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.EndpointInput translate(final EndpointInput endpointInput) { + return endpointInput == null ? null : software.amazon.sagemaker.monitoringschedule.EndpointInput.builder() + .endpointName(endpointInput.endpointName()) + .localPath(endpointInput.localPath()) + .s3DataDistributionType(endpointInput.s3DataDistributionType().toString()) + .s3InputMode(endpointInput.s3InputMode().toString()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringOutputConfig translate(final MonitoringOutputConfig outputConfig) { + return outputConfig == null? null : software.amazon.sagemaker.monitoringschedule.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.monitoringschedule.MonitoringOutput translate(final MonitoringOutput monitoringOutput) { + return monitoringOutput == null ? null : software.amazon.sagemaker.monitoringschedule.MonitoringOutput.builder() + .s3Output(translate(monitoringOutput.s3Output())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.S3Output translate(final MonitoringS3Output s3Output) { + return s3Output == null? null : software.amazon.sagemaker.monitoringschedule.S3Output.builder() + .localPath(s3Output.localPath()) + .s3UploadMode(s3Output.s3UploadMode().toString()) + .s3Uri(s3Output.s3Uri()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.MonitoringResources translate(final MonitoringResources monitoringResources) { + return monitoringResources == null? null : software.amazon.sagemaker.monitoringschedule.MonitoringResources.builder() + .clusterConfig(translate(monitoringResources.clusterConfig())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.ClusterConfig translate(final MonitoringClusterConfig clusterConfig) { + return clusterConfig == null? null : software.amazon.sagemaker.monitoringschedule.ClusterConfig.builder() + .instanceCount(clusterConfig.instanceCount()) + .instanceType(clusterConfig.instanceType().toString()) + .volumeKmsKeyId(clusterConfig.volumeKmsKeyId()) + .volumeSizeInGB(clusterConfig.volumeSizeInGB()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.NetworkConfig translate(final NetworkConfig networkConfig) { + return networkConfig == null? null : software.amazon.sagemaker.monitoringschedule.NetworkConfig.builder() + .enableInterContainerTrafficEncryption(networkConfig.enableInterContainerTrafficEncryption()) + .enableNetworkIsolation(networkConfig.enableNetworkIsolation()) + .vpcConfig(translate(networkConfig.vpcConfig())) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.VpcConfig translate(final VpcConfig vpcConfig) { + return vpcConfig == null? null : software.amazon.sagemaker.monitoringschedule.VpcConfig.builder() + .securityGroupIds(vpcConfig.securityGroupIds()) + .subnets(vpcConfig.subnets()) + .build(); + } + + static software.amazon.sagemaker.monitoringschedule.StoppingCondition translate(final MonitoringStoppingCondition stoppingCondition) { + return stoppingCondition == null? null : software.amazon.sagemaker.monitoringschedule.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()) + ); + } + +} diff --git a/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/UpdateHandler.java b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/UpdateHandler.java new file mode 100644 index 0000000..7a90595 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/main/java/software/amazon/sagemaker/monitoringschedule/UpdateHandler.java @@ -0,0 +1,97 @@ +package software.amazon.sagemaker.monitoringschedule; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +import software.amazon.awssdk.services.sagemaker.model.UpdateMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.UpdateMonitoringScheduleResponse; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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 UpdateHandler extends BaseHandlerStd { + + private static final String OPERATION = "SageMaker::UpdateMonitoringSchedule"; + 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("AWS-SageMaker-MonitoringSchedule::Update", proxyClient, model, callbackContext) + .translateToServiceRequest(TranslatorForRequest::translateToUpdateRequest) + .makeServiceCall(this::updateResource) + .stabilize(this::stabilizedOnUpdate) + .progress()) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + /** + * Client invocation of the update request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param awsRequest the aws service request to update a resource + * @param proxyClient the aws service client to make the call + * @return awsResponse update resource response + */ + private UpdateMonitoringScheduleResponse updateResource( + final UpdateMonitoringScheduleRequest awsRequest, + final ProxyClient proxyClient) { + try { + return proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::updateMonitoringSchedule); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.monitoringScheduleName(), e); + } catch (final AwsServiceException e) { + throw new CfnGeneralServiceException(OPERATION, e); + } + } + + /** + * This is used to ensure MonitoringSchedule resource has moved from Pending to any terminal state + * (e.g. Scheduled, Stopped). + * @param updateMonitoringScheduleRequest the aws service request to update a resource + * @param proxyClient the aws service client to make the call + * @return boolean state of stabilized or not + */ + private boolean stabilizedOnUpdate( + final UpdateMonitoringScheduleRequest updateMonitoringScheduleRequest, + final UpdateMonitoringScheduleResponse updateMonitoringScheduleResponse, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + + if(model.getMonitoringScheduleArn() == null){ + model.setMonitoringScheduleArn(updateMonitoringScheduleResponse.monitoringScheduleArn()); + } + + final ScheduleStatus monitoringScheduleState = proxyClient.injectCredentialsAndInvokeV2( + TranslatorForRequest.translateToReadRequest(model), + proxyClient.client()::describeMonitoringSchedule).monitoringScheduleStatus(); + + switch (monitoringScheduleState) { + case SCHEDULED: + case STOPPED: + logger.log(String.format("%s [%s] has been stabilized with state %s during update operation.", + ResourceModel.TYPE_NAME, model.getPrimaryIdentifier(), monitoringScheduleState)); + return true; + case PENDING: + logger.log(String.format("%s [%s] is stabilizing during update.", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier())); + return false; + default: + throw new CfnGeneralServiceException("Stabilizing during update of " + model.getPrimaryIdentifier()); + + } + } +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/AbstractTestBase.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/AbstractTestBase.java new file mode 100644 index 0000000..fbe61ae --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/AbstractTestBase.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.monitoringschedule; + +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_ARN = "sampleArn"; + protected static final Instant TEST_TIME = Instant.now(); + protected static final String TEST_SCHEDULE_ARN = "testScheduleArn"; + protected static final String TEST_SCHEDULE_NAME = "testScheduleName"; + protected static final String TEST_ERROR_MESSAGE = "test error message"; + protected static final String TEST_JOB_DEFINITION_NAME = "testJobDefinitionName"; + protected static final String TEST_MONITORING_TYPE = "DataQuality"; + 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-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/CreateHandlerTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/CreateHandlerTest.java new file mode 100644 index 0000000..a409aa0 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/CreateHandlerTest.java @@ -0,0 +1,377 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.CreateMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.MonitoringScheduleConfig; +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.awssdk.services.sagemaker.model.ScheduleConfig; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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.monitoringschedule.AbstractTestBase { + + private final ResourceModel requestModel = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .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 DescribeMonitoringScheduleResponse describeMonitoringScheduleResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastModifiedTime(TEST_TIME) + .build(); + + final CreateMonitoringScheduleResponse createMonitoringScheduleResponse = CreateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(describeMonitoringScheduleResponse); + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.class))) + .thenReturn(createMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .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_WithJobDefinitionName_Success() { + ResourceModel model = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .monitoringScheduleConfig(software.amazon.sagemaker.monitoringschedule.MonitoringScheduleConfig.builder() + .monitoringJobDefinitionName(TEST_JOB_DEFINITION_NAME) + .monitoringType(TEST_MONITORING_TYPE) + .scheduleConfig(software.amazon.sagemaker.monitoringschedule.ScheduleConfig.builder() + .scheduleExpression("cron(0 12 ? * * *)").build()) + .build()) + .lastModifiedTime(TEST_TIME.toString()) + .build(); + + final DescribeMonitoringScheduleResponse describeMonitoringScheduleResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastModifiedTime(TEST_TIME) + .monitoringScheduleConfig(MonitoringScheduleConfig.builder() + .monitoringJobDefinitionName(TEST_JOB_DEFINITION_NAME) + .monitoringType(TEST_MONITORING_TYPE) + .scheduleConfig(ScheduleConfig.builder() + .scheduleExpression("cron(0 12 ? * * *)").build()) + .build()) + .build(); + + final CreateMonitoringScheduleResponse createMonitoringScheduleResponse = CreateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(describeMonitoringScheduleResponse); + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.class))) + .thenReturn(createMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .monitoringScheduleConfig(software.amazon.sagemaker.monitoringschedule.MonitoringScheduleConfig.builder() + .monitoringJobDefinitionName(TEST_JOB_DEFINITION_NAME) + .monitoringType(TEST_MONITORING_TYPE) + .scheduleConfig(software.amazon.sagemaker.monitoringschedule.ScheduleConfig.builder() + .scheduleExpression("cron(0 12 ? * * *)").build()) + .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() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.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_ScheduleAlreadyExists_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.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_SCHEDULE_NAME)); + } + + @Test + public void testCreateHandler_ResourceLimitExceededException() { + final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.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 'monitoringScheduleName' " + + "failed to satisfy constraint: Member must not be null") + .statusCode(400) + .build(); + + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.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().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.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_SuccessfulSchedule() { + final DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse secondDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastModifiedTime(TEST_TIME) + .build(); + + final CreateMonitoringScheduleResponse createMonitoringScheduleResponse = CreateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.class))) + .thenReturn(createMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .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_FailedSchedule() { + final DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse secondDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.FAILED) + .lastModifiedTime(TEST_TIME) + .build(); + + final CreateMonitoringScheduleResponse createMonitoringScheduleResponse = CreateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().createMonitoringSchedule(any(CreateMonitoringScheduleRequest.class))) + .thenReturn(createMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + "Stabilizing of {\"/properties/MonitoringScheduleArn\":\"testScheduleArn\"}")); + } + + 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-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/DeleteHandlerTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/DeleteHandlerTest.java new file mode 100644 index 0000000..ec8a9a0 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/DeleteHandlerTest.java @@ -0,0 +1,197 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.DeleteMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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.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 software.amazon.sagemaker.monitoringschedule.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 DeleteMonitoringScheduleResponse deleteMonitoringScheduleResponse = DeleteMonitoringScheduleResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteMonitoringSchedule(any(DeleteMonitoringScheduleRequest.class))) + .thenReturn(deleteMonitoringScheduleResponse); + + + 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() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().deleteMonitoringSchedule(any(DeleteMonitoringScheduleRequest.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_ScheduleDoesNotExists_Fails() { + when(proxyClient.client().deleteMonitoringSchedule(any(DeleteMonitoringScheduleRequest.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_SCHEDULE_NAME)); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete() { + final DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DeleteMonitoringScheduleResponse deleteMonitoringScheduleResponse = DeleteMonitoringScheduleResponse.builder() + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteMonitoringSchedule(any(DeleteMonitoringScheduleRequest.class))) + .thenReturn(deleteMonitoringScheduleResponse); + + 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 DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse secondDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.FAILED) + .lastModifiedTime(TEST_TIME) + .build(); + + final DeleteMonitoringScheduleResponse deleteMonitoringScheduleResponse = DeleteMonitoringScheduleResponse.builder() + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().deleteMonitoringSchedule(any(DeleteMonitoringScheduleRequest.class))) + .thenReturn(deleteMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + "Delete stabilizing of monitoring schedule: " + TEST_SCHEDULE_NAME)); + } + + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .monitoringScheduleName(TEST_SCHEDULE_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-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ListHandlerTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ListHandlerTest.java new file mode 100644 index 0000000..b6c727d --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ListHandlerTest.java @@ -0,0 +1,156 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.ListMonitoringSchedulesRequest; +import software.amazon.awssdk.services.sagemaker.model.ListMonitoringSchedulesResponse; +import software.amazon.awssdk.services.sagemaker.model.MonitoringScheduleSummary; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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 java.util.ArrayList; +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 ListHandlerTest extends software.amazon.sagemaker.monitoringschedule.AbstractTestBase { + + private static final String OPERATION = "SageMaker::ListMonitoringSchedule"; + 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 MonitoringScheduleSummary scheduleSummary = MonitoringScheduleSummary.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastModifiedTime(TEST_TIME) + .build(); + + final ListMonitoringSchedulesResponse listMonitoringSchedulesResponse = + ListMonitoringSchedulesResponse.builder() + .monitoringScheduleSummaries(scheduleSummary) + .nextToken(TEST_TOKEN) + .build(); + + when(proxyClient.client().listMonitoringSchedules(any(ListMonitoringSchedulesRequest.class))) + .thenReturn(listMonitoringSchedulesResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .build(); + + List expectedModels = new ArrayList(); + expectedModels.add(expectedResourceModel); + + 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_NoMonitoringScheduleExist() { + final ListMonitoringSchedulesResponse listMonitoringSchedulesResponse = + ListMonitoringSchedulesResponse.builder() + .monitoringScheduleSummaries(Collections.emptyList()) + .nextToken(null) + .build(); + + when(proxyClient.client().listMonitoringSchedules(any(ListMonitoringSchedulesRequest.class))) + .thenReturn(listMonitoringSchedulesResponse); + + 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(); + } + + @Test + public void testListHandler_ServiceInternalException() { + AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InternalError").build(); + AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build(); + + when(proxyClient.client().listMonitoringSchedules(any(ListMonitoringSchedulesRequest.class))) + .thenThrow(ex); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(), + OPERATION)); + } + + 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-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ReadHandlerTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ReadHandlerTest.java new file mode 100644 index 0000000..0fc7432 --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/ReadHandlerTest.java @@ -0,0 +1,186 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +import software.amazon.awssdk.services.sagemaker.model.ExecutionStatus; +import software.amazon.awssdk.services.sagemaker.model.MonitoringExecutionSummary; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.awssdk.services.sagemaker.model.ScheduleStatus; +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.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 software.amazon.sagemaker.monitoringschedule.AbstractTestBase { + + private static final String TEST_PROCESSING_JOB_ARN = "testProcessingJobArn"; + private static final String TEST_FAILURE_REASON = "Some failure reason"; + + @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() { + MonitoringExecutionSummary responseExecutionSummary = MonitoringExecutionSummary.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .failureReason(TEST_FAILURE_REASON) + .lastModifiedTime(TEST_TIME) + .monitoringExecutionStatus(ExecutionStatus.COMPLETED) + .processingJobArn(TEST_PROCESSING_JOB_ARN) + .scheduledTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse describeMonitoringScheduleResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastMonitoringExecutionSummary(responseExecutionSummary) + .lastModifiedTime(TEST_TIME) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(describeMonitoringScheduleResponse); + + software.amazon.sagemaker.monitoringschedule.MonitoringExecutionSummary resourceModelExecutionSummary = + software.amazon.sagemaker.monitoringschedule.MonitoringExecutionSummary.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .failureReason(TEST_FAILURE_REASON) + .lastModifiedTime(TEST_TIME.toString()) + .monitoringExecutionStatus(ExecutionStatus.COMPLETED.toString()) + .processingJobArn(TEST_PROCESSING_JOB_ARN) + .scheduledTime(TEST_TIME.toString()) + .build(); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastMonitoringExecutionSummary(resourceModelExecutionSummary) + .lastModifiedTime(TEST_TIME.toString()) + .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()).describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.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_ScheduleDoesNotExist_Fails() { + final AwsServiceException resourceNotFoundException = AwsServiceException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.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().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.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_SCHEDULE_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() + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .build(); + } +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/TranslatorTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/TranslatorTest.java new file mode 100644 index 0000000..d99615c --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/TranslatorTest.java @@ -0,0 +1,145 @@ +package software.amazon.sagemaker.monitoringschedule; + +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-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/UpdateHandlerTest.java b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/UpdateHandlerTest.java new file mode 100644 index 0000000..acf80dc --- /dev/null +++ b/aws-sagemaker-monitoringschedule/src/test/java/software/amazon/sagemaker/monitoringschedule/UpdateHandlerTest.java @@ -0,0 +1,259 @@ +package software.amazon.sagemaker.monitoringschedule; + +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.DescribeMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeMonitoringScheduleResponse; +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.awssdk.services.sagemaker.model.ScheduleStatus; +import software.amazon.awssdk.services.sagemaker.model.UpdateMonitoringScheduleRequest; +import software.amazon.awssdk.services.sagemaker.model.UpdateMonitoringScheduleResponse; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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 UpdateHandlerTest extends software.amazon.sagemaker.monitoringschedule.AbstractTestBase { + + private static final String OPERATION = "SageMaker::UpdateMonitoringSchedule"; + + @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 testUpdateHandler_SimpleSuccess() { + final DescribeMonitoringScheduleResponse describeMonitoringScheduleResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED) + .lastModifiedTime(TEST_TIME) + .build(); + + final UpdateMonitoringScheduleResponse updateMonitoringScheduleResponse = UpdateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(describeMonitoringScheduleResponse); + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.class))) + .thenReturn(updateMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .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 testUpdateHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.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(), + OPERATION)); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException() { + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.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_SCHEDULE_NAME)); + } + + @Test + public void testUpdateHandler_ResourceLimitExceededException() { + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.class))) + .thenThrow(ResourceLimitExceededException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + OPERATION)); + } + + @Test + public void testUpdateHandler_VerifyStabilization_SuccessfulUpdate() { + final DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse secondDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.STOPPED) + .lastModifiedTime(TEST_TIME) + .build(); + + final UpdateMonitoringScheduleResponse updateMonitoringScheduleResponse = UpdateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.class))) + .thenReturn(updateMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.STOPPED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .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 testUpdateHandler_VerifyStabilization_FailedSchedule() { + final DescribeMonitoringScheduleResponse firstDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.PENDING) + .lastModifiedTime(TEST_TIME) + .build(); + + final DescribeMonitoringScheduleResponse secondDescribeResponse = + DescribeMonitoringScheduleResponse.builder() + .creationTime(TEST_TIME) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.FAILED) + .lastModifiedTime(TEST_TIME) + .build(); + + final UpdateMonitoringScheduleResponse updateMonitoringScheduleResponse = UpdateMonitoringScheduleResponse.builder() + .monitoringScheduleArn(TEST_SCHEDULE_ARN) + .build(); + + when(proxyClient.client().describeMonitoringSchedule(any(DescribeMonitoringScheduleRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().updateMonitoringSchedule(any(UpdateMonitoringScheduleRequest.class))) + .thenReturn(updateMonitoringScheduleResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + "Stabilizing during update of {\"/properties/MonitoringScheduleArn\":\"testScheduleArn\"}")); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .creationTime(TEST_TIME.toString()) + .endpointName(TEST_ENDPOINT_NAME) + .monitoringScheduleName(TEST_SCHEDULE_NAME) + .monitoringScheduleStatus(ScheduleStatus.SCHEDULED.toString()) + .lastModifiedTime(TEST_TIME.toString()) + .build(); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final UpdateHandler handler = new UpdateHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} \ No newline at end of file diff --git a/aws-sagemaker-monitoringschedule/template.yml b/aws-sagemaker-monitoringschedule/template.yml new file mode 100644 index 0000000..86cc45b --- /dev/null +++ b/aws-sagemaker-monitoringschedule/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::MonitoringSchedule 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.monitoringschedule.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-monitoringschedule-1.0.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.monitoringschedule.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-monitoringschedule-1.0.jar diff --git a/aws-sagemaker-pipeline/.rpdk-config b/aws-sagemaker-pipeline/.rpdk-config new file mode 100644 index 0000000..a387e40 --- /dev/null +++ b/aws-sagemaker-pipeline/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::SageMaker::Pipeline", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.pipeline.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.pipeline.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "sagemaker", + "pipeline" + ], + "protocolVersion": "2.0.0" + } +} diff --git a/aws-sagemaker-pipeline/README.md b/aws-sagemaker-pipeline/README.md new file mode 100644 index 0000000..3ba5e07 --- /dev/null +++ b/aws-sagemaker-pipeline/README.md @@ -0,0 +1,121 @@ +# AWS::SageMaker::Pipeline + +Resource Type definition for AWS::SageMaker::Pipeline + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::Pipeline",
+    "Properties" : {
+        "PipelineName" : String,
+        "PipelineDisplayName" : String,
+        "PipelineDescription" : String,
+        "PipelineDefinition" : PipelineDefinition,
+        "RoleArn" : String,
+        "Tags" : [ Tag, ... ]
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::Pipeline
+Properties:
+    PipelineName: String
+    PipelineDisplayName: String
+    PipelineDescription: String
+    PipelineDefinition: PipelineDefinition
+    RoleArn: String
+    Tags: 
+      - Tag
+
+ +## Properties + +#### PipelineName + +The name of the Pipeline. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_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) + +#### PipelineDisplayName + +The display name of the Pipeline. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_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) + +#### PipelineDescription + +The description of the Pipeline. + +_Required_: No + +_Type_: String + +_Maximum_: 3072 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PipelineDefinition + +_Required_: No + +_Type_: PipelineDefinition + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RoleArn + +Role Arn + +_Required_: Yes + +_Type_: String + +_Minimum_: 20 + +_Maximum_: 2048 + +_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$ + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tags + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the PipelineName. diff --git a/aws-sagemaker-pipeline/aws-sagemaker-pipeline.json b/aws-sagemaker-pipeline/aws-sagemaker-pipeline.json new file mode 100644 index 0000000..3e391e1 --- /dev/null +++ b/aws-sagemaker-pipeline/aws-sagemaker-pipeline.json @@ -0,0 +1,139 @@ +{ + "typeName": "AWS::SageMaker::Pipeline", + "description": "Resource Type definition for AWS::SageMaker::Pipeline", + "additionalProperties": false, + "properties": { + "PipelineName": { + "type": "string", + "description": "The name of the Pipeline.", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*" + }, + "PipelineDisplayName": { + "type": "string", + "description": "The display name of the Pipeline.", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*" + }, + "PipelineDescription": { + "type": "string", + "description": "The description of the Pipeline.", + "minLength": 0, + "maxLength": 3072 + }, + "PipelineDefinition": { + "type": "object", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "PipelineDefinitionBody": { + "type": "string", + "description": "A specification that defines the pipeline in JSON format." + } + }, + "required": ["PipelineDefinitionBody"] + }, + { + "additionalProperties": false, + "properties": { + "PipelineDefinitionS3Location": { + "$ref": "#/definitions/S3Location" + } + }, + "required": ["PipelineDefinitionS3Location"] + } + ] + }, + "RoleArn": { + "type": "string", + "description": "Role Arn", + "minLength": 20, + "maxLength": 2048, + "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$" + }, + "Tags": { + "type": "array", + "uniqueItems": false, + "items": { + "$ref": "#/definitions/Tag" + } + } + }, + "definitions": { + "S3Location": { + "type": "object", + "additionalProperties": false, + "properties": { + "Bucket": { + "description": "The name of the S3 bucket where the PipelineDefinition file is stored.", + "type": "string" + }, + "Key": { + "description": "The file name of the PipelineDefinition file (Amazon S3 object name).", + "type": "string" + }, + "Version": { + "description": "For versioning-enabled buckets, a specific version of the PipelineDefinition file.", + "type": "string" + }, + "ETag": { + "description": "The Amazon S3 ETag (a file checksum) of the PipelineDefinition file. If you don't specify a value, SageMaker skips ETag validation of your PipelineDefinition file.", + "type": "string" + } + }, + "required": ["Bucket", "Key"] + }, + "Tag": { + "type": "object", + "additionalProperties": false, + "properties": { + "Value": { + "type": "string" + }, + "Key": { + "type": "string" + } + }, + "required": ["Value", "Key"] + } + }, + "required": ["PipelineName", "PipelineDefinition", "RoleArn"], + "createOnlyProperties": ["/properties/PipelineName"], + "primaryIdentifier": ["/properties/PipelineName"], + "handlers": { + "create": { + "permissions": [ + "iam:PassRole", + "s3:GetObject", + "sagemaker:CreatePipeline", + "sagemaker:DescribePipeline" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribePipeline" + ] + }, + "update": { + "permissions": [ + "iam:PassRole", + "s3:GetObject", + "sagemaker:UpdatePipeline", + "sagemaker:DescribePipeline" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeletePipeline" + ] + }, + "list": { + "permissions": [ + "sagemaker:ListPipelines" + ] + } + } +} \ No newline at end of file diff --git a/aws-sagemaker-pipeline/docs/README.md b/aws-sagemaker-pipeline/docs/README.md new file mode 100644 index 0000000..3ba5e07 --- /dev/null +++ b/aws-sagemaker-pipeline/docs/README.md @@ -0,0 +1,121 @@ +# AWS::SageMaker::Pipeline + +Resource Type definition for AWS::SageMaker::Pipeline + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::Pipeline",
+    "Properties" : {
+        "PipelineName" : String,
+        "PipelineDisplayName" : String,
+        "PipelineDescription" : String,
+        "PipelineDefinition" : PipelineDefinition,
+        "RoleArn" : String,
+        "Tags" : [ Tag, ... ]
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::Pipeline
+Properties:
+    PipelineName: String
+    PipelineDisplayName: String
+    PipelineDescription: String
+    PipelineDefinition: PipelineDefinition
+    RoleArn: String
+    Tags: 
+      - Tag
+
+ +## Properties + +#### PipelineName + +The name of the Pipeline. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_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) + +#### PipelineDisplayName + +The display name of the Pipeline. + +_Required_: No + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 256 + +_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) + +#### PipelineDescription + +The description of the Pipeline. + +_Required_: No + +_Type_: String + +_Maximum_: 3072 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PipelineDefinition + +_Required_: No + +_Type_: PipelineDefinition + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RoleArn + +Role Arn + +_Required_: Yes + +_Type_: String + +_Minimum_: 20 + +_Maximum_: 2048 + +_Pattern_: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$ + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tags + +_Required_: No + +_Type_: List of Tag + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the PipelineName. diff --git a/aws-sagemaker-pipeline/docs/pipelinedefinition.md b/aws-sagemaker-pipeline/docs/pipelinedefinition.md new file mode 100644 index 0000000..b15c842 --- /dev/null +++ b/aws-sagemaker-pipeline/docs/pipelinedefinition.md @@ -0,0 +1,42 @@ +# AWS::SageMaker::Pipeline PipelineDefinition + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "PipelineDefinitionBody" : String,
+    "PipelineDefinitionS3Location" : S3Location
+}
+
+ +### YAML + +
+PipelineDefinitionBody: String
+PipelineDefinitionS3Location: S3Location
+
+ +## Properties + +#### PipelineDefinitionBody + +A specification that defines the pipeline in JSON format. + +_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) + +#### PipelineDefinitionS3Location + +_Required_: Yes + +_Type_: S3Location + +_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-pipeline/docs/s3location.md b/aws-sagemaker-pipeline/docs/s3location.md new file mode 100644 index 0000000..b8cc885 --- /dev/null +++ b/aws-sagemaker-pipeline/docs/s3location.md @@ -0,0 +1,68 @@ +# AWS::SageMaker::Pipeline S3Location + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Bucket" : String,
+    "Key" : String,
+    "Version" : String,
+    "ETag" : String
+}
+
+ +### YAML + +
+Bucket: String
+Key: String
+Version: String
+ETag: String
+
+ +## Properties + +#### Bucket + +The name of the S3 bucket where the PipelineDefinition file is stored. + +_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) + +#### Key + +The file name of the PipelineDefinition file (Amazon S3 object name). + +_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) + +#### Version + +For versioning-enabled buckets, a specific version of the PipelineDefinition file. + +_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) + +#### ETag + +The Amazon S3 ETag (a file checksum) of the PipelineDefinition file. If you don't specify a value, SageMaker skips ETag validation of your PipelineDefinition file. + +_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) + diff --git a/aws-sagemaker-pipeline/docs/tag.md b/aws-sagemaker-pipeline/docs/tag.md new file mode 100644 index 0000000..651a0d8 --- /dev/null +++ b/aws-sagemaker-pipeline/docs/tag.md @@ -0,0 +1,40 @@ +# AWS::SageMaker::Pipeline Tag + +## 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-pipeline/lombok.config b/aws-sagemaker-pipeline/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-pipeline/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-pipeline/pom.xml b/aws-sagemaker-pipeline/pom.xml new file mode 100644 index 0000000..a290014 --- /dev/null +++ b/aws-sagemaker-pipeline/pom.xml @@ -0,0 +1,216 @@ + + + 4.0.0 + + software.amazon.sagemaker.pipeline + aws-sagemaker-pipeline-handler + aws-sagemaker-pipeline-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + software.amazon.awssdk + sagemaker + 2.15.41 + + + + software.amazon.awssdk + s3 + 2.10.1 + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + 2.0.2 + + + + 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.6 + + + INSTRUCTION + COVEREDRATIO + 0.6 + + + + + + + + + + + + ${project.basedir} + + aws-sagemaker-pipeline.json + + + + + \ No newline at end of file diff --git a/aws-sagemaker-pipeline/resource-role.yaml b/aws-sagemaker-pipeline/resource-role.yaml new file mode 100644 index 0000000..9b38882 --- /dev/null +++ b/aws-sagemaker-pipeline/resource-role.yaml @@ -0,0 +1,37 @@ +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" + - "s3:GetObject" + - "sagemaker:CreatePipeline" + - "sagemaker:DeletePipeline" + - "sagemaker:DescribePipeline" + - "sagemaker:ListPipelines" + - "sagemaker:UpdatePipeline" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/BaseHandlerStd.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/BaseHandlerStd.java new file mode 100644 index 0000000..5a854f4 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/BaseHandlerStd.java @@ -0,0 +1,36 @@ +package software.amazon.sagemaker.pipeline; + +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-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CallbackContext.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CallbackContext.java new file mode 100644 index 0000000..9074a79 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CallbackContext.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.pipeline; + +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-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ClientBuilder.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ClientBuilder.java new file mode 100644 index 0000000..e4eef4b --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ClientBuilder.java @@ -0,0 +1,16 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.services.sagemaker.SageMakerClient; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Provides APIs to build service client. + */ +public class ClientBuilder { + + public static SageMakerClient getClient() { + return SageMakerClient.create(); + } +} \ No newline at end of file diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Configuration.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Configuration.java new file mode 100644 index 0000000..a8479fa --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Configuration.java @@ -0,0 +1,7 @@ +package software.amazon.sagemaker.pipeline; + +class Configuration extends BaseConfiguration { + public Configuration() { + super("aws-sagemaker-pipeline.json"); + } +} \ No newline at end of file diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CreateHandler.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CreateHandler.java new file mode 100644 index 0000000..34724d7 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/CreateHandler.java @@ -0,0 +1,69 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.CreatePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.CreatePipelineResponse; +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 CreateHandler extends BaseHandlerStd { + + private static final String OPERATION = "AWS-SageMaker-Pipeline::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(); + + if (model.getPipelineDefinition().getPipelineDefinitionS3Location() != null) { + String pipelineDefinition = S3ClientWrapper.getBodyFromS3( + model.getPipelineDefinition().getPipelineDefinitionS3Location(), + proxy, + logger + ); + model.setPipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(pipelineDefinition) + .build() + ); + } + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate(OPERATION, 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 CreatePipelineResponse createResource( + final CreatePipelineRequest awsRequest, + final ProxyClient proxyClient) { + + CreatePipelineResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createPipeline); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.CREATE.toString(), ResourceModel.TYPE_NAME, awsRequest.pipelineName(), e); + } + return response; + } +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/DeleteHandler.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/DeleteHandler.java new file mode 100644 index 0000000..c39206c --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/DeleteHandler.java @@ -0,0 +1,67 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DeletePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DeletePipelineResponse; +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 static final String OPERATION = "AWS-SageMaker-Pipeline::Delete"; + private Logger logger; + + @Override + protected ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + ProxyClient proxyClient, + 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) + .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 DeletePipelineResponse deleteResource( + final DeletePipelineRequest awsRequest, + final ProxyClient proxyClient + ) { + + DeletePipelineResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::deletePipeline); + } catch (ResourceNotFoundException e) { + // NotFound responded from Delete handler will be considered as success by CFN backend service. + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.pipelineName()); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.DELETE.toString(), + ResourceModel.TYPE_NAME, awsRequest.pipelineName(), e); + } + + return response; + } +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ListHandler.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ListHandler.java new file mode 100644 index 0000000..7403012 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ListHandler.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesRequest; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesResponse; +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-Pipeline::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(this::listResources) + .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 describe a resource + * @param proxyClient the aws service client to make the call + * @return listPipelinesResponse + */ + private ListPipelinesResponse listResources( + final ListPipelinesRequest awsRequest, + final ProxyClient proxyClient + ) { + + ListPipelinesResponse listPipelinesResponse = null; + try { + listPipelinesResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, + proxyClient.client()::listPipelines); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.LIST.toString(), ResourceModel.TYPE_NAME, null, e); + } + + return listPipelinesResponse; + } + + /** + * Build the Progress Event object from the SageMaker ListPipelines 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 ListPipelinesResponse listResponse + ) { + return ProgressEvent.builder() + .nextToken(listResponse.nextToken()) + .resourceModels(TranslatorForResponse.translateFromListResponse(listResponse)) + .status(OperationStatus.SUCCESS) + .build(); + } + +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ReadHandler.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ReadHandler.java new file mode 100644 index 0000000..d2fd9b9 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/ReadHandler.java @@ -0,0 +1,72 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineResponse; +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 static final String OPERATION = "AWS-SageMaker-Pipeline::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(this::readResource) + .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 DescribePipelineResponse readResource( + final DescribePipelineRequest awsRequest, + final ProxyClient proxyClient + ) { + DescribePipelineResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describePipeline); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.pipelineName(), e); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.READ.toString(), ResourceModel.TYPE_NAME, awsRequest.pipelineName(), 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 DescribePipelineResponse awsResponse) { + return ProgressEvent.defaultSuccessHandler(TranslatorForResponse.translateFromReadResponse(awsResponse)); + } +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/S3ClientWrapper.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/S3ClientWrapper.java new file mode 100644 index 0000000..361e9f1 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/S3ClientWrapper.java @@ -0,0 +1,40 @@ +package software.amazon.sagemaker.pipeline; + +import com.amazonaws.AmazonServiceException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; + +public class S3ClientWrapper { + + private static final S3Client s3Client = S3Client.builder().build(); + + public static String getBodyFromS3( + S3Location s3Location, + AmazonWebServicesClientProxy proxy, + Logger logger + ) { + try { + GetObjectRequest request = GetObjectRequest.builder().bucket(s3Location.getBucket()) + .ifMatch(s3Location.getETag()).versionId(s3Location.getVersion()) + .key(s3Location.getKey()).build(); + logger.log("Fetching file from S3 with request:" + request.toString()); + + String response = proxy.injectCredentialsAndInvokeV2Bytes(request, s3Client::getObjectAsBytes).asUtf8String(); + return response; + } catch (NoSuchKeyException nske) { + String errorMsg = String.format("No such key %s/%s with version: %s", + s3Location.getBucket(), s3Location.getKey(), s3Location.getVersion()); + logger.log(errorMsg); + throw new CfnInvalidRequestException(errorMsg); + } catch (S3Exception | AmazonServiceException se) { + logger.log("Error while fetching file from S3 " + se.getMessage()); + throw new CfnGeneralServiceException(se.getMessage(), se); + } + } +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Translator.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Translator.java new file mode 100644 index 0000000..809319c --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/Translator.java @@ -0,0 +1,79 @@ +package software.amazon.sagemaker.pipeline; + +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-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForRequest.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForRequest.java new file mode 100644 index 0000000..52b9d0b --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForRequest.java @@ -0,0 +1,85 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.services.sagemaker.model.CreatePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DeletePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesRequest; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.awssdk.services.sagemaker.model.UpdatePipelineRequest; + +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 createPipelineRequest - service request to create a resource + */ + static CreatePipelineRequest translateToCreateRequest(final ResourceModel model) { + return CreatePipelineRequest.builder() + .pipelineName(model.getPipelineName()) + .pipelineDisplayName(model.getPipelineDisplayName()) + .pipelineDefinition(model.getPipelineDefinition().getPipelineDefinitionBody()) + .pipelineDescription(model.getPipelineDescription()) + .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 describePipelineRequest - the aws service request to describe a resource + */ + static DescribePipelineRequest translateToReadRequest(final ResourceModel model) { + return DescribePipelineRequest.builder().pipelineName(model.getPipelineName()).build(); + } + + /** + * Request to delete a resource + * @param model resource model + * @return deletePipelineRequest the aws service request to delete a resource + */ + static DeletePipelineRequest translateToDeleteRequest(final ResourceModel model) { + return DeletePipelineRequest.builder().pipelineName(model.getPipelineName()).build(); + } + + /** + * Request to update properties of a previously created resource + * @param model resource model + * @return updatePipelineRequest the aws service request to modify a resource + */ + static UpdatePipelineRequest translateToUpdateRequest(final ResourceModel model) { + return UpdatePipelineRequest.builder() + .pipelineName(model.getPipelineName()) + .pipelineDisplayName(model.getPipelineDisplayName()) + .pipelineDescription(model.getPipelineDescription()) + .pipelineDefinition(model.getPipelineDefinition().getPipelineDefinitionBody()) + .roleArn(model.getRoleArn()) + .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 ListPipelinesRequest translateToListRequest(final String nextToken) { + return ListPipelinesRequest.builder().nextToken(nextToken).build(); + } + +} \ No newline at end of file diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForResponse.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForResponse.java new file mode 100644 index 0000000..c0fc179 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/TranslatorForResponse.java @@ -0,0 +1,48 @@ +package software.amazon.sagemaker.pipeline; + +import com.amazonaws.util.StringUtils; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineResponse; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesResponse; + +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 DescribePipelineResponse awsResponse) { + return ResourceModel.builder() + .pipelineName(awsResponse.pipelineName()) + .pipelineDefinition( + PipelineDefinition.builder() + .pipelineDefinitionBody(awsResponse.pipelineDefinition()) + .build() + ) + .pipelineDescription(awsResponse.pipelineDescription()) + .pipelineDisplayName(awsResponse.pipelineDisplayName()) + .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 ListPipelinesResponse awsResponse) { + return Translator.streamOfOrEmpty(awsResponse.pipelineSummaries()) + .map(summary -> ResourceModel.builder() + .pipelineName(summary.pipelineName()) + .pipelineDisplayName(summary.pipelineDisplayName()) + .pipelineDescription(summary.pipelineDescription()) + .roleArn(summary.roleArn()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/UpdateHandler.java b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/UpdateHandler.java new file mode 100644 index 0000000..50f7be7 --- /dev/null +++ b/aws-sagemaker-pipeline/src/main/java/software/amazon/sagemaker/pipeline/UpdateHandler.java @@ -0,0 +1,76 @@ +package software.amazon.sagemaker.pipeline; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.UpdatePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.UpdatePipelineResponse; +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 UpdateHandler extends BaseHandlerStd { + + private static final String OPERATION = "AWS-SageMaker-Pipeline::Update"; + private Logger logger; + + @Override + protected ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + ProxyClient proxyClient, + Logger logger + ) { + + this.logger = logger; + final ResourceModel model = request.getDesiredResourceState(); + + if (model.getPipelineDefinition().getPipelineDefinitionS3Location() != null) { + String pipelineDefinition = S3ClientWrapper.getBodyFromS3( + model.getPipelineDefinition().getPipelineDefinitionS3Location(), + proxy, + logger + ); + model.setPipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(pipelineDefinition) + .build() + ); + } + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate(OPERATION, proxyClient, model, callbackContext) + .translateToServiceRequest(TranslatorForRequest::translateToUpdateRequest) + .makeServiceCall(this::updateResource) + .progress()) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + /** + * Client invocation of the update request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param awsRequest the aws service request to update a resource + * @param proxyClient the aws service client to make the call + * @return awsResponse update resource response + */ + private UpdatePipelineResponse updateResource( + final UpdatePipelineRequest awsRequest, + final ProxyClient proxyClient + ) { + UpdatePipelineResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::updatePipeline); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, awsRequest.pipelineName(), e); + } catch (final AwsServiceException e) { + Translator.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, awsRequest.pipelineName(), e); + } + return response; + } + +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/AbstractTestBase.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/AbstractTestBase.java new file mode 100644 index 0000000..746564b --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/AbstractTestBase.java @@ -0,0 +1,87 @@ +package software.amazon.sagemaker.pipeline; + +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.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class AbstractTestBase { + + protected static final String TEST_PIPELINE_NAME = "test-pipeline-name"; + protected static final String TEST_PIPELINE_ARN = "test-pipeline-arn"; + protected static final String TEST_PIPELINE_DISPLAY_NAME = "test-pipeline-display-name"; + protected static final String TEST_PIPELINE_DESCRIPTION = "test-pipeline-description"; + protected static final String TEST_PIPELINE_DEFINITION = "test-pipeline-definition"; + protected static final String TEST_ROLE_ARN = "test-role-arn"; + 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; + } + }; + } + + static ResourceModel getResourceModel() { + return ResourceModel.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .pipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(TEST_PIPELINE_DEFINITION).build()) + .roleArn(TEST_ROLE_ARN) + .build(); + } +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/CreateHandlerTest.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/CreateHandlerTest.java new file mode 100644 index 0000000..1a247e5 --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/CreateHandlerTest.java @@ -0,0 +1,217 @@ +package software.amazon.sagemaker.pipeline; + +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.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.CreatePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.CreatePipelineResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineResponse; +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.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 java.time.Duration; +import java.time.Instant; + +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; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest extends AbstractTestBase { + + private final ResourceModel requestModel = getResourceModel(); + + @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_PipelineDefinitionBody() { + final DescribePipelineResponse describePipelineResponse = + DescribePipelineResponse.builder() + .pipelineArn(TEST_PIPELINE_ARN) + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDefinition(TEST_PIPELINE_DEFINITION) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .creationTime(Instant.now()) + .build(); + + final CreatePipelineResponse createPipelineResponse = CreatePipelineResponse.builder() + .pipelineArn(TEST_PIPELINE_ARN) + .build(); + + when(proxyClient.client().describePipeline(any(DescribePipelineRequest.class))) + .thenReturn(describePipelineResponse); + when(proxyClient.client().createPipeline(any(CreatePipelineRequest.class))) + .thenReturn(createPipelineResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(TEST_PIPELINE_DEFINITION).build()) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .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().createPipeline(any(CreatePipelineRequest.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_PipelineAlreadyExists() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createPipeline(any(CreatePipelineRequest.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_PIPELINE_NAME)); + } + + @Test + public void testCreateHandler_ResourceLimitExceededException() { + final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createPipeline(any(CreatePipelineRequest.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 'pipelineName' failed to " + + "satisfy constraint: Member must not be null") + .build()) + .statusCode(400) + .build(); + + when(proxyClient.client().createPipeline(any(CreatePipelineRequest.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().createPipeline(any(CreatePipelineRequest.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 software.amazon.sagemaker.pipeline.CreateHandler handler = new CreateHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/DeleteHandlerTest.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/DeleteHandlerTest.java new file mode 100644 index 0000000..fbe3838 --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/DeleteHandlerTest.java @@ -0,0 +1,118 @@ +package software.amazon.sagemaker.pipeline; + +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.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DeletePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DeletePipelineResponse; +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.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; + +@Slf4j +@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 DeletePipelineResponse deletePipelineResponse = DeletePipelineResponse.builder() + .pipelineArn(TEST_ROLE_ARN) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + when(proxyClient.client().deletePipeline(any(DeletePipelineRequest.class))) + .thenReturn(deletePipelineResponse); + + 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(getResourceModel()) + .build(); + + when(proxyClient.client().deletePipeline(any(DeletePipelineRequest.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_ResourceNotFoundException() { + when(proxyClient.client().deletePipeline(any(DeletePipelineRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PIPELINE_NAME)); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.pipeline.DeleteHandler handler = new DeleteHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ListHandlerTest.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ListHandlerTest.java new file mode 100644 index 0000000..b5faccc --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ListHandlerTest.java @@ -0,0 +1,153 @@ +package software.amazon.sagemaker.pipeline; + +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.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesRequest; +import software.amazon.awssdk.services.sagemaker.model.ListPipelinesResponse; +import software.amazon.awssdk.services.sagemaker.model.PipelineSummary; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +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 java.util.ArrayList; +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; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest extends AbstractTestBase { + + public static final String TEST_TOKEN = "test-token"; + + @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 PipelineSummary pipelineSummary = PipelineSummary.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .pipelineArn(TEST_PIPELINE_ARN) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .build(); + + final ListPipelinesResponse listPipelinesResponse = + ListPipelinesResponse.builder() + .pipelineSummaries(pipelineSummary) + .nextToken(TEST_TOKEN) + .build(); + + when(proxyClient.client().listPipelines(any(ListPipelinesRequest.class))) + .thenReturn(listPipelinesResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .build(); + + List expectedModels = new ArrayList(); + expectedModels.add(expectedResourceModel); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .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_NoPipelineExist() { + final ListPipelinesResponse listPipelinesResponse = + ListPipelinesResponse.builder() + .pipelineSummaries(Collections.emptyList()) + .nextToken(null) + .build(); + + when(proxyClient.client().listPipelines(any(ListPipelinesRequest.class))) + .thenReturn(listPipelinesResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .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(); + } + + @Test + public void testListHandler_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(getResourceModel()) + .build(); + + when(proxyClient.client().listPipelines(any(ListPipelinesRequest.class))) + .thenThrow(serviceInternalException); + + Exception exception = assertThrows(CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.ServiceInternalError.getMessage(), + serviceInternalException.awsErrorDetails().errorMessage())); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.pipeline.ListHandler handler = new ListHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ReadHandlerTest.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ReadHandlerTest.java new file mode 100644 index 0000000..d56ad0b --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/ReadHandlerTest.java @@ -0,0 +1,135 @@ +package software.amazon.sagemaker.pipeline; + +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.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +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 java.time.Instant; + +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; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest extends AbstractTestBase { + + private final ResourceModel requestModel = getResourceModel(); + + @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() { + final DescribePipelineResponse describePipelineResponse = + DescribePipelineResponse.builder() + .pipelineArn(TEST_PIPELINE_ARN) + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDefinition(TEST_PIPELINE_DEFINITION) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .creationTime(Instant.now()) + .build(); + + when(proxyClient.client().describePipeline(any(DescribePipelineRequest.class))) + .thenReturn(describePipelineResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(TEST_PIPELINE_DEFINITION).build()) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .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 testReadHandler_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(getResourceModel()) + .build(); + + when(proxyClient.client().describePipeline(any(DescribePipelineRequest.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 testReadHandler_ResourceNotFoundException() { + when(proxyClient.client().describePipeline(any(DescribePipelineRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PIPELINE_NAME)); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.pipeline.ReadHandler handler = new ReadHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/UpdateHandlerTest.java b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/UpdateHandlerTest.java new file mode 100644 index 0000000..3d16324 --- /dev/null +++ b/aws-sagemaker-pipeline/src/test/java/software/amazon/sagemaker/pipeline/UpdateHandlerTest.java @@ -0,0 +1,185 @@ +package software.amazon.sagemaker.pipeline; + +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.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribePipelineResponse; +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.awssdk.services.sagemaker.model.UpdatePipelineRequest; +import software.amazon.awssdk.services.sagemaker.model.UpdatePipelineResponse; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +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 java.time.Duration; +import java.time.Instant; + +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; + +@Slf4j +@ExtendWith(MockitoExtension.class) +public class UpdateHandlerTest 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 testUpdateHandler_SimpleSuccess() { + final DescribePipelineResponse describePipelineResponse = + DescribePipelineResponse.builder() + .pipelineArn(TEST_PIPELINE_ARN) + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDefinition(TEST_PIPELINE_DEFINITION) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .roleArn(TEST_ROLE_ARN) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .creationTime(Instant.now()) + .build(); + + final UpdatePipelineResponse updatePipelineResponse = UpdatePipelineResponse.builder() + .pipelineArn(TEST_PIPELINE_ARN) + .build(); + + when(proxyClient.client().describePipeline(any(DescribePipelineRequest.class))) + .thenReturn(describePipelineResponse); + when(proxyClient.client().updatePipeline(any(UpdatePipelineRequest.class))) + .thenReturn(updatePipelineResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .pipelineName(TEST_PIPELINE_NAME) + .pipelineDescription(TEST_PIPELINE_DESCRIPTION) + .pipelineDisplayName(TEST_PIPELINE_DISPLAY_NAME) + .pipelineDefinition(PipelineDefinition.builder() + .pipelineDefinitionBody(TEST_PIPELINE_DEFINITION).build()) + .roleArn(TEST_ROLE_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_PipelineAlreadyExists() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().updatePipeline(any(UpdatePipelineRequest.class))) + .thenThrow(resourceInUseException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + Exception exception = assertThrows(ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.AlreadyExists.getMessage(), + ResourceModel.TYPE_NAME, TEST_PIPELINE_NAME)); + } + + @Test + public void testReadHandler_ResourceNotFoundException() { + when(proxyClient.client().updatePipeline(any(UpdatePipelineRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PIPELINE_NAME)); + } + + @Test + public void testUpdateHandler_ResourceLimitExceededException() { + final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().updatePipeline(any(UpdatePipelineRequest.class))) + .thenThrow(resourceLimitExceededException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .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 'pipelineName' failed to " + + "satisfy constraint: Member must not be null") + .build()) + .statusCode(400) + .build(); + + when(proxyClient.client().updatePipeline(any(UpdatePipelineRequest.class))) + .thenThrow(validationFailureException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getResourceModel()) + .build(); + + Exception exception = assertThrows(CfnInvalidRequestException.class, () -> invokeHandleRequest(request)); + + assertThat(exception.getMessage()).isEqualTo(String.format(HandlerErrorCode.InvalidRequest.getMessage(), + validationFailureException.awsErrorDetails().errorMessage())); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.pipeline.UpdateHandler handler = new UpdateHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-pipeline/template.yaml b/aws-sagemaker-pipeline/template.yaml new file mode 100644 index 0000000..176a18d --- /dev/null +++ b/aws-sagemaker-pipeline/template.yaml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::Pipeline 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.pipeline.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-pipeline-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.pipeline.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-pipeline-handler-1.0-SNAPSHOT.jar diff --git a/aws-sagemaker-project/.rpdk-config b/aws-sagemaker-project/.rpdk-config new file mode 100644 index 0000000..012dcfa --- /dev/null +++ b/aws-sagemaker-project/.rpdk-config @@ -0,0 +1,22 @@ +{ + "typeName": "AWS::SageMaker::Project", + "language": "java", + "runtime": "java8", + "entrypoint": "software.amazon.sagemaker.project.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.sagemaker.project.HandlerWrapper::testEntrypoint", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": null, + "namespace": [ + "software", + "amazon", + "sagemaker", + "project" + ], + "codegen_template_path": "guided_aws", + "protocolVersion": "2.0.0" + } +} diff --git a/aws-sagemaker-project/README.md b/aws-sagemaker-project/README.md new file mode 100644 index 0000000..feadf22 --- /dev/null +++ b/aws-sagemaker-project/README.md @@ -0,0 +1,118 @@ +# AWS::SageMaker::Project + +Resource Type definition for AWS::SageMaker::Project + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::Project",
+    "Properties" : {
+        "Tags" : [ Tag, ... ],
+        "ProjectName" : String,
+        "ProjectDescription" : String,
+        "ServiceCatalogProvisioningDetails" : ServiceCatalogProvisioningDetails,
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::Project
+Properties:
+    Tags: 
+      - Tag
+    ProjectName: String
+    ProjectDescription: String
+    ServiceCatalogProvisioningDetails: ServiceCatalogProvisioningDetails
+
+ +## Properties + +#### 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) + +#### ProjectName + +The name of the project. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 32 + +_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) + +#### ProjectDescription + +The description of the project. + +_Required_: No + +_Type_: String + +_Maximum_: 1024 + +_Pattern_: .* + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ServiceCatalogProvisioningDetails + +Input ServiceCatalog Provisioning Details + +_Required_: Yes + +_Type_: ServiceCatalogProvisioningDetails + +_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 ProjectArn. + +### 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). + +#### ProjectArn + +The Amazon Resource Name (ARN) of the Project. + +#### CreationTime + +The time at which the project was created. + +#### ProjectId + +Project Id. + +#### ServiceCatalogProvisionedProductDetails + +Provisioned ServiceCatalog Details + +#### ProjectStatus + +The status of a project. + diff --git a/aws-sagemaker-project/aws-sagemaker-project.json b/aws-sagemaker-project/aws-sagemaker-project.json new file mode 100644 index 0000000..56becfe --- /dev/null +++ b/aws-sagemaker-project/aws-sagemaker-project.json @@ -0,0 +1,232 @@ +{ + "typeName": "AWS::SageMaker::Project", + "description": "Resource Type definition for AWS::SageMaker::Project", + "definitions": { + "Tag" : { + "description" : "A key-value pair to associate with a resource.", + "type" : "object", + "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" ], + "additionalProperties": false + }, + "ProjectDescription": { + "type" : "string", + "description" : "The description of the project.", + "pattern": ".*", + "maxLength" : 1024 + }, + "ProjectId": { + "type" : "string", + "description" : "Project Id.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*", + "maxLength" : 20 + }, + "ProvisionedProductStatusMessage": { + "type" : "string", + "description" : "Provisioned Product Status Message" + }, + "ProjectName": { + "type" : "string", + "description" : "The name of the project.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "minLength": 1, + "maxLength" : 32 + }, + "ProjectArn": { + "description": "The Amazon Resource Name (ARN) of the Project.", + "type" : "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "arn:aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:project.*" + }, + "ProductId": { + "type" : "string", + "description" : "Service Catalog product identifier.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "maxLength" : 100 + }, + "ProvisioningArtifactId": { + "type" : "string", + "description" : "The identifier of the provisioning artifact (also known as a version).", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "maxLength" : 100 + }, + "PathId": { + "type" : "string", + "description" : "The path identifier of the product.", + "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9])*$", + "maxLength" : 100 + }, + "ProvisioningParameter" : { + "description" : "Information about a parameter used to provision a product.", + "type" : "object", + "properties" : { + "Key" : { + "type" : "string", + "description" : "The parameter key.", + "minLength" : 1, + "maxLength" : 1000, + "pattern": ".*" + }, + "Value" : { + "type" : "string", + "description" : "The parameter value.", + "maxLength" : 4096, + "pattern": ".*" + } + }, + "required" : [ "Key", "Value" ], + "additionalProperties": false + } + }, + "properties": { + "Tags" : { + "type" : "array", + "maxItems" : 40, + "description" : "An array of key-value pairs to apply to this resource.", + "items" : { + "$ref" : "#/definitions/Tag" + } + }, + "ProjectArn": { + "$ref" : "#/definitions/ProjectArn" + }, + "ProjectId": { + "$ref" : "#/definitions/ProjectId" + }, + "ProjectName": { + "$ref" : "#/definitions/ProjectName" + }, + "ProjectDescription": { + "$ref" : "#/definitions/ProjectDescription" + }, + "CreationTime": { + "description": "The time at which the project was created.", + "type": "string" + }, + "ServiceCatalogProvisioningDetails" : { + "description" : "Input ServiceCatalog Provisioning Details", + "type" : "object", + "properties" : { + "ProductId" : { + "$ref": "#/definitions/ProductId" + }, + "ProvisioningArtifactId" : { + "$ref": "#/definitions/ProvisioningArtifactId" + }, + "PathId" : { + "$ref": "#/definitions/PathId" + }, + "ProvisioningParameters" : { + "type" : "array", + "description" : "Parameters specified by the administrator that are required for provisioning the product.", + "items" : { + "$ref": "#/definitions/ProvisioningParameter" + } + } + }, + "required" : [ "ProductId", "ProvisioningArtifactId" ], + "additionalProperties": false + }, + "ServiceCatalogProvisionedProductDetails" : { + "description" : "Provisioned ServiceCatalog Details", + "type" : "object", + "properties" : { + "ProvisionedProductId" : { + "$ref": "#/definitions/ProvisioningArtifactId" + }, + "ProvisionedProductStatusMessage" : { + "$ref": "#/definitions/ProvisionedProductStatusMessage" + } + }, + "additionalProperties": false + }, + "ProjectStatus": { + "description": "The status of a project.", + "type": "string", + "enum": [ + "Pending", + "CreateInProgress", + "CreateCompleted", + "CreateFailed", + "DeleteInProgress", + "DeleteFailed", + "DeleteCompleted" + ] + } + }, + "additionalProperties": false, + "required": [ + "ProjectName", + "ServiceCatalogProvisioningDetails" + ], + "readOnlyProperties": [ + "/properties/ProjectArn", + "/properties/CreationTime", + "/properties/ProjectId", + "/properties/ServiceCatalogProvisionedProductDetails", + "/properties/ProjectStatus" + ], + "createOnlyProperties": [ + "/properties/ProjectName", + "/properties/ProjectDescription", + "/properties/ServiceCatalogProvisioningDetails", + "/properties/Tags" + ], + "primaryIdentifier": [ + "/properties/ProjectArn" + ], + "handlers": { + "create": { + "permissions": [ + "sagemaker:CreateProject", + "sagemaker:DescribeProject", + "servicecatalog:DescribeProduct", + "servicecatalog:DescribeProvisioningArtifact", + "servicecatalog:ProvisionProduct", + "servicecatalog:DescribeProvisionedProduct", + "servicecatalog:TerminateProvisionedProduct" + ] + }, + "read": { + "permissions": [ + "sagemaker:DescribeProject", + "sagemaker:ListTags" + ] + }, + "update": { + "permissions": [ + "sagemaker:DescribeProject", + "sagemaker:ListTags", + "sagemaker:AddTags", + "sagemaker:DeleteTags" + ] + }, + "delete": { + "permissions": [ + "sagemaker:DeleteProject" + ] + }, + "list": { + "permissions": [ + "sagemaker:ListProjects" + ] + } + } +} + + diff --git a/aws-sagemaker-project/docs/README.md b/aws-sagemaker-project/docs/README.md new file mode 100644 index 0000000..feadf22 --- /dev/null +++ b/aws-sagemaker-project/docs/README.md @@ -0,0 +1,118 @@ +# AWS::SageMaker::Project + +Resource Type definition for AWS::SageMaker::Project + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SageMaker::Project",
+    "Properties" : {
+        "Tags" : [ Tag, ... ],
+        "ProjectName" : String,
+        "ProjectDescription" : String,
+        "ServiceCatalogProvisioningDetails" : ServiceCatalogProvisioningDetails,
+    }
+}
+
+ +### YAML + +
+Type: AWS::SageMaker::Project
+Properties:
+    Tags: 
+      - Tag
+    ProjectName: String
+    ProjectDescription: String
+    ServiceCatalogProvisioningDetails: ServiceCatalogProvisioningDetails
+
+ +## Properties + +#### 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) + +#### ProjectName + +The name of the project. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 32 + +_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) + +#### ProjectDescription + +The description of the project. + +_Required_: No + +_Type_: String + +_Maximum_: 1024 + +_Pattern_: .* + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ServiceCatalogProvisioningDetails + +Input ServiceCatalog Provisioning Details + +_Required_: Yes + +_Type_: ServiceCatalogProvisioningDetails + +_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 ProjectArn. + +### 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). + +#### ProjectArn + +The Amazon Resource Name (ARN) of the Project. + +#### CreationTime + +The time at which the project was created. + +#### ProjectId + +Project Id. + +#### ServiceCatalogProvisionedProductDetails + +Provisioned ServiceCatalog Details + +#### ProjectStatus + +The status of a project. + diff --git a/aws-sagemaker-project/docs/provisioningparameter.md b/aws-sagemaker-project/docs/provisioningparameter.md new file mode 100644 index 0000000..434391f --- /dev/null +++ b/aws-sagemaker-project/docs/provisioningparameter.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::Project ProvisioningParameter + +Information about a parameter used to provision a product. + +## 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 parameter key. + +_Required_: Yes + +_Type_: String + +_Minimum_: 1 + +_Maximum_: 1000 + +_Pattern_: .* + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Value + +The parameter value. + +_Required_: Yes + +_Type_: String + +_Maximum_: 4096 + +_Pattern_: .* + +_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-project/docs/servicecatalogprovisionedproductdetails.md b/aws-sagemaker-project/docs/servicecatalogprovisionedproductdetails.md new file mode 100644 index 0000000..8929fa4 --- /dev/null +++ b/aws-sagemaker-project/docs/servicecatalogprovisionedproductdetails.md @@ -0,0 +1,50 @@ +# AWS::SageMaker::Project ServiceCatalogProvisionedProductDetails + +Provisioned ServiceCatalog Details + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "ProvisionedProductId" : String,
+    "ProvisionedProductStatusMessage" : String
+}
+
+ +### YAML + +
+ProvisionedProductId: String
+ProvisionedProductStatusMessage: String
+
+ +## Properties + +#### ProvisionedProductId + +The identifier of the provisioning artifact (also known as a version). + +_Required_: No + +_Type_: String + +_Maximum_: 100 + +_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) + +#### ProvisionedProductStatusMessage + +Provisioned Product Status Message + +_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) + diff --git a/aws-sagemaker-project/docs/servicecatalogprovisioningdetails.md b/aws-sagemaker-project/docs/servicecatalogprovisioningdetails.md new file mode 100644 index 0000000..a075215 --- /dev/null +++ b/aws-sagemaker-project/docs/servicecatalogprovisioningdetails.md @@ -0,0 +1,83 @@ +# AWS::SageMaker::Project ServiceCatalogProvisioningDetails + +Input ServiceCatalog Provisioning Details + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "ProductId" : String,
+    "ProvisioningArtifactId" : String,
+    "PathId" : String,
+    "ProvisioningParameters" : [ ProvisioningParameter, ... ]
+}
+
+ +### YAML + +
+ProductId: String
+ProvisioningArtifactId: String
+PathId: String
+ProvisioningParameters: 
+      - ProvisioningParameter
+
+ +## Properties + +#### ProductId + +Service Catalog product identifier. + +_Required_: Yes + +_Type_: String + +_Maximum_: 100 + +_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) + +#### ProvisioningArtifactId + +The identifier of the provisioning artifact (also known as a version). + +_Required_: Yes + +_Type_: String + +_Maximum_: 100 + +_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) + +#### PathId + +The path identifier of the product. + +_Required_: No + +_Type_: String + +_Maximum_: 100 + +_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) + +#### ProvisioningParameters + +Parameters specified by the administrator that are required for provisioning the product. + +_Required_: No + +_Type_: List of ProvisioningParameter + +_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-project/docs/tag.md b/aws-sagemaker-project/docs/tag.md new file mode 100644 index 0000000..9b7936b --- /dev/null +++ b/aws-sagemaker-project/docs/tag.md @@ -0,0 +1,56 @@ +# AWS::SageMaker::Project 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-project/lombok.config b/aws-sagemaker-project/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-sagemaker-project/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-sagemaker-project/pom.xml b/aws-sagemaker-project/pom.xml new file mode 100644 index 0000000..3f573dd --- /dev/null +++ b/aws-sagemaker-project/pom.xml @@ -0,0 +1,215 @@ + + + 4.0.0 + + software.amazon.sagemaker.project + aws-sagemaker-project-handler + aws-sagemaker-project-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + com.amazonaws + aws-java-sdk-ec2 + 1.11.606 + + + + software.amazon.awssdk + sagemaker + 2.15.41 + + + + 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.5 + + + INSTRUCTION + COVEREDRATIO + 0.7 + + + + + + + + + + + + ${project.basedir} + + aws-sagemaker-project.json + + + + + diff --git a/aws-sagemaker-project/resource-role.yaml b/aws-sagemaker-project/resource-role.yaml new file mode 100644 index 0000000..8c3b75d --- /dev/null +++ b/aws-sagemaker-project/resource-role.yaml @@ -0,0 +1,42 @@ +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: + - "sagemaker:AddTags" + - "sagemaker:CreateProject" + - "sagemaker:DeleteProject" + - "sagemaker:DeleteTags" + - "sagemaker:DescribeProject" + - "sagemaker:ListProjects" + - "sagemaker:ListTags" + - "servicecatalog:DescribeProduct" + - "servicecatalog:DescribeProvisionedProduct" + - "servicecatalog:DescribeProvisioningArtifact" + - "servicecatalog:ProvisionProduct" + - "servicecatalog:TerminateProvisionedProduct" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn \ No newline at end of file diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/BaseHandlerStd.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/BaseHandlerStd.java new file mode 100644 index 0000000..8c0da59 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/BaseHandlerStd.java @@ -0,0 +1,35 @@ +package software.amazon.sagemaker.project; + +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); +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CallbackContext.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CallbackContext.java new file mode 100644 index 0000000..4aaf6e6 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CallbackContext.java @@ -0,0 +1,10 @@ +package software.amazon.sagemaker.project; + +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-project/src/main/java/software/amazon/sagemaker/project/ClientBuilder.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ClientBuilder.java new file mode 100644 index 0000000..75691f6 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ClientBuilder.java @@ -0,0 +1,12 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.services.sagemaker.SageMakerClient; + +/** + * Provides APIs to build service client. + */ +public class ClientBuilder { + public static SageMakerClient getClient() { + return SageMakerClient.builder().build(); + } +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Configuration.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Configuration.java new file mode 100644 index 0000000..c4c8e99 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Configuration.java @@ -0,0 +1,8 @@ +package software.amazon.sagemaker.project; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-sagemaker-project.json"); + } +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CreateHandler.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CreateHandler.java new file mode 100644 index 0000000..5f3a4ff --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/CreateHandler.java @@ -0,0 +1,117 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; + +import software.amazon.awssdk.services.sagemaker.model.CreateProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; + +import software.amazon.cloudformation.Action; + +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +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; + +public class CreateHandler extends BaseHandlerStd { + + private Logger logger; + + @Override + 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("AWS-SageMaker-Project::Create", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToCreateRequest) + .makeServiceCall(this::createProject) + .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 createRequest the aws service request to create a project + * @param proxyClient the aws service client to make the call + * @return awsResponse create project resource response + */ + private CreateProjectResponse createProject( + final CreateProjectRequest createRequest, + final ProxyClient proxyClient) { + CreateProjectResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2( + createRequest, proxyClient.client()::createProject); + } catch (final ResourceInUseException e) { + throw new ResourceAlreadyExistsException(ResourceModel.TYPE_NAME, createRequest.projectName()); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.CREATE.toString(), ResourceModel.TYPE_NAME, createRequest.projectName(), e); + } + return response; + } + + /** + * This is used to ensure project resource has moved from Pending to Scheduled/Failed state. + * @param createRequest the aws service request to create a project + * @param createResponse the aws service response on creating a project + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return boolean state of stabilized or not + */ + private boolean stabilizedOnCreate( + final CreateProjectRequest createRequest, + final CreateProjectResponse createResponse, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + + if(model.getProjectArn() == null){ + model.setProjectArn(createResponse.projectArn()); + } + + final DescribeProjectResponse response = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToReadRequest(model), + proxyClient.client()::describeProject); + + final ProjectStatus projectStatus = response.projectStatus(); + + switch (projectStatus) { + case CREATE_COMPLETED: + case CREATE_FAILED: + case DELETE_FAILED: + logger.log(String.format("%s [%s] has been stabilized with status %s.", ResourceModel.TYPE_NAME, + model.getPrimaryIdentifier(), projectStatus)); + return true; + + case PENDING: + case CREATE_IN_PROGRESS: + case DELETE_IN_PROGRESS: + logger.log(String.format("%s [%s] is stabilizing.", ResourceModel.TYPE_NAME, model.getPrimaryIdentifier())); + return false; + default: + throw new CfnGeneralServiceException( + "Stabilizing of " + model.getPrimaryIdentifier() + + " failed with unexpected status " + projectStatus); + } + } + + + +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/DeleteHandler.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/DeleteHandler.java new file mode 100644 index 0000000..6218eed --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/DeleteHandler.java @@ -0,0 +1,153 @@ +package software.amazon.sagemaker.project; + +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.DeleteProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.DeleteProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +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.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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> + proxy.initiate("AWS-SageMaker-Project::Delete", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToDeleteRequest) + .makeServiceCall(this::deleteResource) + .stabilize(this::stabilizedOnDelete) + .done(this::checkAndReturnDeleteStatus)); + + } + + private ProgressEvent checkAndReturnDeleteStatus( + final DeleteProjectRequest deleteProjectRequest, + final DeleteProjectResponse deleteProjectResponse, + final ProxyClient proxyClient, + final ResourceModel model, final CallbackContext callbackContext) { + + OperationStatus delStatus = OperationStatus.SUCCESS; + try { + + DescribeProjectResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToReadRequest(model), + proxyClient.client()::describeProject); + delStatus = response.projectStatus() == ProjectStatus.DELETE_COMPLETED ? + OperationStatus.SUCCESS : OperationStatus.FAILED; + logger.log(String.format("%s Project status post stabilizing: [%s].", model.getProjectName(), + delStatus.toString())); + + } catch (final ResourceNotFoundException e) { + } catch (final SageMakerException e) { + if (false == isExceptionFromDeletedProject(e)) { + throw e; + } + } + return ProgressEvent.builder() + .status(delStatus) + .build(); + } + + /** + * Implement client invocation of the delete request through the proxyClient. + * @param deleteProjectRequest the aws service request to delete a project + * @param proxyClient the aws service client to make the call + * @return delete project response + */ + private DeleteProjectResponse deleteResource( + final DeleteProjectRequest deleteProjectRequest, + final ProxyClient proxyClient) { + + DeleteProjectResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(deleteProjectRequest, proxyClient.client()::deleteProject); + } 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 + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, deleteProjectRequest.projectName()); + } catch (final AwsServiceException e) { + if (isExceptionFromDeletedProject(e)) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, deleteProjectRequest.projectName()); + } + ExceptionMapper.throwCfnException(Action.DELETE.toString(), ResourceModel.TYPE_NAME, deleteProjectRequest.projectName(), e); + } + + return response; + } + + /** + * Sync delete API moves resource in PENDING state and actual deletion happens asynchronously. + * Stabilization is required to ensure project resource deletion has been completed. + * @param deleteProjectRequest the aws service request to delete a project + * @param deleteProjectResult the aws service response on deleting a project + * @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 DeleteProjectRequest deleteProjectRequest, + final DeleteProjectResponse deleteProjectResult, + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + DescribeProjectResponse response = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToReadRequest(model), + proxyClient.client()::describeProject); + final ProjectStatus projectStatus = response.projectStatus(); + + switch (projectStatus) { + case DELETE_IN_PROGRESS: + case PENDING: + logger.log(String.format("%s with name [%s] is stabilizing while delete.", ResourceModel.TYPE_NAME, model.getProjectName())); + return false; + //Delete failure case + case DELETE_FAILED: + case DELETE_COMPLETED: + logger.log(String.format("%s with name [%s] is stabilizing while delete.", ResourceModel.TYPE_NAME, model.getProjectName())); + return true; + default: + throw new CfnGeneralServiceException("Delete stabilizing of project: " + model.getProjectName()); + } + } catch (final ResourceNotFoundException e) { + return true; + } catch (final SageMakerException e) { + if (isExceptionFromDeletedProject(e)) { + return true; + } + throw e; + } + } + + private boolean isExceptionFromDeletedProject(final AwsServiceException e) { + if (StringUtils.isNotBlank(e.getMessage()) + && (e.getMessage().matches(".*Project .* does not exist.*") + || e.getMessage().matches(".*Project.*in DeleteCompleted status.*"))) { + return true; + } + return false; + } + +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ExceptionMapper.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ExceptionMapper.java new file mode 100644 index 0000000..ae8a429 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ExceptionMapper.java @@ -0,0 +1,62 @@ +package software.amazon.sagemaker.project; + +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 software.amazon.cloudformation.exceptions.ResourceAlreadyExistsException; + +public class ExceptionMapper { + /** + * Throws Cfn exception corresponding to error code of the given exception. + * + * @param operation + * @param e exception + */ + static void throwCfnException(final String operation, 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(operation, 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); + } + + static void throwCfnException(final String operation, final String resourceType, final String resourceName, final AwsServiceException e) { + if (StringUtils.isNotBlank(e.getMessage()) + && (e.getMessage().matches(".*Cannot find Project:.*") + || e.getMessage().matches(".*Project .* does not exist.*"))) { + throw new CfnNotFoundException(resourceType, resourceName, e); + } + if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().matches(".*Project already exists.*")) { + throw new ResourceAlreadyExistsException(resourceType, resourceName, e); + } + throwCfnException(operation, e); + } +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ListHandler.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ListHandler.java new file mode 100644 index 0000000..8fdca92 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ListHandler.java @@ -0,0 +1,73 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.ListProjectsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListProjectsResponse; +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 Logger logger; + + @Override + public 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("AWS-SageMaker-Project::List", proxyClient, model, callbackContext) + .translateToServiceRequest(resourceModel -> Translator.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 listProjectsRequest the aws service request to list projects + * @param proxyClient the aws service client to make the call + * @return list project response + */ + private ListProjectsResponse listResources( + final ListProjectsRequest listProjectsRequest, + final ProxyClient proxyClient) { + + ListProjectsResponse listProjectsResponse = null; + try { + listProjectsResponse = proxyClient.injectCredentialsAndInvokeV2(listProjectsRequest, + proxyClient.client()::listProjects); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.LIST.toString(), e); + } + + return listProjectsResponse; + } + + /** + * Build the Progress Event object from the SageMaker ListProjectsResult. + * @param listResponse the aws service list resource response + * @return progressEvent indicating success, in progress with delay callback or failed state + */ + private ProgressEvent constructResourceModelFromResponse( + final ListProjectsResponse listResponse) { + return ProgressEvent.builder() + .nextToken(listResponse.nextToken()) + .resourceModels(Translator.translateFromListResponse(listResponse)) + .status(OperationStatus.SUCCESS) + .build(); + } + +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ReadHandler.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ReadHandler.java new file mode 100644 index 0000000..fd19c10 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/ReadHandler.java @@ -0,0 +1,89 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +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; + +import java.util.List; + +public class ReadHandler extends BaseHandlerStd { + + private Logger logger; + private ProxyClient proxyClient; + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + this.logger = logger; + this.proxyClient = proxyClient; + + final ResourceModel model = request.getDesiredResourceState(); + + return proxy.initiate("AWS-SageMaker-Project::Read", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest)) + .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 describeProjectRequest the aws service request to describe project + * @return describe project response + */ + private DescribeProjectResponse readResource( + final DescribeProjectRequest describeProjectRequest) { + DescribeProjectResponse response = null; + try { + response = proxyClient.injectCredentialsAndInvokeV2(describeProjectRequest, proxyClient.client()::describeProject); + if (response.projectStatus().equals(ProjectStatus.DELETE_COMPLETED)) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, describeProjectRequest.projectName()); + } + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, describeProjectRequest.projectName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.READ.toString(), ResourceModel.TYPE_NAME, describeProjectRequest.projectName(), e); + } + return response; + } + + /** + * Construction of resource model from the read response, add tags to model + * + * @param awsResponse the aws service describe project response + * @return progressEvent indicating success, in progress with delay callback or failed state + */ + private ProgressEvent constructResourceModelFromResponse( + final DescribeProjectResponse awsResponse) { + ResourceModel model = Translator.translateFromReadResponse(awsResponse); + addTagsToModel(model); + return ProgressEvent.defaultSuccessHandler(model); + } + + /** + * Add tags to the model after fetching it + * + * @param model the resource model to which tags have to be added + */ + private void addTagsToModel(ResourceModel model) { + ListTagsResponse tagsResponse = proxyClient.injectCredentialsAndInvokeV2(Translator.translateToListTagsRequest(model), proxyClient.client()::listTags); + List tags = Translator.sdkTagsToCfnTags(tagsResponse.tags()); + model.setTags(tags); + } +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Translator.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Translator.java new file mode 100644 index 0000000..afb58af --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/Translator.java @@ -0,0 +1,282 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.services.sagemaker.model.AddTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ListProjectsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListProjectsResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; + +public class Translator { + + /** + * Request to create a resource + * + * @param model resource model + * @return awsRequest the aws service request to create a resource + */ + static CreateProjectRequest translateToCreateRequest(final ResourceModel model) { + List tags = cfnTagsToSdkTags(model.getTags()); + + CreateProjectRequest.Builder builder = CreateProjectRequest.builder() + .projectName(model.getProjectName()) + .projectDescription(model.getProjectDescription()) + .serviceCatalogProvisioningDetails(translate(model.getServiceCatalogProvisioningDetails())) + .tags(tags); + + //optional fields + Optional.ofNullable(model.getProjectDescription()).ifPresent( + inp -> builder.projectDescription(inp) + ); + + return builder.build(); + } + + /** + * Request to read a resource + * + * @param model resource model + * @return awsRequest the aws service request to describe a resource + */ + static DescribeProjectRequest translateToReadRequest(final ResourceModel model) { + + return DescribeProjectRequest.builder() + .projectName(model.getProjectName()) + .build(); + } + + /** + * Translates resource object from sdk into a resource model + * + * @param describeProjectResponse the aws service describe resource response + * @return model resource model + */ + static ResourceModel translateFromReadResponse(final DescribeProjectResponse describeProjectResponse) { + + return ResourceModel.builder() + .projectArn(describeProjectResponse.projectArn()) + .projectName(describeProjectResponse.projectName()) + .projectStatus(describeProjectResponse.projectStatus().toString()) + .projectDescription(describeProjectResponse.projectDescription()) + .serviceCatalogProvisioningDetails(translate(describeProjectResponse.serviceCatalogProvisioningDetails())) + .serviceCatalogProvisionedProductDetails(translate(describeProjectResponse.serviceCatalogProvisionedProductDetails())) + .creationTime(describeProjectResponse.creationTime().toString()) + .build(); + } + + private static ServiceCatalogProvisionedProductDetails translate( + software.amazon.awssdk.services.sagemaker.model.ServiceCatalogProvisionedProductDetails provisionedProductDetails) { + return provisionedProductDetails != null ? ServiceCatalogProvisionedProductDetails.builder() + .provisionedProductId(provisionedProductDetails.provisionedProductId()) + .provisionedProductStatusMessage(provisionedProductDetails.provisionedProductStatusMessage()) + .build() : null; + } + + private static ServiceCatalogProvisioningDetails translate( + software.amazon.awssdk.services.sagemaker.model.ServiceCatalogProvisioningDetails + provisioningDetails) { + return provisioningDetails != null ? ServiceCatalogProvisioningDetails.builder() + .pathId(provisioningDetails.pathId()) + .productId(provisioningDetails.productId()) + .provisioningArtifactId(provisioningDetails.provisioningArtifactId()) + .provisioningParameters(translateFrom(provisioningDetails.provisioningParameters())) + .build() : null; + } + + private static List translateFrom( + List provisioningParameters) { + return provisioningParameters != null && isNotEmpty(provisioningParameters) ? + provisioningParameters.stream() + .map(e -> ProvisioningParameter.builder() + .key(e.key()) + .value(e.value()) + .build()) + .collect(Collectors.toList()) : null; + } + + /** + * Request to delete a resource + * + * @param model resource model + * @return awsRequest the aws service request to delete a resource + */ + static DeleteProjectRequest translateToDeleteRequest(final ResourceModel model) { + return DeleteProjectRequest.builder() + .projectName(model.getProjectName()) + .build(); + } + + /** + * Request to list resources + * + * @param nextToken token passed to the aws service list resources request + * @return awsRequest the aws service request to list resources within aws account + */ + static ListProjectsRequest translateToListRequest(final String nextToken) { + return ListProjectsRequest.builder() + .nextToken(nextToken).build(); + } + + /** + * Translates resource objects from sdk into a resource model (primary identifier only) + * + * @param awsResponse the aws service describe resource response + * @return list of resource models + */ + static List translateFromListResponse(final ListProjectsResponse awsResponse) { + + return Translator.streamOfOrEmpty(awsResponse.projectSummaryList()) + .map(summary -> ResourceModel.builder() + .creationTime(summary.creationTime().toString()) + .projectArn(summary.projectArn()) + .projectName(summary.projectName()) + .projectStatus(summary.projectStatus().toString()) + .projectDescription(summary.projectDescription()) + .build()) + .filter(summary -> { + return false == summary.getProjectStatus().equals(ProjectStatus.DELETE_COMPLETED.toString()); + }) + .collect(Collectors.toList()); + + } + + private static Stream streamOfOrEmpty(final Collection collection) { + return Optional.ofNullable(collection) + .map(Collection::stream) + .orElseGet(Stream::empty); + } + + /** + * Converts input object to a Sagemaker SDK object. + * @param input ServiceCatalogProvisioningDetails + * @return Sagemaker ServiceCatalogProvisioningDetails object. + */ + static software.amazon.awssdk.services.sagemaker.model.ServiceCatalogProvisioningDetails translate( + final ServiceCatalogProvisioningDetails input) { + return input == null ? null : software.amazon.awssdk.services.sagemaker.model.ServiceCatalogProvisioningDetails.builder() + .pathId(input.getPathId()) + .productId(input.getProductId()) + .provisioningArtifactId(input.getProvisioningArtifactId()) + .provisioningParameters(translate(input.getProvisioningParameters())) + .build(); + } + + /** + * Converts input object to a Sagemaker SDK object. + * @param input ServiceCatalogProvisioningDetails + * @return Sagemaker ServiceCatalogProvisioningDetails object. + */ + static List translate( + final List input) { + return input != null && isNotEmpty(input) ? input.stream() + .map(e -> software.amazon.awssdk.services.sagemaker.model.ProvisioningParameter.builder() + .key(e.getKey()) + .value(e.getValue()) + .build()) + .collect(Collectors.toList()) : null; + } + + static List cfnTagsToSdkTags(final List tags) { + if (tags == null) { + return new ArrayList<>(); + } + for (final software.amazon.sagemaker.project.Tag tag : tags) { + if (tag.getKey() == null) { + throw new CfnInvalidRequestException("Tags cannot have a null key"); + } + if (tag.getValue() == null) { + throw new CfnInvalidRequestException("Tags cannot have a null value"); + } + } + return tags.stream() + .map(e -> Tag.builder() + .key(e.getKey()) + .value(e.getValue()) + .build()) + .collect(Collectors.toList()); + } + + /** + * Translates the model to request to list tags of project + * @param model resource model + * @return awsRequest the aws service to list tags of project + */ + static ListTagsRequest translateToListTagsRequest(final ResourceModel model) { + return ListTagsRequest.builder() + .resourceArn(model.getProjectArn()) + .build(); + } + + /** + * Translates tag objects from resource model into list of tag objects of sdk + * @param tags, sdk tags got from the aws service response + * @return list of resource model tags + */ + static List sdkTagsToCfnTags(final List tags) { + if (tags == null || tags.isEmpty()) { + return null; + } + final List cfnTags = + tags.stream() + .map(e -> software.amazon.sagemaker.project.Tag.builder() + .key(e.key()) + .value(e.value()) + .build()) + .collect(Collectors.toList()); + return cfnTags; + } + + /** + * Construct add tags request from list of tags and project arn + * @param tagsToAdd, list of tags to be added to the project + * @param arn, arn of the project to which tags have to be added. + * @return awsRequest the aws service request to add tags to project + */ + static AddTagsRequest translateToAddTagsRequest(final List tagsToAdd, String arn) { + return AddTagsRequest.builder() + .resourceArn(arn) + .tags(tagsToAdd) + .build(); + } + + /** + * Construct delete tags request from list of tags and project arn + * @param tagsToDelete, list of tags to be deleted from the project + * @param arn, arn of the project from which tags have to be deleted. + * @return awsRequest the aws service request to add tags to project + */ + static DeleteTagsRequest translateToDeleteTagsRequest(final List tagsToDelete, String arn) { + return DeleteTagsRequest.builder() + .resourceArn(arn) + .tagKeys(tagsToDelete) + .build(); + } + + /** + * Validate required fields does not differ + * @param model resource model + * @param response describe response + */ + static boolean compareRequiredFields(final ResourceModel model, final DescribeProjectResponse response) { + return model.getProjectDescription() == response.projectDescription() + || model.getServiceCatalogProvisioningDetails().getProductId() + == response.serviceCatalogProvisioningDetails().productId() + || model.getServiceCatalogProvisioningDetails().getProvisioningArtifactId() + == response.serviceCatalogProvisioningDetails().provisioningArtifactId(); + } +} diff --git a/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/UpdateHandler.java b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/UpdateHandler.java new file mode 100644 index 0000000..f204928 --- /dev/null +++ b/aws-sagemaker-project/src/main/java/software/amazon/sagemaker/project/UpdateHandler.java @@ -0,0 +1,120 @@ +package software.amazon.sagemaker.project; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sagemaker.SageMakerClient; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.Tag; +import software.amazon.cloudformation.Action; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +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; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class UpdateHandler 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(); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> addProjectArnIfNotAvailable(proxyClient, model, callbackContext)) + .then(progress -> updateTags(proxyClient, model, callbackContext)) + .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } + + /** + * Adding the project arn, if not available in the model + * and validate input fields are not modified + * + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent addProjectArnIfNotAvailable( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + if (model.getProjectArn() == null) { + DescribeProjectResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToReadRequest(model), proxyClient.client()::describeProject); + + model.setProjectArn(response.projectArn()); + + // validate no changes to input fields + if (false == Translator.compareRequiredFields(model, response)) { + throw new CfnInvalidRequestException("Update not supported"); + } + } + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getProjectName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, model.getProjectName(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + + /** + * Client invocation of the update tags request through the proxyClient, which is already initialised with + * caller credentials, region and retry settings + * @param proxyClient the aws service client to make the call + * @param model the resource model + * @param callbackContext the call back context + * @return progressEvent, in progress with delay callback and model state + */ + private ProgressEvent updateTags( + final ProxyClient proxyClient, + final ResourceModel model, + final CallbackContext callbackContext) { + try { + handleTagging(proxyClient, model); + } catch (ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getProjectName(), e); + } catch (final AwsServiceException e) { + ExceptionMapper.throwCfnException(Action.UPDATE.toString(), ResourceModel.TYPE_NAME, model.getProjectName(), e); + } + return ProgressEvent.progress(model, callbackContext); + } + + /** + * Validate no tag difference between existing model and updated model + * @param proxyClient the aws service client to make the call + * @param model the resource model + */ + private void handleTagging(final ProxyClient proxyClient, + final ResourceModel model) { + final Set newTags = new HashSet<>(Translator.cfnTagsToSdkTags(model.getTags())); + final Set existingTags + = new HashSet<>(proxyClient.injectCredentialsAndInvokeV2( + Translator.translateToListTagsRequest(model), proxyClient.client()::listTags).tags()); + + final List tagsToRemove = existingTags.stream() + .filter(tag -> !newTags.contains(tag)) + .map(tag -> tag.key()) + .collect(Collectors.toList()); + final List tagsToAdd = newTags.stream() + .filter(tag -> !existingTags.contains(tag)) + .collect(Collectors.toList()); + if (false == (tagsToRemove.isEmpty() && tagsToAdd.isEmpty())) { + throw new CfnInvalidRequestException("Tag update not supported"); + } + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/AbstractTestBase.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/AbstractTestBase.java new file mode 100644 index 0000000..fe11a32 --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/AbstractTestBase.java @@ -0,0 +1,90 @@ +package software.amazon.sagemaker.project; + +import com.google.common.collect.ImmutableList; +import org.json.JSONObject; +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.awssdk.services.sagemaker.model.Tag; +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.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class AbstractTestBase { + protected static final Instant TEST_CREATION_TIME = Instant.now(); + protected static final String TEST_PROJECT_ARN = "testProjectArn"; + protected static final String TEST_PROJECT_NAME = "testProjectName"; + protected static final String TEST_PRODUCT_ID = "product_id"; + protected static final String TEST_PATH_ID = "path_id"; + protected static final String TEST_PROVISIONING_ARTIFACT_ID = "artifact_id"; + protected static final String TEST_ERROR_MESSAGE = "test error message"; + + protected static final String PROJECT_ALREADY_EXISTS_ERROR_MESSAGE = "Project already exists: sample_arn"; + protected static final String PROJECT_NOT_EXISTS_ERROR_MESSAGE = "Project sample_arn does not exist."; + protected static final String CANNOT_FIND_PROJECT_ERROR_MESSAGE = "Cannot find Project: sample_arn"; + protected static final List TEST_SDK_TAGS = ImmutableList.of(Tag.builder().key("key1").value("value1").build()); + protected static final List TEST_CFN_MODEL_TAGS + = ImmutableList.of(software.amazon.sagemaker.project.Tag.builder() + .key("key1").value("value1").build()); + 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; + } + }; + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/CreateHandlerTest.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/CreateHandlerTest.java new file mode 100644 index 0000000..b46747d --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/CreateHandlerTest.java @@ -0,0 +1,420 @@ +package software.amazon.sagemaker.project; + +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.CreateProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.CreateProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +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 java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +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 { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + SageMakerClient sdkClient; + + private final ResourceModel requestModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .serviceCatalogProvisioningDetails( + ServiceCatalogProvisioningDetails.builder() + .productId(TEST_PRODUCT_ID) + .pathId(TEST_PATH_ID) + .provisioningArtifactId(TEST_PROVISIONING_ARTIFACT_ID) + .provisioningParameters(Arrays.asList( + ProvisioningParameter.builder() + .key("key1") + .value("value1") + .build())) + .build()) + .build(); + + private final ResourceModel requestModelWithTags = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .serviceCatalogProvisioningDetails( + ServiceCatalogProvisioningDetails.builder() + .productId(TEST_PRODUCT_ID) + .pathId(TEST_PATH_ID) + .provisioningArtifactId(TEST_PROVISIONING_ARTIFACT_ID) + .build()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + + @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 DescribeProjectResponse describeProjectResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(describeProjectResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final CreateProjectResponse createProjectResponse = CreateProjectResponse.builder() + .projectArn(TEST_PROJECT_ARN) + .build(); + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenReturn(createProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_withTags() { + final DescribeProjectResponse describeProjectResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(describeProjectResponse); + + final CreateProjectResponse createProjectResponse = CreateProjectResponse.builder() + .projectArn(TEST_PROJECT_ARN) + .build(); + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenReturn(createProjectResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModelWithTags) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(500) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(serviceInternalException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_ProjectAlreadyExists_Fails() { + final ResourceInUseException resourceExistexception = ResourceInUseException.builder() + .message(PROJECT_ALREADY_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(resourceExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.AlreadyExists.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + @Test + public void testCreateHandler_ResourceInUseException_Fails() { + final ResourceInUseException resourceInUseException = ResourceInUseException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(resourceInUseException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( ResourceAlreadyExistsException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.AlreadyExists.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + @Test + public void testCreateHandler_ResourceLimitExceededException() { + final ResourceLimitExceededException resourceLimitExceededException = ResourceLimitExceededException.builder() + .message(TEST_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(resourceLimitExceededException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_ValidationFailure() { + final AwsServiceException validationFailureException = SageMakerException.builder() + .message("1 validation error detected: Value null at 'ProjectName' " + + "failed to satisfy constraint: Member must not be null") + .statusCode(400) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(validationFailureException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnInvalidRequestException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.InvalidRequest.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_NoExceptionMessage() { + final AwsServiceException someException = SageMakerException.builder() + .statusCode(400) + .build(); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenThrow(someException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.CREATE), exception.getMessage()); + } + + @Test + public void testCreateHandler_VerifyStabilization_CompletedStatus() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.PENDING) + .build(); + + final DescribeProjectResponse secondDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + + final CreateProjectResponse createProjectResponse = CreateProjectResponse.builder() + .projectArn(TEST_PROJECT_ARN) + .build(); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse).thenReturn(listTagsResponse); + + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenReturn(createProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testCreateHandler_VerifyStabilization_FailedStatus() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_IN_PROGRESS) + .build(); + + final DescribeProjectResponse secondDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_FAILED) + .build(); + + final CreateProjectResponse createProjectResponse = CreateProjectResponse.builder() + .projectArn(TEST_PROJECT_ARN) + .build(); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse).thenReturn(secondDescribeResponse); + when(proxyClient.client().createProject(any(CreateProjectRequest.class))) + .thenReturn(createProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(requestModel) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus("DeleteFailed") + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.project.CreateHandler handler = new software.amazon.sagemaker.project.CreateHandler(); + return handler.handleRequest(proxy, request, new software.amazon.sagemaker.project.CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/DeleteHandlerTest.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/DeleteHandlerTest.java new file mode 100644 index 0000000..0968e39 --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/DeleteHandlerTest.java @@ -0,0 +1,299 @@ +package software.amazon.sagemaker.project; + +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.DeleteProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DeleteProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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 DeleteProjectResponse deleteProjectResponse = DeleteProjectResponse.builder() + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenThrow(ResourceNotFoundException.class) + .thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenReturn(deleteProjectResponse); + + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + assertNull(response.getResourceModel()); + } + + @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().deleteProject(any(DeleteProjectRequest.class))) + .thenThrow(serviceInternalException); + + Exception exception = assertThrows(CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.DELETE), exception.getMessage()); + } + + @Test + public void testDeleteHandler_ProjectDoesNotExists_Fails() { + final ResourceNotFoundException resourceNotExistexception = ResourceNotFoundException.builder() + .message(PROJECT_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenThrow(resourceNotExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + @Test + public void testDeleteHandler_ResourceNotFoundException_Fails() { + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete_WithResourceNotFoundException() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_IN_PROGRESS) + .build(); + + final DeleteProjectResponse deleteProjectResponse = DeleteProjectResponse.builder() + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse) + .thenThrow(ResourceNotFoundException.class) + .thenThrow(ResourceNotFoundException.class); + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenReturn(deleteProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete_WithProjectNotExist() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_IN_PROGRESS) + .build(); + + final AwsServiceException resourceNotExistexception = SageMakerException.builder() + .message(PROJECT_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + final DeleteProjectResponse deleteProjectResponse = DeleteProjectResponse.builder() + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse) + .thenThrow(resourceNotExistexception) + .thenThrow(resourceNotExistexception); + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenReturn(deleteProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_SuccessfulDelete_WithDeleteCompleted() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_IN_PROGRESS) + .build(); + + final DescribeProjectResponse secondDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_COMPLETED) + .build(); + + final DeleteProjectResponse deleteProjectResponse = DeleteProjectResponse.builder() + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse) + .thenReturn(secondDescribeResponse) + .thenReturn(secondDescribeResponse); + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenReturn(deleteProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testDeleteHandler_VerifyStabilization_ResourceNotDeleted() { + final DescribeProjectResponse firstDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_IN_PROGRESS) + .build(); + + final DescribeProjectResponse secondDescribeResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.DELETE_FAILED) + .build(); + + final DeleteProjectResponse deleteProjectResponse = DeleteProjectResponse.builder() + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(firstDescribeResponse) + .thenReturn(secondDescribeResponse) + .thenReturn(secondDescribeResponse); + when(proxyClient.client().deleteProject(any(DeleteProjectRequest.class))) + .thenReturn(deleteProjectResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.FAILED, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .projectName(TEST_PROJECT_NAME) + .build(); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.project.DeleteHandler handler = new DeleteHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ListHandlerTest.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ListHandlerTest.java new file mode 100644 index 0000000..5072e3c --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ListHandlerTest.java @@ -0,0 +1,154 @@ +package software.amazon.sagemaker.project; + +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.ListProjectsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListProjectsResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +import software.amazon.awssdk.services.sagemaker.model.ProjectSummary; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.cloudformation.Action; +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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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 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 ProjectSummary projectSummary = ProjectSummary.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + + final ListProjectsResponse listProjectsResponse = + ListProjectsResponse.builder() + .projectSummaryList(projectSummary) + .nextToken(TEST_TOKEN) + .build(); + + when(proxyClient.client().listProjects(any(ListProjectsRequest.class))) + .thenReturn(listProjectsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + + List expectedModels = new ArrayList(); + expectedModels.add(expectedResourceModel); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getResourceModel()); + assertEquals(expectedModels, response.getResourceModels()); + assertEquals(TEST_TOKEN, response.getNextToken()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testListHandler_SimpleSuccess_NoProjectExist() { + final ListProjectsResponse listProjectsResponse = + ListProjectsResponse.builder() + .projectSummaryList(Collections.emptyList()) + .nextToken(null) + .build(); + + when(proxyClient.client().listProjects(any(ListProjectsRequest.class))) + .thenReturn(listProjectsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertNull(response.getResourceModel()); + assertEquals(Collections.emptyList(), response.getResourceModels()); + assertNull(response.getNextToken()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testListHandler_ServiceInternalException() { + AwsErrorDetails errorDetails = AwsErrorDetails.builder().errorCode("InternalError").build(); + AwsServiceException ex = SageMakerException.builder().awsErrorDetails(errorDetails).build(); + + when(proxyClient.client().listProjects(any(ListProjectsRequest.class))) + .thenThrow(ex); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnServiceInternalErrorException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.ServiceInternalError.getMessage(), + Action.LIST.toString()), exception.getMessage()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.project.ListHandler handler = new ListHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder().build(); + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ReadHandlerTest.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ReadHandlerTest.java new file mode 100644 index 0000000..9c111f1 --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/ReadHandlerTest.java @@ -0,0 +1,205 @@ +package software.amazon.sagemaker.project; + +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.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +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.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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; + +@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() { + final DescribeProjectResponse describeProjectResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(describeProjectResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .tags(TEST_CFN_MODEL_TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeProject(any(DescribeProjectRequest.class)); + } + + @Test + public void testReadHandler_WithEmptyTags() { + final DescribeProjectResponse describeProjectResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(describeProjectResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceModel expectedResourceModel = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedResourceModel, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + verify(proxyClient.client()).describeProject(any(DescribeProjectRequest.class)); + } + + @Test + public void testReadHandler_ServiceInternalException() { + final AwsServiceException serviceInternalException = SageMakerException.builder() + .message("test error message") + .statusCode(500) + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenThrow(serviceInternalException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows( CfnGeneralServiceException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.GeneralServiceException.getMessage(), + Action.READ), exception.getMessage()); + } + + @Test + public void testReadHandler_ProjectNotExist_Fails() { + final ResourceNotFoundException resourceNotExistexception = ResourceNotFoundException.builder() + .message(PROJECT_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenThrow(resourceNotExistexception); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + + @Test + public void testReadHandler_ResourceNotFoundException() { + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModel()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.project.ReadHandler handler = new ReadHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .projectName(TEST_PROJECT_NAME) + .build(); + } +} diff --git a/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/UpdateHandlerTest.java b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/UpdateHandlerTest.java new file mode 100644 index 0000000..d0a1566 --- /dev/null +++ b/aws-sagemaker-project/src/test/java/software/amazon/sagemaker/project/UpdateHandlerTest.java @@ -0,0 +1,206 @@ +package software.amazon.sagemaker.project; + +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.DescribeProjectRequest; +import software.amazon.awssdk.services.sagemaker.model.DescribeProjectResponse; +import software.amazon.awssdk.services.sagemaker.model.ListTagsRequest; +import software.amazon.awssdk.services.sagemaker.model.ListTagsResponse; +import software.amazon.awssdk.services.sagemaker.model.ProjectStatus; +import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sagemaker.model.SageMakerException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +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.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +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) +class UpdateHandlerTest 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 testUpdateHandler_SimpleSuccess() { + final DescribeProjectResponse describeProjectResponse = + DescribeProjectResponse.builder() + .creationTime(TEST_CREATION_TIME) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED) + .build(); + when(proxyClient.client().describeProject(any(DescribeProjectRequest.class))) + .thenReturn(describeProjectResponse); + + final ListTagsResponse listTagsResponse = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithoutTags()) + .build(); + final ProgressEvent response = invokeHandleRequest(request); + + ResourceModel expectedModelFromResponse = ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectArn(TEST_PROJECT_ARN) + .projectName(TEST_PROJECT_NAME) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + + assertNotNull(response); + assertEquals(OperationStatus.SUCCESS, response.getStatus()); + assertEquals(0, response.getCallbackDelaySeconds()); + assertEquals(expectedModelFromResponse, response.getResourceModel()); + assertNull(response.getMessage()); + assertNull(response.getErrorCode()); + } + + @Test + public void testUpdateHandler_SimpleSuccess_AddTags() { + + final ListTagsResponse listTagsResponseWithTags = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + final ListTagsResponse listTagsResponseWithoutTags = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponseWithoutTags).thenReturn(listTagsResponseWithTags); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithTags()) + .build(); + + assertThrows( CfnInvalidRequestException.class, () -> invokeHandleRequest(request)); + } + + @Test + public void testUpdateHandler_SimpleSuccess_DeleteTags() { + + final ListTagsResponse listTagsResponseWithTags = + ListTagsResponse.builder() + .tags(TEST_SDK_TAGS) + .build(); + final ListTagsResponse listTagsResponseWithoutTags = + ListTagsResponse.builder() + .tags(new ArrayList<>()) + .build(); + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenReturn(listTagsResponseWithTags).thenReturn(listTagsResponseWithoutTags); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithoutTags()) + .build(); + + assertThrows( CfnInvalidRequestException.class, () -> invokeHandleRequest(request)); + } + + @Test + public void testUpdateHandler_ResourceNotFoundException_UpdatingTags() { + + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenThrow(ResourceNotFoundException.class); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithTags()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.project.ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + @Test + public void testUpdateHandler_ProjectNotExists_UpdatingTags() { + + final AwsServiceException resourceNotExistsException = SageMakerException.builder() + .message(PROJECT_NOT_EXISTS_ERROR_MESSAGE) + .statusCode(400) + .build(); + + when(proxyClient.client().listTags(any(ListTagsRequest.class))) + .thenThrow(resourceNotExistsException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(getRequestResourceModelWithoutTags()) + .build(); + + Exception exception = assertThrows(CfnNotFoundException.class, () -> invokeHandleRequest(request)); + + assertEquals(String.format(HandlerErrorCode.NotFound.getMessage(), + software.amazon.sagemaker.project.ResourceModel.TYPE_NAME, TEST_PROJECT_NAME), exception.getMessage()); + } + + private ResourceModel getRequestResourceModelWithoutTags() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + } + + private ResourceModel getRequestResourceModelWithTags() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .tags(TEST_CFN_MODEL_TAGS) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + } + + private ResourceModel getRequestResourceModel() { + return ResourceModel.builder() + .creationTime(TEST_CREATION_TIME.toString()) + .projectName(TEST_PROJECT_NAME) + .projectArn(TEST_PROJECT_ARN) + .projectStatus(ProjectStatus.CREATE_COMPLETED.toString()) + .build(); + } + + private ProgressEvent invokeHandleRequest(ResourceHandlerRequest request) { + final software.amazon.sagemaker.project.UpdateHandler handler = new UpdateHandler(); + return handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } +} diff --git a/aws-sagemaker-project/template.yml b/aws-sagemaker-project/template.yml new file mode 100644 index 0000000..c19ed47 --- /dev/null +++ b/aws-sagemaker-project/template.yml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::SageMaker::Project 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.project.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-sagemaker-project-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.sagemaker.project.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-sagemaker-project-handler-1.0-SNAPSHOT.jar +