diff --git a/aws-iot-domainconfiguration/.rpdk-config b/aws-iot-domainconfiguration/.rpdk-config new file mode 100644 index 0000000..b456e6a --- /dev/null +++ b/aws-iot-domainconfiguration/.rpdk-config @@ -0,0 +1,16 @@ +{ + "typeName": "AWS::IoT::DomainConfiguration", + "language": "java", + "runtime": "java8", + "entrypoint": "com.amazonaws.iot.domainconfiguration.HandlerWrapper::handleRequest", + "testEntrypoint": "com.amazonaws.iot.domainconfiguration.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "com", + "amazonaws", + "iot", + "domainconfiguration" + ], + "protocolVersion": "2.0.0" + } +} diff --git a/aws-iot-domainconfiguration/README.md b/aws-iot-domainconfiguration/README.md new file mode 100644 index 0000000..f04eba0 --- /dev/null +++ b/aws-iot-domainconfiguration/README.md @@ -0,0 +1,17 @@ +# AWS::IoT::DomainConfiguration + +Congratulations on starting development! Next steps: + +1. Write the JSON schema describing your resource, `aws-iot-domainconfiguration.json` +2. The RPDK will automatically generate the correct resource model from the + schema whenever the project is built via Maven. You can also do this manually + with the following command: `cfn generate` +3. Implement your resource handlers + + +Please don't modify files under `target/generated-sources/rpdk`, as they will be +automatically overwritten. + +The code use [Lombok](https://projectlombok.org/), and [you may have to install +IDE integrations](https://projectlombok.org/) to enable auto-complete for +Lombok-annotated classes. diff --git a/aws-iot-domainconfiguration/aws-iot-domainconfiguration.json b/aws-iot-domainconfiguration/aws-iot-domainconfiguration.json new file mode 100644 index 0000000..9ad1e5e --- /dev/null +++ b/aws-iot-domainconfiguration/aws-iot-domainconfiguration.json @@ -0,0 +1,180 @@ +{ + "typeName": "AWS::IoT::DomainConfiguration", + "description": "Create and manage a Domain Configuration", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", + "definitions": { + "AuthorizerConfig": { + "type": "object", + "properties": { + "AllowAuthorizerOverride": { + "type": "boolean" + }, + "DefaultAuthorizerName": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[\\w=,@-]+$" + } + }, + "additionalProperties": false + }, + "ServerCertificateSummary": { + "type": "object", + "properties": { + "ServerCertificateArn": { + "type": "string", + "pattern": "^arn:aws(-cn|-us-gov|-iso-b|-iso)?:acm:[a-z]{2}-(gov-|iso-|isob-)?[a-z]{4,9}-\\d{1}:\\d{12}:certificate/[a-zA-Z0-9/-]+$", + "minLength": 1, + "maxLength": 2048 + }, + "ServerCertificateStatus": { + "type": "string", + "enum": [ + "INVALID", + "VALID" + ] + }, + "ServerCertificateStatusDetail": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Key": { + "type": "string" + }, + "Value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "Key", + "Value" + ] + } + } + }, + "properties": { + "DomainConfigurationName": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[\\w.-]+$" + }, + "AuthorizerConfig": { + "$ref": "#/definitions/AuthorizerConfig" + }, + "DomainName": { + "type": "string", + "minLength": 1, + "maxLength": 253 + }, + "ServerCertificateArns": { + "type": "array", + "minItems": 0, + "maxItems": 1, + "items": { + "type": "string", + "pattern": "^arn:aws(-cn|-us-gov|-iso-b|-iso)?:acm:[a-z]{2}-(gov-|iso-|isob-)?[a-z]{4,9}-\\d{1}:\\d{12}:certificate/[a-zA-Z0-9/-]+$", + "minLength": 1, + "maxLength": 2048 + } + }, + "ServiceType": { + "type": "string", + "enum": [ + "DATA", + "CREDENTIAL_PROVIDER", + "JOBS" + ] + }, + "ValidationCertificateArn": { + "type": "string", + "pattern": "^arn:aws(-cn|-us-gov|-iso-b|-iso)?:acm:[a-z]{2}-(gov-|iso-|isob-)?[a-z]{4,9}-\\d{1}:\\d{12}:certificate/[a-zA-Z0-9/-]+$" + }, + "Arn": { + "type": "string" + }, + "DomainConfigurationStatus": { + "type": "string", + "enum": [ + "ENABLED", + "DISABLED" + ] + }, + "DomainType": { + "type": "string", + "enum": [ + "ENDPOINT", + "AWS_MANAGED", + "CUSTOMER_MANAGED" + ] + }, + "ServerCertificates": { + "type": "array", + "items": { + "$ref": "#/definitions/ServerCertificateSummary" + } + }, + "Tags": { + "$ref": "#/definitions/Tags" + } + }, + "additionalProperties": false, + "required": [], + "createOnlyProperties": [ + "/properties/DomainConfigurationName", + "/properties/DomainName", + "/properties/ServiceType", + "/properties/ValidationCertificateArn", + "/properties/ServerCertificateArns" + ], + "readOnlyProperties": [ + "/properties/Arn", + "/properties/DomainType", + "/properties/ServerCertificates" + ], + "writeOnlyProperties": [ + "/properties/ServerCertificateArns" + ], + "primaryIdentifier": [ + "/properties/DomainConfigurationName" + ], + "handlers": { + "create": { + "permissions": [ + "iot:CreateDomainConfiguration", + "iot:UpdateDomainConfiguration", + "iot:DescribeDomainConfiguration" + ] + }, + "read": { + "permissions": [ + "iot:DescribeDomainConfiguration" + ] + }, + "update": { + "permissions": [ + "iot:UpdateDomainConfiguration", + "iot:DescribeDomainConfiguration" + ] + }, + "delete": { + "permissions": [ + "iot:DeleteDomainConfiguration", + "iot:UpdateDomainConfiguration" + ] + }, + "list": { + "permissions": [ + "iot:ListDomainConfigurations" + ] + } + } +} diff --git a/aws-iot-domainconfiguration/lombok.config b/aws-iot-domainconfiguration/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-iot-domainconfiguration/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-iot-domainconfiguration/pom.xml b/aws-iot-domainconfiguration/pom.xml new file mode 100644 index 0000000..c77cce0 --- /dev/null +++ b/aws-iot-domainconfiguration/pom.xml @@ -0,0 +1,213 @@ + + + 4.0.0 + + com.amazonaws.iot.domainconfiguration + aws-iot-domainconfiguration-handler + aws-iot-domainconfiguration-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + central + https://repo.maven.apache.org/maven2 + + + + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + 2.0.2 + + + + software.amazon.awssdk + iot + 2.14.9 + + + + 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 + + + org.jacoco + jacoco-maven-plugin + 0.8.4 + + + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-iot-domainconfiguration.json + + + + + diff --git a/aws-iot-domainconfiguration/resource-role.yaml b/aws-iot-domainconfiguration/resource-role.yaml new file mode 100644 index 0000000..90bcf54 --- /dev/null +++ b/aws-iot-domainconfiguration/resource-role.yaml @@ -0,0 +1,35 @@ +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: + - "iot:CreateDomainConfiguration" + - "iot:DeleteDomainConfiguration" + - "iot:DescribeDomainConfiguration" + - "iot:ListDomainConfigurations" + - "iot:UpdateDomainConfiguration" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CallbackContext.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CallbackContext.java new file mode 100644 index 0000000..a76912e --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CallbackContext.java @@ -0,0 +1,19 @@ +package com.amazonaws.iot.domainconfiguration; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Context used for CloudFormation handlers when creating/updating/deleting domain configuration request. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CallbackContext { + private boolean domainConfigurationDisabled; + private boolean createOrUpdateInProgress; + private int retryCount; +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ClientBuilder.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ClientBuilder.java new file mode 100644 index 0000000..abbc73f --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ClientBuilder.java @@ -0,0 +1,24 @@ +package com.amazonaws.iot.domainconfiguration; + +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.services.iot.IotClient; + +class ClientBuilder { + private static volatile IotClient iotClient; + + static IotClient getClient() { + if (iotClient != null) { + return iotClient; + } + + synchronized (ClientBuilder.class) { + iotClient = IotClient.builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(RetryPolicy.builder().numRetries(3).build()) + .build()) + .build(); + return iotClient; + } + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/Configuration.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/Configuration.java new file mode 100644 index 0000000..785202d --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/Configuration.java @@ -0,0 +1,8 @@ +package com.amazonaws.iot.domainconfiguration; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-iot-domainconfiguration.json"); + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CreateHandler.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CreateHandler.java new file mode 100644 index 0000000..6881606 --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/CreateHandler.java @@ -0,0 +1,157 @@ +package com.amazonaws.iot.domainconfiguration; + +import com.amazonaws.util.StringUtils; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.CreateDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.CreateDomainConfigurationResponse; +import software.amazon.awssdk.services.iot.model.InternalException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.LimitExceededException; +import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; +import software.amazon.awssdk.services.iot.model.Tag; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationRequest; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnResourceConflictException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.resource.IdentifierUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class CreateHandler extends BaseHandler { + private static final int MAX_DOMAIN_CONFIG_NAME = 128; + private static final String OPERATION = "CreateDomainConfiguration"; + + private IotClient iotClient; + + public CreateHandler() { + iotClient = ClientBuilder.getClient(); + } + + public CreateHandler(IotClient iotClient) { + this.iotClient = iotClient; + } + + /** + * Return the domain configuration name if specified in the model or auto-generate one based on the request and + * the resource's logical ID. + * + * @param model The desired resource state + * @param request The resource handler request (used to get logical ID and request token) + * @return template name to use in create request + */ + private static String getDomainConfigurationName(final ResourceModel model, + final ResourceHandlerRequest request) { + return StringUtils.isNullOrEmpty(model.getDomainConfigurationName()) + ? IdentifierUtils.generateResourceIdentifier( + request.getLogicalResourceIdentifier(), + request.getClientRequestToken(), + MAX_DOMAIN_CONFIG_NAME) + : model.getDomainConfigurationName(); + } + + /** + * Get the converted tags of the request model or return null if none are present + * @param model + * @return A collection of tags or null + */ + private static Collection getTags(ResourceModel model) { + final List modelTags = model.getTags(); + return Objects.isNull(modelTags) + ? null + : modelTags.stream() + .map(tag -> Tag.builder().key(tag.getKey()).value(tag.getValue()).build()) + .collect(Collectors.toList()); + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final ResourceModel model = request.getDesiredResourceState(); + final String domainConfigName = getDomainConfigurationName(model, request); + model.setDomainConfigurationName(domainConfigName); + + if (callbackContext != null && callbackContext.isCreateOrUpdateInProgress()) { + int currentRetryCount = callbackContext.getRetryCount(); + try { + final ProgressEvent readResponse = (new ReadHandler(iotClient)) + .handleRequest(proxy, request, CallbackContext.builder().build(), logger); + logger.log(String.format("%s [%s] created successfully", ResourceModel.TYPE_NAME, domainConfigName)); + return readResponse; + } catch (final CfnNotFoundException e) { + if(currentRetryCount >= ResourceUtil.MAX_RETRIES) + throw new CfnResourceConflictException(model.getDomainName(), model.getArn(), + "Unable to create the resource", e); + else + return ProgressEvent.defaultInProgressHandler( + CallbackContext.builder().createOrUpdateInProgress(true).retryCount(currentRetryCount + 1).build(), + ResourceUtil.DELAY_CONSTANT, model); + } + } + + final CreateDomainConfigurationRequest domainRequest = CreateDomainConfigurationRequest.builder() + .domainConfigurationName(domainConfigName) + .domainName(model.getDomainName()) + .authorizerConfig(ResourceUtil.getSdkAuthorizerConfig(model)) + .serverCertificateArns(model.getServerCertificateArns()) + .serviceType(model.getServiceType()) + .tags(getTags(model)) + .validationCertificateArn(model.getValidationCertificateArn()) + .build(); + + try { + CreateDomainConfigurationResponse response = proxy.injectCredentialsAndInvokeV2(domainRequest, + iotClient::createDomainConfiguration); + logger.log(String.format("%s [%s] created. Waiting for %d seconds before returning success", + ResourceModel.TYPE_NAME, domainConfigName, ResourceUtil.DELAY_CONSTANT)); + + // Since we have a property that only shows up in updates, we need to handle it in create as well as + // there is no support for updateOnlyProperties. + if (model.getDomainConfigurationStatus() != null) { + UpdateDomainConfigurationRequest updateRequest = UpdateDomainConfigurationRequest.builder() + .domainConfigurationName(domainConfigName) + .domainConfigurationStatus(model.getDomainConfigurationStatus()) + .build(); + proxy.injectCredentialsAndInvokeV2(updateRequest, iotClient::updateDomainConfiguration); + logger.log(String.format("%s [%s] updated during creation with status [%s]", + ResourceModel.TYPE_NAME, + domainConfigName, + model.getDomainConfigurationStatus())); + + } + return ProgressEvent.defaultInProgressHandler( + CallbackContext.builder().createOrUpdateInProgress(true).retryCount(1).build(), + ResourceUtil.DELAY_CONSTANT, + ResourceModel.builder(). + domainConfigurationName(response.domainConfigurationName()) + .arn(response.domainConfigurationArn()) + .build()); + + } catch (final ResourceAlreadyExistsException e) { + throw new CfnAlreadyExistsException(ResourceModel.TYPE_NAME, domainConfigName); + } catch (final InvalidRequestException e) { + throw new CfnInvalidRequestException(domainRequest.toString(), e); + } catch (final LimitExceededException e) { + throw new CfnServiceLimitExceededException(ResourceModel.TYPE_NAME, e.toString()); + } catch (final InternalException e) { + throw new CfnServiceInternalErrorException(OPERATION, e); + } catch (final ThrottlingException e) { + throw new CfnThrottlingException(OPERATION, e); + } + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/DeleteHandler.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/DeleteHandler.java new file mode 100644 index 0000000..2b5192a --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/DeleteHandler.java @@ -0,0 +1,99 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.DeleteDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.DomainConfigurationStatus; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ServiceUnavailableException; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UnauthorizedException; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationResponse; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class DeleteHandler extends BaseHandler { + private static final String DELETE_OPERATION = "DeleteDomainConfiguration"; + private static final String UPDATE_OPERATION = "UpdateDomainConfiguration"; + + private IotClient iotClient; + + public DeleteHandler() { + iotClient = ClientBuilder.getClient(); + } + + public DeleteHandler(IotClient iotClient) { + this.iotClient = iotClient; + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final ResourceModel model = request.getDesiredResourceState(); + final String domainConfigName = model.getDomainConfigurationName(); + + String operation = DELETE_OPERATION; + + final DeleteDomainConfigurationRequest deleteRequest = DeleteDomainConfigurationRequest.builder() + .domainConfigurationName(domainConfigName) + .build(); + boolean previouslyDisabled = callbackContext != null && callbackContext.isDomainConfigurationDisabled(); + try { + if(!previouslyDisabled && + !StringUtils.equals(DomainConfigurationStatus.DISABLED.toString(), model.getDomainConfigurationStatus())) { + operation = UPDATE_OPERATION; + final UpdateDomainConfigurationRequest updateRequest = UpdateDomainConfigurationRequest.builder() + .domainConfigurationName(model.getDomainConfigurationName()) + .authorizerConfig(ResourceUtil.getSdkAuthorizerConfig(model)) + .domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()) + .removeAuthorizerConfig(false) + .build(); + UpdateDomainConfigurationResponse response = proxy.injectCredentialsAndInvokeV2(updateRequest, iotClient::updateDomainConfiguration); + logger.log(String.format("%s [%s] set as %s before deleting", + ResourceModel.TYPE_NAME, model.getDomainConfigurationName(), DomainConfigurationStatus.DISABLED)); + + return ProgressEvent.defaultInProgressHandler( + CallbackContext.builder().domainConfigurationDisabled(true).build(), + ResourceUtil.DELAY_CONSTANT, + ResourceModel.builder() + .arn(response.domainConfigurationArn()) + .domainConfigurationName(response.domainConfigurationName()) + .build()); + } + + proxy.injectCredentialsAndInvokeV2(deleteRequest, iotClient::deleteDomainConfiguration); + logger.log(String.format("%s [%s] deleted successfully", ResourceModel.TYPE_NAME, domainConfigName)); + + } catch (final ThrottlingException e) { + throw new CfnThrottlingException(operation, e); + } catch (final UnauthorizedException e) { + throw new CfnAccessDeniedException(operation, e); + } catch (final ServiceUnavailableException e) { + throw new CfnGeneralServiceException(operation, e); + } catch (final InternalFailureException e) { + throw new CfnInternalFailureException(e); + } catch (final InvalidRequestException e) { + throw new CfnInvalidRequestException(String.format("Request: %s \n Message: %s", deleteRequest.toString(), + e.getMessage()), e); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDomainConfigurationName()); + } + + return ProgressEvent.defaultSuccessHandler(null); + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ListHandler.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ListHandler.java new file mode 100644 index 0000000..639ef6c --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ListHandler.java @@ -0,0 +1,73 @@ +package com.amazonaws.iot.domainconfiguration; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ListDomainConfigurationsRequest; +import software.amazon.awssdk.services.iot.model.ListDomainConfigurationsResponse; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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; + +import java.util.List; +import java.util.stream.Collectors; + +public class ListHandler extends BaseHandler { + private static final String OPERATION = "ListProvisioningTemplates"; + + private IotClient iotClient; + + public ListHandler() { + iotClient = ClientBuilder.getClient(); + } + + public ListHandler(IotClient iotClient) { + this.iotClient = iotClient; + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final ListDomainConfigurationsRequest domainRequest = ListDomainConfigurationsRequest.builder() + .pageSize(50) + .marker(request.getNextToken()) + .build(); + + try { + final ListDomainConfigurationsResponse response = proxy.injectCredentialsAndInvokeV2( + domainRequest, + iotClient::listDomainConfigurations); + + final List models = response.domainConfigurations().stream() + .map(domainConfig-> ResourceModel.builder() + .domainConfigurationName(domainConfig.domainConfigurationName()) + .arn(domainConfig.domainConfigurationArn()) + .serviceType(domainConfig.serviceTypeAsString()) + .build()) + .collect(Collectors.toList()); + + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(response.nextMarker()) + .status(OperationStatus.SUCCESS) + .build(); + + } catch (final InternalFailureException e) { + throw new CfnServiceInternalErrorException(OPERATION, e); + } catch (final InvalidRequestException e) { + throw new CfnInvalidRequestException(domainRequest.toString(), e); + } catch (final ThrottlingException e) { + throw new CfnThrottlingException(OPERATION, e); + } + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ReadHandler.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ReadHandler.java new file mode 100644 index 0000000..0a214d3 --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ReadHandler.java @@ -0,0 +1,92 @@ +package com.amazonaws.iot.domainconfiguration; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationResponse; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ServerCertificateSummary; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.List; +import java.util.stream.Collectors; + +public class ReadHandler extends BaseHandler { + private static final String OPERATION = "DescribeProvisioningTemplate"; + + private IotClient iotClient; + + public ReadHandler() { + iotClient = ClientBuilder.getClient(); + } + + public ReadHandler(IotClient iotClient) { + this.iotClient = iotClient; + } + + /** + * Convert the SDK's authorizer config to the type expected by CloudFormation + * @param certs The SDK AuthorizerConfig + * @return A converted AuthorizerConfig or null if none was present in the response + */ + private static List getServerCertificates(List certs) { + if (certs != null) { + return certs.stream() + .map(cert -> com.amazonaws.iot.domainconfiguration.ServerCertificateSummary.builder() + .serverCertificateArn(cert.serverCertificateArn()) + .serverCertificateStatus(cert.serverCertificateStatusAsString()) + .serverCertificateStatusDetail(cert.serverCertificateStatusDetail()) + .build()) + .collect(Collectors.toList()); + } + return null; + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final ResourceModel model = request.getDesiredResourceState(); + final DescribeDomainConfigurationRequest domainRequest = DescribeDomainConfigurationRequest.builder() + .domainConfigurationName(model.getDomainConfigurationName()) + .build(); + + try { + final DescribeDomainConfigurationResponse response = proxy.injectCredentialsAndInvokeV2( + domainRequest, + iotClient::describeDomainConfiguration); + + return ProgressEvent.defaultSuccessHandler(ResourceModel.builder() + .arn(response.domainConfigurationArn()) + .domainConfigurationName(response.domainConfigurationName()) + .domainConfigurationStatus(response.domainConfigurationStatusAsString()) + .domainName(response.domainName()) + .domainType(response.domainTypeAsString()) + .serviceType(response.serviceTypeAsString()) + .authorizerConfig(ResourceUtil.getResourceModelAuthorizerConfig(response.authorizerConfig())) + .serverCertificates(getServerCertificates(response.serverCertificates())) + .build()); + + } catch (final InternalFailureException e) { + throw new CfnServiceInternalErrorException(OPERATION, e); + } catch (final InvalidRequestException e) { + throw new CfnInvalidRequestException(domainRequest.toString(), e); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, domainRequest.domainConfigurationName()); + } catch (final ThrottlingException e) { + throw new CfnThrottlingException(OPERATION, e); + } + } +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ResourceUtil.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ResourceUtil.java new file mode 100644 index 0000000..8512aa8 --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/ResourceUtil.java @@ -0,0 +1,41 @@ +package com.amazonaws.iot.domainconfiguration; + +import software.amazon.awssdk.services.iot.model.AuthorizerConfig; + +public final class ResourceUtil { + + public static final int DELAY_CONSTANT = 35; + public static final int MAX_RETRIES = 3; + + /** + * Convert the resource model's authorizer config to the type expected by the SDK + * @param model The resource model + * @return A converted AuthorizerConfig or null if none was present in the model + */ + public static AuthorizerConfig getSdkAuthorizerConfig(ResourceModel model) { + final com.amazonaws.iot.domainconfiguration.AuthorizerConfig config = model.getAuthorizerConfig(); + if (config != null) { + return AuthorizerConfig.builder() + .allowAuthorizerOverride(config.getAllowAuthorizerOverride()) + .defaultAuthorizerName(config.getDefaultAuthorizerName()) + .build(); + } + return null; + } + + /** + * Convert the SDK's authorizer config to the type expected by CloudFormation + * @param config The SDK AuthorizerConfig + * @return A converted AuthorizerConfig or null if none was present in the response + */ + public static com.amazonaws.iot.domainconfiguration.AuthorizerConfig getResourceModelAuthorizerConfig(AuthorizerConfig config) { + if (config != null) { + return com.amazonaws.iot.domainconfiguration.AuthorizerConfig.builder() + .allowAuthorizerOverride(config.allowAuthorizerOverride()) + .defaultAuthorizerName(config.defaultAuthorizerName()) + .build(); + } + return null; + } + +} diff --git a/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/UpdateHandler.java b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/UpdateHandler.java new file mode 100644 index 0000000..56e06ae --- /dev/null +++ b/aws-iot-domainconfiguration/src/main/java/com/amazonaws/iot/domainconfiguration/UpdateHandler.java @@ -0,0 +1,119 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.CertificateValidationException; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationResponse; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnNotUpdatableException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.List; + +public class UpdateHandler extends BaseHandler { + private static final String OPERATION = "UpdateDomainConfiguration"; + + private IotClient iotClient; + + public UpdateHandler() { + iotClient = ClientBuilder.getClient(); + } + + public UpdateHandler(IotClient iotClient) { + this.iotClient = iotClient; + } + + private boolean areServerCertificatesUnchanged(List newModelCerts, List prevModelCerts) { + if (newModelCerts.size() != prevModelCerts.size()) return false; + return newModelCerts.containsAll(prevModelCerts); + } + + private void validatePropertiesAreUpdatable(ResourceModel prevModel, ResourceModel newModel) { + if (!StringUtils.equals(newModel.getDomainConfigurationName(), prevModel.getDomainConfigurationName())) { + throw new CfnNotUpdatableException(ResourceModel.TYPE_NAME, "DomainConfigurationName"); + } + if (!StringUtils.equals(newModel.getDomainName(), prevModel.getDomainName())) { + throw new CfnNotUpdatableException(ResourceModel.TYPE_NAME, "DomainName"); + } + if (!StringUtils.equals(newModel.getServiceType(), prevModel.getServiceType())) { + throw new CfnNotUpdatableException(ResourceModel.TYPE_NAME, "ServiceType"); + } + if (!StringUtils.equals(newModel.getValidationCertificateArn(), prevModel.getValidationCertificateArn())) { + throw new CfnNotUpdatableException(ResourceModel.TYPE_NAME, "ValidationCertificateArn"); + } + if (!areServerCertificatesUnchanged(newModel.getServerCertificateArns(), prevModel.getServerCertificateArns())) { + throw new CfnNotUpdatableException(ResourceModel.TYPE_NAME, "ValidationCertificateArn"); + } + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + final ResourceModel model = request.getDesiredResourceState(); + + final ProgressEvent readResponse = (new ReadHandler(iotClient)) + .handleRequest(proxy, request, CallbackContext.builder().build(), logger); + + if (callbackContext != null && callbackContext.isCreateOrUpdateInProgress()) { + logger.log(String.format("%s [%s] updated successfully", ResourceModel.TYPE_NAME, model.getDomainConfigurationName())); + return readResponse; + } + + final ResourceModel prevModel = request.getPreviousResourceState(); + + validatePropertiesAreUpdatable(prevModel, model); + + // Determine if we need to set the removeAuthorizerConfig flag by comparing the original state with the + // desired state. + boolean removeAuthorizerConfig = false; + if (prevModel.getAuthorizerConfig() != null && model.getAuthorizerConfig() == null) { + removeAuthorizerConfig = true; + } + + final UpdateDomainConfigurationRequest domainRequest = UpdateDomainConfigurationRequest.builder() + .domainConfigurationName(model.getDomainConfigurationName()) + .authorizerConfig(ResourceUtil.getSdkAuthorizerConfig(model)) + .domainConfigurationStatus(model.getDomainConfigurationStatus()) + .removeAuthorizerConfig(removeAuthorizerConfig) + .build(); + + try { + UpdateDomainConfigurationResponse response = proxy.injectCredentialsAndInvokeV2(domainRequest, + iotClient::updateDomainConfiguration); + logger.log(String.format("%s [%s] updated. Waiting for %d seconds before returning success.", + ResourceModel.TYPE_NAME, model.getDomainConfigurationName(),ResourceUtil.DELAY_CONSTANT)); + + return ProgressEvent.defaultInProgressHandler( + CallbackContext.builder().createOrUpdateInProgress(true).build(), + ResourceUtil.DELAY_CONSTANT, + ResourceModel.builder(). + domainConfigurationName(response.domainConfigurationName()) + .arn(response.domainConfigurationArn()) + .build()); + } catch (final ResourceNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDomainConfigurationName()); + } catch (final InvalidRequestException|CertificateValidationException e) { + throw new CfnInvalidRequestException(domainRequest.toString(), e); + } catch (final ThrottlingException e) { + throw new CfnThrottlingException(OPERATION, e); + } catch (final InternalFailureException e) { + throw new CfnServiceInternalErrorException(OPERATION, e); + } + + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/CreateHandlerTest.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/CreateHandlerTest.java new file mode 100644 index 0000000..7bc7bca --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/CreateHandlerTest.java @@ -0,0 +1,211 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.Assertions; +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.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.DomainConfigurationStatus; +import software.amazon.awssdk.services.iot.model.InternalException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.LimitExceededException; +import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest extends DomainConfigurationTestBase { + private CreateHandler handler; + private final ResourceModel expectedResponse = ResourceModel.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .arn(DOMAIN_CONFIG_ARN) + .build(); + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new CreateHandler(); + } + + @Test + public void handleRequest_SimpleCreateInProgress() { + final ResourceModel model = defaultModelBuilder() + .domainConfigurationStatus(null) + .build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(DEFAULT_CREATE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackContext()).isNotNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(ResourceUtil.DELAY_CONSTANT); + assertThat(response.getResourceModel()).isEqualTo(DEFAULT_RESOURCE_MODEL); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getDomainConfigurationName()).isEqualTo(DOMAIN_CONFIG_NAME); + + verify(proxy, times(1)).injectCredentialsAndInvokeV2(any(), any()); + } + + @Test + public void handleRequest_SimpleCreateSuccess() { + final ResourceModel model = defaultModelBuilder() + .domainConfigurationStatus(null) + .build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + + when(proxy.injectCredentialsAndInvokeV2(any(DescribeDomainConfigurationRequest.class), any())) + .thenReturn(DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response = handler.handleRequest(proxy, request, + CallbackContext.builder().createOrUpdateInProgress(true).retryCount(1).build(), logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNotNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getDomainConfigurationName()).isEqualTo(DOMAIN_CONFIG_NAME); + + verify(proxy, times(1)).injectCredentialsAndInvokeV2(any(), any()); + } + + @Test + public void handleRequest_InvokesTwice() { + final ResourceModel model = defaultModelBuilder() + .domainConfigurationStatus(DomainConfigurationStatus.ENABLED.toString()) + .build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(DEFAULT_CREATE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackContext()).isNotNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(ResourceUtil.DELAY_CONSTANT); + assertThat(response.getResourceModel()).isEqualTo(DEFAULT_RESOURCE_MODEL); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getDomainConfigurationName()).isEqualTo(DOMAIN_CONFIG_NAME); + + verify(proxy, times(2)).injectCredentialsAndInvokeV2(any(), any()); + } + + @Test + public void handleRequest_GeneratesName() { + final ResourceModel model = defaultModelBuilder().domainConfigurationName(null).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(GENERATED_CREATE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getResourceModel().getDomainConfigurationName()).isNotNull(); + assertThat(response.getResourceModel().getDomainConfigurationName()).isNotEqualTo(DOMAIN_CONFIG_NAME); + } + + @Test + public void handleRequest_ResourceConflictFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ResourceAlreadyExistsException.builder().resourceId(DOMAIN_CONFIG_NAME).build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnAlreadyExistsException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_InvalidRequestFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InvalidRequestException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_LimitExceededFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(LimitExceededException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnServiceLimitExceededException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_InternalExceptionFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InternalException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnServiceInternalErrorException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_ThrottlingFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ThrottlingException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnThrottlingException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DeleteHandlerTest.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DeleteHandlerTest.java new file mode 100644 index 0000000..d66e11a --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DeleteHandlerTest.java @@ -0,0 +1,182 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.Assertions; +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.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ServiceUnavailableException; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UnauthorizedException; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationRequest; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +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 DomainConfigurationTestBase { + private DeleteHandler handler; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new DeleteHandler(); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceModel model = defaultModelBuilder().domainConfigurationStatus("DISABLED").build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_DisableBeforeDelete() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(DEFAULT_UPDATE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + verify(proxy, times(1)).injectCredentialsAndInvokeV2(any(UpdateDomainConfigurationRequest.class), any()); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackContext()).isNotNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(ResourceUtil.DELAY_CONSTANT); + assertThat(response.getResourceModel()).isEqualTo(DEFAULT_RESOURCE_MODEL); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_InProgressDeleteSucceeds() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, + CallbackContext.builder().domainConfigurationDisabled(true).build(), logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_ResourceNotFoundFails() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ResourceNotFoundException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CfnInternalFailureException() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InternalFailureException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnInternalFailureException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CfnInvalidRequestException() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InvalidRequestException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CfnGeneralServiceExceptionUnavailable() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ServiceUnavailableException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnGeneralServiceException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CfnThrottlingException() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ThrottlingException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnThrottlingException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CfnAccessDeniedException() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(UnauthorizedException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnAccessDeniedException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DomainConfigurationTestBase.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DomainConfigurationTestBase.java new file mode 100644 index 0000000..8a64c1f --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/DomainConfigurationTestBase.java @@ -0,0 +1,107 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.BeforeEach; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.AuthorizerConfig; +import software.amazon.awssdk.services.iot.model.CreateDomainConfigurationResponse; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationResponse; +import software.amazon.awssdk.services.iot.model.DomainConfigurationStatus; +import software.amazon.awssdk.services.iot.model.DomainType; +import software.amazon.awssdk.services.iot.model.ServerCertificateSummary; +import software.amazon.awssdk.services.iot.model.ServiceType; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationResponse; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.Collections; + +import static org.mockito.Mockito.mock; + +public class DomainConfigurationTestBase { + protected final static String DOMAIN_CONFIG_NAME = "SampleDomainConfig"; + protected final static String DOMAIN_CONFIG_NAME_GENERATED = "GeneratedDomainConfig"; + protected final static String DOMAIN_CONFIG_NAME_UPDATED = "SampleDomainConfigUpdated"; + protected final static String DOMAIN_CONFIG_ARN = "arn:aws:iot:us-east-1:0123456789:domainconfiguration/SampleDomain/2e8nm"; + protected final static String DOMAIN_NAME = "example.com"; + + protected final static String DEFAULT_AUTHORIZER_NAME = "authorizer"; + protected final static AuthorizerConfig AUTHORIZER_CONFIG = AuthorizerConfig.builder() + .defaultAuthorizerName(DEFAULT_AUTHORIZER_NAME) + .allowAuthorizerOverride(false) + .build(); + + protected final static String SERVER_CERT_ARN = "arn:aws:iam::01234567890:certificate/Something"; + protected final static String CERT_STATUS = "VALID"; + protected final static String CERT_DETAIL = "Details"; + protected final static ServerCertificateSummary SERVER_CERTIFICATE_SUMMARY = ServerCertificateSummary.builder() + .serverCertificateArn(SERVER_CERT_ARN) + .serverCertificateStatus(CERT_STATUS) + .serverCertificateStatusDetail(CERT_DETAIL) + .build(); + + protected final static DescribeDomainConfigurationResponse DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE = DescribeDomainConfigurationResponse.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .domainName(DOMAIN_NAME) + .authorizerConfig(AUTHORIZER_CONFIG) + .domainConfigurationStatus("ENABLED") + .serverCertificates(SERVER_CERTIFICATE_SUMMARY) + .domainType(DomainType.ENDPOINT) + .serviceType(ServiceType.CREDENTIAL_PROVIDER) + .build(); + + protected final static CreateDomainConfigurationResponse DEFAULT_CREATE_DOMAIN_CONFIGURATION_RESPONSE = CreateDomainConfigurationResponse.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .build(); + + protected final static CreateDomainConfigurationResponse GENERATED_CREATE_DOMAIN_CONFIGURATION_RESPONSE = CreateDomainConfigurationResponse.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME_GENERATED) + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .build(); + + protected final static ResourceModel DEFAULT_RESOURCE_MODEL = ResourceModel.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .arn(DOMAIN_CONFIG_ARN) + .build(); + + protected final static UpdateDomainConfigurationResponse DEFAULT_UPDATE_DOMAIN_CONFIGURATION_RESPONSE = UpdateDomainConfigurationResponse.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .build(); + + protected final static String REQUEST_TOKEN = "RequestToken"; + protected final static String LOGICAL_ID = "LogicalResourceId"; + + protected IotClient iotClient; + + @BeforeEach + public void setup() { + iotClient = mock(IotClient.class); + } + + protected ResourceModel.ResourceModelBuilder defaultModelBuilder() { + return ResourceModel.builder() + .domainName(DOMAIN_NAME) + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .arn(DOMAIN_CONFIG_ARN) + .serverCertificates(Collections.singletonList(com.amazonaws.iot.domainconfiguration.ServerCertificateSummary.builder() + .serverCertificateArn(SERVER_CERT_ARN) + .serverCertificateStatus(CERT_STATUS) + .serverCertificateStatusDetail(CERT_DETAIL) + .build())) + .authorizerConfig(com.amazonaws.iot.domainconfiguration.AuthorizerConfig.builder() + .allowAuthorizerOverride(false) + .defaultAuthorizerName(DEFAULT_AUTHORIZER_NAME) + .build()) + .serviceType(ServiceType.CREDENTIAL_PROVIDER.toString()) + .domainType(DomainType.ENDPOINT.toString()) + .domainConfigurationStatus(DomainConfigurationStatus.ENABLED.toString()); + } + + protected ResourceHandlerRequest.ResourceHandlerRequestBuilder defaultRequestBuilder(ResourceModel model) { + return ResourceHandlerRequest.builder() + .clientRequestToken(REQUEST_TOKEN) + .logicalResourceIdentifier(LOGICAL_ID) + .desiredResourceState(model); + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ListHandlerTest.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ListHandlerTest.java new file mode 100644 index 0000000..c33e86f --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ListHandlerTest.java @@ -0,0 +1,145 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.Assertions; +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.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.iot.model.DomainConfigurationSummary; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ListDomainConfigurationsRequest; +import software.amazon.awssdk.services.iot.model.ListDomainConfigurationsResponse; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest extends DomainConfigurationTestBase { + private ListHandler handler; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @Captor + ArgumentCaptor domainRequestCaptor; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new ListHandler(); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceHandlerRequest request = defaultRequestBuilder(null).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenReturn(ListDomainConfigurationsResponse.builder() + .nextMarker(null) + .domainConfigurations(DomainConfigurationSummary.builder() + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .build()) + .build()); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNotNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + List models = response.getResourceModels(); + assertThat(models.size()).isEqualTo(1); + + ResourceModel model = models.get(0); + assertThat(model.getDomainConfigurationName()).isEqualTo(DOMAIN_CONFIG_NAME); + assertThat(model.getArn()).isEqualTo(DOMAIN_CONFIG_ARN); + } + + @Test + public void handleRequest_PassedNextToken() { + final String nextToken = "NEXT"; + final ResourceHandlerRequest request = defaultRequestBuilder(null) + .nextToken(nextToken) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenReturn(ListDomainConfigurationsResponse.builder() + .nextMarker(null) + .domainConfigurations(DomainConfigurationSummary.builder() + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .domainConfigurationName(DOMAIN_NAME) + .build()) + .build()); + + handler.handleRequest(proxy, request, null, logger); + + verify(proxy).injectCredentialsAndInvokeV2(domainRequestCaptor.capture(), any()); + assertThat(domainRequestCaptor.getValue().marker()).isEqualTo(nextToken); + } + + @Test + public void handleRequest_InternalFailure() { + final ResourceHandlerRequest request = defaultRequestBuilder(null).build(); + + doThrow(InternalFailureException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnServiceInternalErrorException.class, () -> { + handler.handleRequest(proxy, request, null, logger); + }); + } + + @Test + public void handleRequest_InvalidRequest() { + final ResourceHandlerRequest request = defaultRequestBuilder(null).build(); + + doThrow(InvalidRequestException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> { + handler.handleRequest(proxy, request, null, logger); + }); + } + + @Test + public void handleRequest_ThrottlingExceptio() { + final ResourceHandlerRequest request = defaultRequestBuilder(null).build(); + + doThrow(ThrottlingException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnThrottlingException.class, () -> { + handler.handleRequest(proxy, request, null, logger); + }); + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ReadHandlerTest.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ReadHandlerTest.java new file mode 100644 index 0000000..86b0f9c --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/ReadHandlerTest.java @@ -0,0 +1,120 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.Assertions; +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.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationResponse; +import software.amazon.awssdk.services.iot.model.DomainType; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ServiceType; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest extends DomainConfigurationTestBase { + private ReadHandler handler; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new ReadHandler(); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenReturn(DescribeDomainConfigurationResponse.builder() + .domainConfigurationName(DOMAIN_CONFIG_NAME) + .domainConfigurationArn(DOMAIN_CONFIG_ARN) + .domainName(DOMAIN_NAME) + .authorizerConfig(AUTHORIZER_CONFIG) + .domainConfigurationStatus("ENABLED") + .serverCertificates(SERVER_CERTIFICATE_SUMMARY) + .domainType(DomainType.ENDPOINT) + .serviceType(ServiceType.CREDENTIAL_PROVIDER) + .build()); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_InternalFailure() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InternalFailureException.builder().build()).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnServiceInternalErrorException.class, () -> + handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_InvalidRequest() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(InvalidRequestException.builder().build()).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> + handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_ResourceNotFound() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ResourceNotFoundException.builder().build()).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnNotFoundException.class, () -> + handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_ThrottlingException() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).build(); + + doThrow(ThrottlingException.builder().build()).when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnThrottlingException.class, () -> + handler.handleRequest(proxy, request, null, logger)); + } +} diff --git a/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/UpdateHandlerTest.java b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/UpdateHandlerTest.java new file mode 100644 index 0000000..fca5018 --- /dev/null +++ b/aws-iot-domainconfiguration/src/test/java/com/amazonaws/iot/domainconfiguration/UpdateHandlerTest.java @@ -0,0 +1,194 @@ +package com.amazonaws.iot.domainconfiguration; + +import org.junit.jupiter.api.Assertions; +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.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.iot.model.CertificateValidationException; +import software.amazon.awssdk.services.iot.model.DescribeDomainConfigurationRequest; +import software.amazon.awssdk.services.iot.model.DomainConfigurationStatus; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UpdateDomainConfigurationRequest; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnNotUpdatableException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UpdateHandlerTest extends DomainConfigurationTestBase { + private UpdateHandler handler; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new UpdateHandler(); + } + + @Test + public void handleRequest_SimpleInProgress() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenAnswer(invocationOnMock -> { + if (invocationOnMock.getArguments()[0] instanceof UpdateDomainConfigurationRequest) + return DEFAULT_UPDATE_DOMAIN_CONFIGURATION_RESPONSE; + else if (invocationOnMock.getArguments()[0] instanceof DescribeDomainConfigurationRequest) + return DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE; + return null; + }); + + final ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); + assertThat(response.getCallbackContext()).isNotNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(ResourceUtil.DELAY_CONSTANT); + assertThat(response.getResourceModel()).isEqualTo(DEFAULT_RESOURCE_MODEL); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(DescribeDomainConfigurationRequest.class), any())) + .thenReturn(DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE); + + final ProgressEvent response = handler.handleRequest(proxy, request, + CallbackContext.builder().createOrUpdateInProgress(true).build(), logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_ResourceNotUpdatable() { + final ResourceModel model = defaultModelBuilder().domainConfigurationName(DOMAIN_CONFIG_NAME_UPDATED).build(); + final ResourceModel prevModel = defaultModelBuilder().build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(DescribeDomainConfigurationRequest.class), any())) + .thenReturn(DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE); + + + + Assertions.assertThrows(CfnNotUpdatableException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_ResourceNotFound() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + doThrow(ResourceNotFoundException.builder().build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_InvalidRequest() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenAnswer(invocationOnMock -> { + if (invocationOnMock.getArguments()[0] instanceof UpdateDomainConfigurationRequest) + throw InvalidRequestException.builder().build(); + else if (invocationOnMock.getArguments()[0] instanceof DescribeDomainConfigurationRequest) + return DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE; + return null; + }); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_CertificateValidation() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenAnswer(invocationOnMock -> { + if (invocationOnMock.getArguments()[0] instanceof UpdateDomainConfigurationRequest) + throw CertificateValidationException.builder().build(); + else if (invocationOnMock.getArguments()[0] instanceof DescribeDomainConfigurationRequest) + return DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE; + return null; + }); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_Throttling() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenAnswer(invocationOnMock -> { + if (invocationOnMock.getArguments()[0] instanceof UpdateDomainConfigurationRequest) + throw ThrottlingException.builder().build(); + else if (invocationOnMock.getArguments()[0] instanceof DescribeDomainConfigurationRequest) + return DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE; + return null; + }); + + Assertions.assertThrows(CfnThrottlingException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_InternalFailure() { + final ResourceModel model = defaultModelBuilder().build(); + final ResourceModel prevModel = defaultModelBuilder().domainConfigurationStatus(DomainConfigurationStatus.DISABLED.toString()).build(); + final ResourceHandlerRequest request = defaultRequestBuilder(model).previousResourceState(prevModel).build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())).thenAnswer(invocationOnMock -> { + if (invocationOnMock.getArguments()[0] instanceof UpdateDomainConfigurationRequest) + throw InternalFailureException.builder().build(); + else if (invocationOnMock.getArguments()[0] instanceof DescribeDomainConfigurationRequest) + return DEFAULT_DESCRIBE_DOMAIN_CONFIGURATION_RESPONSE; + return null; + }); + + Assertions.assertThrows(CfnServiceInternalErrorException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } +} diff --git a/aws-iot-domainconfiguration/template.yml b/aws-iot-domainconfiguration/template.yml new file mode 100644 index 0000000..7ba3bae --- /dev/null +++ b/aws-iot-domainconfiguration/template.yml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::IoT::DomainConfiguration resource type + +Globals: + Function: + Timeout: 60 # docker start-up times can be long for SAM CLI + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.iot.domainconfiguration.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-iot-domainconfiguration-handler-1.0-SNAPSHOT.jar + MemorySize: 256 + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.iot.domainconfiguration.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-iot-domainconfiguration-handler-1.0-SNAPSHOT.jar + MemorySize: 256