package software.amazon.sagemaker.imageversion; import java.util.ArrayList; import java.util.List; 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.CreateImageVersionRequest; import software.amazon.awssdk.services.sagemaker.model.CreateImageVersionResponse; import software.amazon.awssdk.services.sagemaker.model.ImageVersionStatus; import software.amazon.awssdk.services.sagemaker.model.ResourceInUseException; import software.amazon.awssdk.services.sagemaker.model.ResourceNotFoundException; import software.amazon.cloudformation.Action; import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.CfnResourceConflictException; 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; /** * CloudFormation resource handler to be invoked when creating a new AWS::SageMaker::ImageVersion resource. */ public class CreateHandler extends BaseHandlerStd { private Logger logger; protected ProgressEvent<ResourceModel, CallbackContext> handleRequest( final AmazonWebServicesClientProxy proxy, final ResourceHandlerRequest<ResourceModel> request, final CallbackContext callbackContext, final ProxyClient<SageMakerClient> proxyClient, final Logger logger) { this.logger = logger; final ResourceModel model = request.getDesiredResourceState(); if (callbackContext.callGraphs().isEmpty()) { final List<String> readOnlyProperties = getReadOnlyProperties(model); if (!readOnlyProperties.isEmpty()) { throw new CfnInvalidRequestException(String.format("The following ReadOnly properties were set: [%s]", StringUtils.join(readOnlyProperties,","))); } } return ProgressEvent.progress(model, callbackContext) .then(progress -> proxy.initiate("AWS-SageMaker-ImageVersion::Create", proxyClient, model, callbackContext) .translateToServiceRequest(resourceModel -> Translator.translateToCreateRequest(model, request.getClientRequestToken())) .makeServiceCall(this::createImageVersion) .stabilize(this::stabilizedOnCreate) .progress()) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } /** * Returns a list of any readOnly properties that are set on the input model. * @param model the input CFN resource model * @return List of Strings of any readOnly properties set */ private List<String> getReadOnlyProperties(final ResourceModel model) { final List<String> readOnlyPropertiesSet = new ArrayList<>(); if (model.getImageArn() != null) { readOnlyPropertiesSet.add("ImageArn"); } if (model.getImageVersionArn() != null) { readOnlyPropertiesSet.add("ImageVersionArn"); } if (model.getVersion() != null) { readOnlyPropertiesSet.add("Version"); } if (model.getContainerImage() != null) { readOnlyPropertiesSet.add("ContainerImage"); } return readOnlyPropertiesSet; } /** * Invokes the create request using the provided proxyClient. * @param createImageVersionRequest the aws service request to create an image version * @param proxyClient the aws client used to make service calls * @return createImageResponse aws service response from creating an image version resource */ private CreateImageVersionResponse createImageVersion( final CreateImageVersionRequest createImageVersionRequest, final ProxyClient<SageMakerClient> proxyClient) { final CreateImageVersionResponse response; try { response = proxyClient.injectCredentialsAndInvokeV2(createImageVersionRequest, proxyClient.client()::createImageVersion); } catch (final ResourceInUseException e) { throw new CfnResourceConflictException(ResourceModel.TYPE_NAME, createImageVersionRequest.imageName(), String.format("Image: [%s] is in use. Could not create new Image Version.", createImageVersionRequest.imageName()), e); } catch (final ResourceNotFoundException e) { throw new CfnInvalidRequestException(String.format("Image: [%s] not found. Failed to create Image Version", createImageVersionRequest.imageName()), e); } catch (final AwsServiceException e) { throw ExceptionMapper.getCfnException(Action.CREATE.toString(), ResourceModel.TYPE_NAME, String.format( "ImageVersion for image: %s failed to create.", createImageVersionRequest.imageName()), e); } return response; } /** * Stabilization method to ensure that a newly created image version resource has moved from CREATING status to CREATED. * @param createImageVersionRequest the aws service request to create an image version * @param createImageVersionResponse the aws service response from creating an image version resource * @param proxyClient the aws client used to make service calls * @param model the CloudFormation resource model * @param callbackContext the callback context * @return boolean state of whether the image version resource has stabilized or not */ private boolean stabilizedOnCreate( final CreateImageVersionRequest createImageVersionRequest, final CreateImageVersionResponse createImageVersionResponse, final ProxyClient<SageMakerClient> proxyClient, final ResourceModel model, final CallbackContext callbackContext) { if (model.getImageVersionArn() == null) { model.setImageVersionArn(createImageVersionResponse.imageVersionArn()); } final ImageVersionStatus imageVersionStatus= proxyClient.injectCredentialsAndInvokeV2( Translator.translateToReadRequest(model), proxyClient.client()::describeImageVersion) .imageVersionStatus(); switch (imageVersionStatus) { case CREATE_FAILED: throw new CfnGeneralServiceException(String.format("%s [%s] failed to create.", ResourceModel.TYPE_NAME, model.getImageVersionArn())); case CREATED: logger.log(String.format("%s [%s] has been stabilized with status %s.", ResourceModel.TYPE_NAME, model.getImageVersionArn(), imageVersionStatus)); return true; case CREATING: logger.log(String.format("%s [%s] is stabilizing %s.", ResourceModel.TYPE_NAME, model.getImageVersionArn(), imageVersionStatus)); return false; default: throw new CfnGeneralServiceException( String.format("Stabilizing of %s failed with an unexpected status %s", model.getImageVersionArn(), imageVersionStatus)); } } }