diff --git a/aws-qbusiness-application/aws-qbusiness-application.json b/aws-qbusiness-application/aws-qbusiness-application.json index e5dc430..96d86eb 100755 --- a/aws-qbusiness-application/aws-qbusiness-application.json +++ b/aws-qbusiness-application/aws-qbusiness-application.json @@ -98,6 +98,18 @@ "EncryptionConfiguration": { "$ref": "#/definitions/EncryptionConfiguration" }, + "IdentityCenterApplicationArn": { + "type": "string", + "maxLength": 1224, + "minLength": 10, + "pattern": "^arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):sso::\\d{12}:application/(sso)?ins-[a-zA-Z0-9-.]{16}/apl-[a-zA-Z0-9]{16}$" + }, + "IdentityCenterInstanceArn": { + "type": "string", + "maxLength": 1224, + "minLength": 10, + "pattern": "^arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}$" + }, "RoleArn": { "type": "string", "maxLength": 1284, @@ -128,9 +140,13 @@ "/properties/ApplicationArn", "/properties/ApplicationId", "/properties/CreatedAt", + "/properties/IdentityCenterApplicationArn", "/properties/Status", "/properties/UpdatedAt" ], + "writeOnlyProperties": [ + "/properties/IdentityCenterInstanceArn" + ], "createOnlyProperties": [ "/properties/EncryptionConfiguration" ], @@ -146,7 +162,12 @@ "qbusiness:CreateApplication", "qbusiness:GetApplication", "qbusiness:ListTagsForResource", - "qbusiness:TagResource" + "qbusiness:TagResource", + "sso:CreateApplication", + "sso:DeleteApplication", + "sso:PutApplicationAccessScope", + "sso:PutApplicationAuthenticationMethod", + "sso:PutApplicationGrant" ] }, "read": { @@ -162,14 +183,20 @@ "qbusiness:ListTagsForResource", "qbusiness:TagResource", "qbusiness:UntagResource", - "qbusiness:UpdateApplication" + "qbusiness:UpdateApplication", + "sso:CreateApplication", + "sso:DeleteApplication", + "sso:PutApplicationAccessScope", + "sso:PutApplicationAuthenticationMethod", + "sso:PutApplicationGrant" ] }, "delete": { "permissions": [ "kms:RetireGrant", "qbusiness:DeleteApplication", - "qbusiness:GetApplication" + "qbusiness:GetApplication", + "sso:DeleteApplication" ] }, "list": { diff --git a/aws-qbusiness-application/pom.xml b/aws-qbusiness-application/pom.xml index 2e4e189..b6ca5f6 100644 --- a/aws-qbusiness-application/pom.xml +++ b/aws-qbusiness-application/pom.xml @@ -55,19 +55,19 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 software.amazon.awssdk aws-core - 2.23.12 + 2.25.42 software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java index 9a36350..92bbda0 100644 --- a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java +++ b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.services.qbusiness.model.TagResourceRequest; import software.amazon.awssdk.services.qbusiness.model.UntagResourceRequest; import software.amazon.awssdk.services.qbusiness.model.UpdateApplicationRequest; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; /** @@ -36,10 +37,15 @@ public class Translator { * @return awsRequest the aws service request to create a resource */ static CreateApplicationRequest translateToCreateRequest(final String idempotentToken, final ResourceModel model) { + // https://w.amazon.com/bin/view/AWS21/Design/Uluru/Onboarding_Guide/ModelingGuidelines#HRequiredWriteOnlyProperties + if (model.getIdentityCenterInstanceArn() == null) { + throw new CfnInvalidRequestException("IdentityCenterInstanceArn is required."); + } return CreateApplicationRequest.builder() .clientToken(idempotentToken) .displayName(model.getDisplayName()) .roleArn(model.getRoleArn()) + .identityCenterInstanceArn(model.getIdentityCenterInstanceArn()) .description(model.getDescription()) .encryptionConfiguration(toServiceEncryptionConfig(model.getEncryptionConfiguration())) .attachmentsConfiguration(toServiceAttachmentConfiguration(model.getAttachmentsConfiguration())) @@ -79,6 +85,7 @@ static ResourceModel translateFromReadResponse(final GetApplicationResponse awsR .applicationId(awsResponse.applicationId()) .applicationArn(awsResponse.applicationArn()) .roleArn(awsResponse.roleArn()) + .identityCenterApplicationArn(awsResponse.identityCenterApplicationArn()) .status(awsResponse.statusAsString()) .description(awsResponse.description()) .createdAt(instantToString(awsResponse.createdAt())) @@ -176,6 +183,7 @@ static UpdateApplicationRequest translateToUpdateRequest(final ResourceModel mod .displayName(model.getDisplayName()) .description(model.getDescription()) .roleArn(model.getRoleArn()) + .identityCenterInstanceArn(model.getIdentityCenterInstanceArn()) .attachmentsConfiguration(toServiceAttachmentConfiguration(model.getAttachmentsConfiguration())) .build(); } @@ -203,6 +211,10 @@ static List translateFromListResponse(final ListApplicationsRespo .stream() .map(application -> ResourceModel.builder() .applicationId(application.applicationId()) + .displayName(application.displayName()) + .createdAt(instantToString(application.createdAt())) + .updatedAt(instantToString(application.updatedAt())) + .status(application.statusAsString()) .build() ) .toList(); diff --git a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java index 3e1a503..a7249c4 100644 --- a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java +++ b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java @@ -22,7 +22,6 @@ public static String buildApplicationArn(final ResourceHandlerRequestbuilder() diff --git a/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/ReadHandlerTest.java b/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/ReadHandlerTest.java index 4c2c01e..bdf09bc 100644 --- a/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/ReadHandlerTest.java +++ b/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/ReadHandlerTest.java @@ -106,6 +106,7 @@ public void handleRequest_SimpleSuccess() { .updatedAt(Instant.ofEpochMilli(1697839335000L)) .description("this is a description, there are many like it but this one is mine.") .displayName("Foobar") + .identityCenterApplicationArn("arn:aws:sso::123456789012:application/ssoins/apl") .status(ApplicationStatus.ACTIVE) .encryptionConfiguration(EncryptionConfiguration.builder() .kmsKeyId("keyblade") @@ -141,6 +142,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultModel.getApplicationId()).isEqualTo(APP_ID); assertThat(resultModel.getApplicationArn()).isEqualTo("this-is-an-arn-there-are-many-like-it-but-this-one-is-mine"); assertThat(resultModel.getRoleArn()).isEqualTo("role1"); + assertThat(resultModel.getIdentityCenterApplicationArn()).isEqualTo("arn:aws:sso::123456789012:application/ssoins/apl"); assertThat(resultModel.getCreatedAt()).isEqualTo("2023-10-20T18:02:15Z"); assertThat(resultModel.getUpdatedAt()).isEqualTo("2023-10-20T22:02:15Z"); assertThat(resultModel.getDescription()).isEqualTo("this is a description, there are many like it but this one is mine."); diff --git a/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/UpdateHandlerTest.java b/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/UpdateHandlerTest.java index e24dfe0..a47a958 100644 --- a/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/UpdateHandlerTest.java +++ b/aws-qbusiness-application/src/test/java/software/amazon/qbusiness/application/UpdateHandlerTest.java @@ -92,6 +92,7 @@ public void setup() { .attachmentsControlMode(AttachmentsControlMode.DISABLED.toString()) .build() ) + .identityCenterInstanceArn("arn:aws:sso:::instance/before") .tags(List.of( Tag.builder().key("remain").value("thesame").build(), Tag.builder().key("toremove").value("nolongerthere").build(), @@ -108,6 +109,7 @@ public void setup() { .attachmentsControlMode(AttachmentsControlMode.ENABLED.toString()) .build() ) + .identityCenterInstanceArn("arn:aws:sso:::instance/after") .tags(List.of( Tag.builder().key("remain").value("thesame").build(), Tag.builder().key("iwillchange").value("nowanewvalue").build(), @@ -179,6 +181,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(updateAppRequest.displayName()).isEqualTo("New Phone Who dis"); assertThat(updateAppRequest.description()).isEqualTo("It's a new description"); assertThat(updateAppRequest.roleArn()).isEqualTo("now-better-role"); + assertThat(updateAppRequest.identityCenterInstanceArn()).isEqualTo("arn:aws:sso:::instance/after"); assertThat(updateAppRequest.attachmentsConfiguration()).isEqualTo(software.amazon.awssdk.services.qbusiness.model.AttachmentsConfiguration.builder() .attachmentsControlMode(AttachmentsControlMode.ENABLED) .build()); diff --git a/aws-qbusiness-datasource/aws-qbusiness-datasource.json b/aws-qbusiness-datasource/aws-qbusiness-datasource.json index 05a95b3..81069a4 100755 --- a/aws-qbusiness-datasource/aws-qbusiness-datasource.json +++ b/aws-qbusiness-datasource/aws-qbusiness-datasource.json @@ -346,6 +346,8 @@ } }, "required": [ + "ApplicationId", + "IndexId", "Configuration", "DisplayName" ], diff --git a/aws-qbusiness-datasource/pom.xml b/aws-qbusiness-datasource/pom.xml index f476d2c..3379fcf 100644 --- a/aws-qbusiness-datasource/pom.xml +++ b/aws-qbusiness-datasource/pom.xml @@ -30,13 +30,13 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-index/aws-qbusiness-index.json b/aws-qbusiness-index/aws-qbusiness-index.json index 4a9d4c9..aba7ec1 100755 --- a/aws-qbusiness-index/aws-qbusiness-index.json +++ b/aws-qbusiness-index/aws-qbusiness-index.json @@ -1,7 +1,6 @@ { "typeName": "AWS::QBusiness::Index", "description": "Definition of AWS::QBusiness::Index Resource Type", - "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-qbusiness", "definitions": { "AttributeType": { "type": "string", @@ -17,8 +16,9 @@ "properties": { "Name": { "type": "string", - "maxLength": 2048, - "minLength": 1 + "maxLength": 30, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_-]*$" }, "Type": { "$ref": "#/definitions/AttributeType" @@ -58,6 +58,13 @@ "UPDATING" ] }, + "IndexType": { + "type": "string", + "enum": [ + "ENTERPRISE", + "STARTER" + ] + }, "Status": { "type": "string", "enum": [ @@ -98,10 +105,6 @@ } }, "additionalProperties": false - }, - "Unit": { - "type": "object", - "additionalProperties": false } }, "properties": { @@ -154,6 +157,9 @@ "IndexStatistics": { "$ref": "#/definitions/IndexStatistics" }, + "Type": { + "$ref": "#/definitions/IndexType" + }, "Status": { "$ref": "#/definitions/IndexStatus" }, @@ -172,6 +178,7 @@ } }, "required": [ + "ApplicationId", "DisplayName" ], "readOnlyProperties": [ @@ -183,7 +190,8 @@ "/properties/UpdatedAt" ], "createOnlyProperties": [ - "/properties/ApplicationId" + "/properties/ApplicationId", + "/properties/Type" ], "primaryIdentifier": [ "/properties/ApplicationId", @@ -235,5 +243,9 @@ } } }, + "tagging": { + "taggable": true + }, + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-qbusiness", "additionalProperties": false } diff --git a/aws-qbusiness-index/pom.xml b/aws-qbusiness-index/pom.xml index 1dd34b4..5d14d8b 100644 --- a/aws-qbusiness-index/pom.xml +++ b/aws-qbusiness-index/pom.xml @@ -37,13 +37,13 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-index/src/main/java/software/amazon/qbusiness/index/Translator.java b/aws-qbusiness-index/src/main/java/software/amazon/qbusiness/index/Translator.java index 2261ba3..0c530fe 100644 --- a/aws-qbusiness-index/src/main/java/software/amazon/qbusiness/index/Translator.java +++ b/aws-qbusiness-index/src/main/java/software/amazon/qbusiness/index/Translator.java @@ -43,6 +43,7 @@ static CreateIndexRequest translateToCreateRequest(final String idempotentToken, .displayName(model.getDisplayName()) .applicationId(model.getApplicationId()) .description(model.getDescription()) + .type(model.getType()) .capacityConfiguration(toServiceCapacityConfiguration(model.getCapacityConfiguration())) .tags(TagHelper.serviceTagsFromCfnTags(model.getTags())) .build(); @@ -90,6 +91,7 @@ static ResourceModel translateFromReadResponse(final GetIndexResponse awsRespons .indexStatistics(fromServiceIndexStatistics(awsResponse.indexStatistics())) .status(awsResponse.statusAsString()) .description(awsResponse.description()) + .type(awsResponse.typeAsString()) .documentAttributeConfigurations(fromServiceDocumentAttributeConfigurations(awsResponse.documentAttributeConfigurations())) .createdAt(instantToString(awsResponse.createdAt())) .updatedAt(instantToString(awsResponse.updatedAt())) diff --git a/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/CreateHandlerTest.java b/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/CreateHandlerTest.java index 4acfdf0..d47fa4a 100644 --- a/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/CreateHandlerTest.java +++ b/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/CreateHandlerTest.java @@ -30,10 +30,12 @@ import software.amazon.awssdk.services.qbusiness.QBusinessClient; import software.amazon.awssdk.services.qbusiness.model.AccessDeniedException; +import software.amazon.awssdk.services.qbusiness.model.AttributeType; import software.amazon.awssdk.services.qbusiness.model.ConflictException; import software.amazon.awssdk.services.qbusiness.model.CreateIndexRequest; import software.amazon.awssdk.services.qbusiness.model.CreateIndexResponse; import software.amazon.awssdk.services.qbusiness.model.ErrorDetail; +import software.amazon.awssdk.services.qbusiness.model.IndexType; import software.amazon.awssdk.services.qbusiness.model.QBusinessException; import software.amazon.awssdk.services.qbusiness.model.GetIndexRequest; import software.amazon.awssdk.services.qbusiness.model.GetIndexResponse; @@ -43,7 +45,10 @@ import software.amazon.awssdk.services.qbusiness.model.ListTagsForResourceResponse; import software.amazon.awssdk.services.qbusiness.model.ResourceNotFoundException; import software.amazon.awssdk.services.qbusiness.model.ServiceQuotaExceededException; +import software.amazon.awssdk.services.qbusiness.model.Status; import software.amazon.awssdk.services.qbusiness.model.ThrottlingException; +import software.amazon.awssdk.services.qbusiness.model.UpdateIndexRequest; +import software.amazon.awssdk.services.qbusiness.model.UpdateIndexResponse; import software.amazon.awssdk.services.qbusiness.model.ValidationException; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -96,6 +101,7 @@ public void setup() { .description("A Description") .applicationId(APP_ID) .capacityConfiguration(new IndexCapacityConfiguration(10D)) + .type(IndexType.ENTERPRISE.toString()) .build(); testRequest = ResourceHandlerRequest.builder() @@ -132,6 +138,7 @@ public void handleRequest_SimpleSuccess() { .createdAt(Instant.ofEpochMilli(1697824935000L)) .updatedAt(Instant.ofEpochMilli(1697839335000L)) .status(IndexStatus.ACTIVE) + .type(IndexType.ENTERPRISE) .description(createModel.getDescription()) .displayName(createModel.getDisplayName()) .capacityConfiguration(software.amazon.awssdk.services.qbusiness.model.IndexCapacityConfiguration.builder() @@ -152,6 +159,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(model.getApplicationId()).isEqualTo(createModel.getApplicationId()); assertThat(model.getIndexId()).isEqualTo(createModel.getIndexId()); assertThat(model.getDescription()).isEqualTo(createModel.getDescription()); + assertThat(model.getType()).isEqualTo(createModel.getType()); assertThat(model.getStatus()).isEqualTo(IndexStatus.ACTIVE.toString()); assertThat(model.getCapacityConfiguration().getUnits()).isEqualTo(createModel.getCapacityConfiguration().getUnits()); @@ -162,12 +170,65 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger verify(QBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); } + @Test + public void handleCreateRequestWithDocumentAttributeConfiguration() { + // set up scenario + when(QBusinessClient.createIndex(any(CreateIndexRequest.class))) + .thenReturn(CreateIndexResponse.builder() + .indexId(INDEX_ID) + .build() + ); + when(QBusinessClient.listTagsForResource(any(ListTagsForResourceRequest.class))).thenReturn(ListTagsForResourceResponse.builder() + .tags(List.of()) + .build()); + + var statusResponseBuilder = GetIndexResponse.builder() + .applicationId(APP_ID) + .indexId(INDEX_ID) + .type(IndexType.ENTERPRISE) + .createdAt(Instant.ofEpochMilli(1697824935000L)) + .updatedAt(Instant.ofEpochMilli(1697839335000L)) + .status(IndexStatus.ACTIVE); + + when(QBusinessClient.getIndex(any(GetIndexRequest.class))) + .thenReturn(statusResponseBuilder.status(IndexStatus.ACTIVE).build()) + .thenReturn(statusResponseBuilder.status(IndexStatus.UPDATING).build()) + .thenReturn(statusResponseBuilder.status(IndexStatus.ACTIVE).build()) + .thenReturn(statusResponseBuilder + .status(IndexStatus.ACTIVE) + .type(IndexType.ENTERPRISE) + .description(createModel.getDescription()) + .displayName(createModel.getDisplayName()) + .capacityConfiguration(software.amazon.awssdk.services.qbusiness.model.IndexCapacityConfiguration.builder() + .units(10) + .build()) + .build()); + createModel.setDocumentAttributeConfigurations(List.of( + DocumentAttributeConfiguration.builder() + .name("that-attrib") + .type(AttributeType.STRING.toString()) + .search(Status.DISABLED.toString()) + .build() + )); + + // call method under test + final ProgressEvent resultProgress = underTest.handleRequest( + proxy, testRequest, new CallbackContext(), proxyClient, logger + ); + assertThat(resultProgress).isNotNull(); + assertThat(resultProgress.isSuccess()).isTrue(); + verify(QBusinessClient).createIndex(any(CreateIndexRequest.class)); + verify(QBusinessClient, times(2)).getIndex(any(GetIndexRequest.class)); + verify(QBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); + } + @Test public void handleRequestFromProcessingStateToActive() { // set up scenario var getResponse = GetIndexResponse.builder() .applicationId(APP_ID) .indexId(INDEX_ID) + .type(IndexType.ENTERPRISE) .createdAt(Instant.ofEpochMilli(1697824935000L)) .updatedAt(Instant.ofEpochMilli(1697839335000L)) .description(createModel.getDescription()) @@ -219,6 +280,7 @@ public void testItFailsWithErrorMessageWhenGetReturnsFailStatus() { .thenReturn(GetIndexResponse.builder() .applicationId(APP_ID) .indexId(INDEX_ID) + .type(IndexType.ENTERPRISE) .createdAt(Instant.ofEpochMilli(1697824935000L)) .updatedAt(Instant.ofEpochMilli(1697839335000L)) .status(IndexStatus.FAILED) diff --git a/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/ReadHandlerTest.java b/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/ReadHandlerTest.java index 61038c8..5168e62 100644 --- a/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/ReadHandlerTest.java +++ b/aws-qbusiness-index/src/test/java/software/amazon/qbusiness/index/ReadHandlerTest.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.services.qbusiness.QBusinessClient; import software.amazon.awssdk.services.qbusiness.model.AccessDeniedException; import software.amazon.awssdk.services.qbusiness.model.AttributeType; +import software.amazon.awssdk.services.qbusiness.model.IndexType; import software.amazon.awssdk.services.qbusiness.model.QBusinessException; import software.amazon.awssdk.services.qbusiness.model.GetIndexRequest; import software.amazon.awssdk.services.qbusiness.model.GetIndexResponse; @@ -98,6 +99,7 @@ public void handleRequest_SimpleSuccess() { .thenReturn(GetIndexResponse.builder() .applicationId(APP_ID) .indexId(INDEX_ID) + .type(IndexType.ENTERPRISE) .createdAt(Instant.ofEpochMilli(1697824935000L)) .updatedAt(Instant.ofEpochMilli(1697839335000L)) .description("This is a description of the index.") @@ -144,6 +146,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultModel.getDisplayName()).isEqualTo("IndexName"); assertThat(resultModel.getApplicationId()).isEqualTo(APP_ID); assertThat(resultModel.getIndexId()).isEqualTo(INDEX_ID); + assertThat(resultModel.getType()).isEqualTo(IndexType.ENTERPRISE.toString()); assertThat(resultModel.getCreatedAt()).isEqualTo("2023-10-20T18:02:15Z"); assertThat(resultModel.getUpdatedAt()).isEqualTo("2023-10-20T22:02:15Z"); assertThat(resultModel.getDescription()).isEqualTo("This is a description of the index."); diff --git a/aws-qbusiness-plugin/aws-qbusiness-plugin.json b/aws-qbusiness-plugin/aws-qbusiness-plugin.json index c1efd19..3ace575 100755 --- a/aws-qbusiness-plugin/aws-qbusiness-plugin.json +++ b/aws-qbusiness-plugin/aws-qbusiness-plugin.json @@ -2,6 +2,42 @@ "typeName": "AWS::QBusiness::Plugin", "description": "Definition of AWS::QBusiness::Plugin Resource Type", "definitions": { + "APISchema": { + "oneOf": [ + { + "type": "object", + "title": "Payload", + "properties": { + "Payload": { + "type": "string" + } + }, + "required": [ + "Payload" + ], + "additionalProperties": false + }, + { + "type": "object", + "title": "S3", + "properties": { + "S3": { + "$ref": "#/definitions/S3" + } + }, + "required": [ + "S3" + ], + "additionalProperties": false + } + ] + }, + "APISchemaType": { + "type": "string", + "enum": [ + "OPEN_API_V3" + ] + }, "BasicAuthConfiguration": { "type": "object", "properties": { @@ -24,6 +60,32 @@ ], "additionalProperties": false }, + "CustomPluginConfiguration": { + "type": "object", + "properties": { + "Description": { + "type": "string", + "maxLength": 200, + "minLength": 1 + }, + "ApiSchemaType": { + "$ref": "#/definitions/APISchemaType" + }, + "ApiSchema": { + "$ref": "#/definitions/APISchema" + } + }, + "required": [ + "ApiSchema", + "ApiSchemaType", + "Description" + ], + "additionalProperties": false + }, + "NoAuthConfiguration": { + "type": "object", + "additionalProperties": false + }, "OAuth2ClientCredentialConfiguration": { "type": "object", "properties": { @@ -73,9 +135,34 @@ "OAuth2ClientCredentialConfiguration" ], "additionalProperties": false + }, + { + "type": "object", + "title": "NoAuthConfiguration", + "properties": { + "NoAuthConfiguration": { + "$ref": "#/definitions/NoAuthConfiguration" + } + }, + "required": [ + "NoAuthConfiguration" + ], + "additionalProperties": false } ] }, + "PluginBuildStatus": { + "type": "string", + "enum": [ + "READY", + "CREATE_IN_PROGRESS", + "CREATE_FAILED", + "UPDATE_IN_PROGRESS", + "UPDATE_FAILED", + "DELETE_IN_PROGRESS", + "DELETE_FAILED" + ] + }, "PluginState": { "type": "string", "enum": [ @@ -89,9 +176,31 @@ "SERVICE_NOW", "SALESFORCE", "JIRA", - "ZENDESK" + "ZENDESK", + "CUSTOM" ] }, + "S3": { + "type": "object", + "properties": { + "Bucket": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9][\\.\\-a-z0-9]{1,61}[a-z0-9]$" + }, + "Key": { + "type": "string", + "maxLength": 1024, + "minLength": 1 + } + }, + "required": [ + "Bucket", + "Key" + ], + "additionalProperties": false + }, "Tag": { "type": "object", "properties": { @@ -123,10 +232,16 @@ "AuthConfiguration": { "$ref": "#/definitions/PluginAuthConfiguration" }, + "BuildStatus": { + "$ref": "#/definitions/PluginBuildStatus" + }, "CreatedAt": { "type": "string", "format": "date-time" }, + "CustomPluginConfiguration": { + "$ref": "#/definitions/CustomPluginConfiguration" + }, "DisplayName": { "type": "string", "maxLength": 100, @@ -172,12 +287,13 @@ } }, "required": [ + "ApplicationId", "AuthConfiguration", "DisplayName", - "ServerUrl", "Type" ], "readOnlyProperties": [ + "/properties/BuildStatus", "/properties/CreatedAt", "/properties/PluginArn", "/properties/PluginId", diff --git a/aws-qbusiness-plugin/pom.xml b/aws-qbusiness-plugin/pom.xml index ee3a77b..fecbac4 100644 --- a/aws-qbusiness-plugin/pom.xml +++ b/aws-qbusiness-plugin/pom.xml @@ -43,7 +43,7 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 @@ -90,7 +90,7 @@ software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/AuthConfigHelper.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/AuthConfigHelper.java index 6cada98..d909f9b 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/AuthConfigHelper.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/AuthConfigHelper.java @@ -2,22 +2,32 @@ import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import java.util.Collections; +import java.util.Map; + public class AuthConfigHelper { public static PluginAuthConfiguration convertFromServiceAuthConfig( software.amazon.awssdk.services.qbusiness.model.PluginAuthConfiguration serviceAuthConfig ) { + + final PluginAuthConfiguration.PluginAuthConfigurationBuilder builder = PluginAuthConfiguration.builder(); + if (serviceAuthConfig.oAuth2ClientCredentialConfiguration() != null) { - return software.amazon.qbusiness.plugin.PluginAuthConfiguration.builder() - .oAuth2ClientCredentialConfiguration( - convertFromServiceOAuth(serviceAuthConfig.oAuth2ClientCredentialConfiguration())) - .build(); + builder.oAuth2ClientCredentialConfiguration( + convertFromServiceOAuth(serviceAuthConfig.oAuth2ClientCredentialConfiguration())); } - return software.amazon.qbusiness.plugin.PluginAuthConfiguration.builder() - .basicAuthConfiguration( - convertFromServiceBasicAuth(serviceAuthConfig.basicAuthConfiguration())) - .build(); + if (serviceAuthConfig.basicAuthConfiguration() != null) { + builder.basicAuthConfiguration( + convertFromServiceBasicAuth(serviceAuthConfig.basicAuthConfiguration())); + } + + if (serviceAuthConfig.noAuthConfiguration() != null) { + builder.noAuthConfiguration(convertFromServiceNoAuth(serviceAuthConfig.noAuthConfiguration())); + } + + return builder.build(); } public static software.amazon.awssdk.services.qbusiness.model.PluginAuthConfiguration convertToServiceAuthConfig( @@ -38,6 +48,12 @@ public static software.amazon.awssdk.services.qbusiness.model.PluginAuthConfigur .basicAuthConfiguration(convertToServiceBasicAuth(cfnAuthConfig.getBasicAuthConfiguration())) .build(); } + + if (cfnAuthConfig.getNoAuthConfiguration() != null) { + return software.amazon.awssdk.services.qbusiness.model.PluginAuthConfiguration.builder() + .noAuthConfiguration(convertToServiceNoAuth(cfnAuthConfig.getNoAuthConfiguration())) + .build(); + } throw new CfnGeneralServiceException("Unknown auth configuration"); } @@ -59,6 +75,12 @@ private static OAuth2ClientCredentialConfiguration convertFromServiceOAuth( .build(); } + private static Map convertFromServiceNoAuth( + software.amazon.awssdk.services.qbusiness.model.NoAuthConfiguration serviceNoAuth + ) { + return Collections.emptyMap(); + } + private static software.amazon.awssdk.services.qbusiness.model.BasicAuthConfiguration convertToServiceBasicAuth( BasicAuthConfiguration cfnBasicAuth ) { @@ -77,4 +99,11 @@ private static software.amazon.awssdk.services.qbusiness.model.OAuth2ClientCrede .build(); } + private static software.amazon.awssdk.services.qbusiness.model.NoAuthConfiguration convertToServiceNoAuth( + Map cfnNoAuth + ) { + return software.amazon.awssdk.services.qbusiness.model.NoAuthConfiguration.builder() + .build(); + } + } diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/BaseHandlerStd.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/BaseHandlerStd.java index 63688f7..1276ae5 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/BaseHandlerStd.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/BaseHandlerStd.java @@ -4,6 +4,10 @@ import software.amazon.awssdk.services.qbusiness.QBusinessClient; import software.amazon.awssdk.services.qbusiness.model.AccessDeniedException; import software.amazon.awssdk.services.qbusiness.model.ConflictException; +import software.amazon.awssdk.services.qbusiness.model.GetDataSourceRequest; +import software.amazon.awssdk.services.qbusiness.model.GetDataSourceResponse; +import software.amazon.awssdk.services.qbusiness.model.GetPluginRequest; +import software.amazon.awssdk.services.qbusiness.model.GetPluginResponse; import software.amazon.awssdk.services.qbusiness.model.QBusinessRequest; import software.amazon.awssdk.services.qbusiness.model.ListTagsForResourceRequest; import software.amazon.awssdk.services.qbusiness.model.ListTagsForResourceResponse; @@ -89,5 +93,17 @@ protected ProgressEvent handleError( return ProgressEvent.failed(resourceModel, context, cfnException.getErrorCode(), cfnException.getMessage()); } + protected GetPluginResponse getPlugin(ResourceModel model, ProxyClient proxyClient) { + var request = GetPluginRequest.builder() + .applicationId(model.getApplicationId()) + .pluginId(model.getPluginId()) + .build(); + return callGetPlugin(request, proxyClient); + } + + protected GetPluginResponse callGetPlugin(GetPluginRequest request, ProxyClient proxyClient) { + var client = proxyClient.client(); + return proxyClient.injectCredentialsAndInvokeV2(request, client::getPlugin); + } } diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CreateHandler.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CreateHandler.java index 81abdeb..1cdb9df 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CreateHandler.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CreateHandler.java @@ -1,10 +1,19 @@ package software.amazon.qbusiness.plugin; import software.amazon.awssdk.services.qbusiness.QBusinessClient; +import software.amazon.awssdk.services.qbusiness.model.ApplicationStatus; import software.amazon.awssdk.services.qbusiness.model.CreatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.CreatePluginResponse; +import software.amazon.awssdk.services.qbusiness.model.DataSourceStatus; +import software.amazon.awssdk.services.qbusiness.model.GetApplicationRequest; +import software.amazon.awssdk.services.qbusiness.model.GetApplicationResponse; +import software.amazon.awssdk.services.qbusiness.model.GetDataSourceResponse; +import software.amazon.awssdk.services.qbusiness.model.GetPluginResponse; +import software.amazon.awssdk.services.qbusiness.model.InternalServerException; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginResponse; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -13,6 +22,7 @@ import software.amazon.cloudformation.proxy.delay.Constant; import java.time.Duration; +import java.util.Objects; import static software.amazon.qbusiness.plugin.Constants.API_CREATE_PLUGIN; import static software.amazon.qbusiness.plugin.Constants.API_UPDATE_PLUGIN; @@ -54,6 +64,7 @@ protected ProgressEvent handleRequest( .translateToServiceRequest(model -> Translator.translateToCreateRequest(model, request.getClientRequestToken())) .backoffDelay(backOffStrategy) .makeServiceCall((awsRequest, clientProxyClient) -> callCreatePlugin(awsRequest, clientProxyClient, progress.getResourceModel())) + .stabilize((createReq, createResponse, client, model, context) -> isStabilized(request, client, model, logger)) .handleError((createPluginRequest, error, client, model, context) -> handleError(createPluginRequest, model, error, context, logger, API_CREATE_PLUGIN)) .progress() @@ -91,4 +102,39 @@ private UpdatePluginResponse callUpdatePlugin( ) { return client.injectCredentialsAndInvokeV2(request, client.client()::updatePlugin); } + + private boolean isStabilized( + final ResourceHandlerRequest request, + ProxyClient proxyClient, + ResourceModel model, + Logger logger + ) { + logger.log("[INFO] Checking for Create Complete for Plugin process in stack: %s with ID: %s, For Account: %s, Application: %s" + .formatted(request.getStackId(), model.getPluginId(), request.getAwsAccountId(), model.getApplicationId()) + ); + + GetPluginResponse getPluginRes = getPlugin(model, proxyClient); + var status = getPluginRes.buildStatus(); + + if (PluginBuildStatus.READY.equals(status)) { + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s has stabilized".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + + return true; + } + + if (PluginBuildStatus.CREATE_IN_PROGRESS.equals(status)) { + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s is still stabilizing".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + return false; + } + + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s has failed to stabilize".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + + throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getPluginId(), null); + } } diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CustomPluginConfigHelper.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CustomPluginConfigHelper.java new file mode 100644 index 0000000..5a7f266 --- /dev/null +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/CustomPluginConfigHelper.java @@ -0,0 +1,83 @@ +package software.amazon.qbusiness.plugin; + +import org.apache.commons.lang3.StringUtils; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; + +public class CustomPluginConfigHelper { + + public static CustomPluginConfiguration convertFromServiceCustomPluginConfig( + software.amazon.awssdk.services.qbusiness.model.CustomPluginConfiguration customPluginConfig + ) { + if (customPluginConfig == null) { + return null; + } + + return CustomPluginConfiguration.builder() + .apiSchemaType(customPluginConfig.apiSchemaTypeAsString()) + .apiSchema(convertFromServiceApiSchema(customPluginConfig.apiSchema())) + .description(customPluginConfig.description()) + .build(); + } + + public static software.amazon.awssdk.services.qbusiness.model.CustomPluginConfiguration convertToServiceCustomPluginConfig( + CustomPluginConfiguration customPluginConfig + ) { + if (customPluginConfig == null) { + return null; + } + + return software.amazon.awssdk.services.qbusiness.model.CustomPluginConfiguration.builder() + .apiSchemaType(customPluginConfig.getApiSchemaType()) + .apiSchema(convertToServiceApiSchema(customPluginConfig.getApiSchema())) + .description(customPluginConfig.getDescription()) + .build(); + } + + public static APISchema convertFromServiceApiSchema( + software.amazon.awssdk.services.qbusiness.model.APISchema apiSchema + ) { + if (apiSchema == null) { + return null; + } + + final APISchema.APISchemaBuilder builder = APISchema.builder(); + if (StringUtils.isNotBlank(apiSchema.payload())) { + builder.payload(apiSchema.payload()); + } + + if (apiSchema.s3() != null) { + builder.s3(S3.builder() + .bucket(apiSchema.s3().bucket()) + .key(apiSchema.s3().key()) + .build()); + } + + return builder.build(); + } + + public static software.amazon.awssdk.services.qbusiness.model.APISchema convertToServiceApiSchema( + APISchema apiSchema + ) { + if (apiSchema == null) { + return null; + } + + if (apiSchema.getPayload() != null) { + return software.amazon.awssdk.services.qbusiness.model.APISchema.builder() + .payload(apiSchema.getPayload()) + .build(); + } + + if (apiSchema.getS3() != null) { + return software.amazon.awssdk.services.qbusiness.model.APISchema.builder() + .s3(software.amazon.awssdk.services.qbusiness.model.S3.builder() + .bucket(apiSchema.getS3().getBucket()) + .key(apiSchema.getS3().getKey()) + .build()) + .build(); + } + + throw new CfnGeneralServiceException("Unknown Api Schema"); + } + +} diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/DeleteHandler.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/DeleteHandler.java index 437e277..9070c1d 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/DeleteHandler.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/DeleteHandler.java @@ -3,6 +3,10 @@ import software.amazon.awssdk.services.qbusiness.QBusinessClient; import software.amazon.awssdk.services.qbusiness.model.DeletePluginRequest; import software.amazon.awssdk.services.qbusiness.model.DeletePluginResponse; +import software.amazon.awssdk.services.qbusiness.model.GetPluginResponse; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; +import software.amazon.awssdk.services.qbusiness.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -31,6 +35,7 @@ protected ProgressEvent handleRequest( proxy.initiate("AWS-QBusiness-Retriever::Delete", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) .translateToServiceRequest(Translator::translateToDeleteRequest) .makeServiceCall(this::callDeleteRetriever) + .stabilize((deleteReq, deleteRes, client, model, context) -> isDoneDeleting(client, model)) .handleError((deleteRetrieverRequest, error, client, model, context) -> handleError(deleteRetrieverRequest, model, error, context, logger, API_DELETE_PLUGIN)) .progress() @@ -42,4 +47,28 @@ protected DeletePluginResponse callDeleteRetriever(DeletePluginRequest request, return client.injectCredentialsAndInvokeV2(request, client.client()::deletePlugin); } + private boolean isDoneDeleting( + ProxyClient proxyClient, + ResourceModel model + ) { + try { + GetPluginResponse getPluginRes = getPlugin(model, proxyClient); + var status = getPluginRes.buildStatus(); + if (!PluginBuildStatus.DELETE_FAILED.equals(status)) { + logger.log("[INFO] Delete of %s still stabilizing for Resource id: %s, application: %s" + .formatted(ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId())); + return false; + } else { + logger.log("[INFO] %s with ID: %s, for App: %s, has failed to stabilize".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId() + )); + throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getPluginId(), null); + } + } catch (ResourceNotFoundException e) { + logger.log("[INFO] Delete process of %s has stabilized for Resource id: %s, application: %s" + .formatted(ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId())); + return true; + } + } + } diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/ReadHandler.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/ReadHandler.java index fedac25..78fef99 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/ReadHandler.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/ReadHandler.java @@ -49,9 +49,4 @@ protected ProgressEvent handleRequest( ) ); } - - protected GetPluginResponse callGetPlugin(GetPluginRequest request, ProxyClient client) { - return client.injectCredentialsAndInvokeV2(request, client.client()::getPlugin); - } - } diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/Translator.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/Translator.java index 2daed6a..b9db0e3 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/Translator.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/Translator.java @@ -41,6 +41,7 @@ static CreatePluginRequest translateToCreateRequest(final ResourceModel model, f .type(model.getType()) .serverUrl(model.getServerUrl()) .authConfiguration(AuthConfigHelper.convertToServiceAuthConfig(model.getAuthConfiguration())) + .customPluginConfiguration(CustomPluginConfigHelper.convertToServiceCustomPluginConfig(model.getCustomPluginConfiguration())) .clientToken(idempotenceToken) .tags(TagHelper.serviceTagsFromCfnTags(model.getTags())) .build(); @@ -83,6 +84,8 @@ static ResourceModel translateFromReadResponse(final GetPluginResponse awsRespon .serverUrl(awsResponse.serverUrl()) .authConfiguration(AuthConfigHelper.convertFromServiceAuthConfig(awsResponse.authConfiguration())) .state(awsResponse.stateAsString()) + .buildStatus(awsResponse.buildStatusAsString()) + .customPluginConfiguration(CustomPluginConfigHelper.convertFromServiceCustomPluginConfig(awsResponse.customPluginConfiguration())) .createdAt(instantToString(awsResponse.createdAt())) .updatedAt(instantToString(awsResponse.updatedAt())) .build(); @@ -114,6 +117,7 @@ static UpdatePluginRequest translateToUpdateRequest(final ResourceModel model) { .displayName(model.getDisplayName()) .serverUrl(model.getServerUrl()) .authConfiguration(AuthConfigHelper.convertToServiceAuthConfig(model.getAuthConfiguration())) + .customPluginConfiguration(CustomPluginConfigHelper.convertToServiceCustomPluginConfig(model.getCustomPluginConfiguration())) .state(model.getState()) .build(); } @@ -147,6 +151,7 @@ static List translateFromListResponse(final String applicationId, .type(plugin.type().toString()) .serverUrl(plugin.serverUrl()) .state(plugin.stateAsString()) + .buildStatus(plugin.buildStatusAsString()) .createdAt(instantToString(plugin.createdAt())) .updatedAt(instantToString(plugin.updatedAt())) .build()) diff --git a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/UpdateHandler.java b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/UpdateHandler.java index 88c4efe..51a8290 100644 --- a/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/UpdateHandler.java +++ b/aws-qbusiness-plugin/src/main/java/software/amazon/qbusiness/plugin/UpdateHandler.java @@ -2,12 +2,15 @@ import org.apache.commons.collections.CollectionUtils; import software.amazon.awssdk.services.qbusiness.QBusinessClient; +import software.amazon.awssdk.services.qbusiness.model.GetPluginResponse; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; import software.amazon.awssdk.services.qbusiness.model.TagResourceRequest; import software.amazon.awssdk.services.qbusiness.model.TagResourceResponse; import software.amazon.awssdk.services.qbusiness.model.UntagResourceRequest; import software.amazon.awssdk.services.qbusiness.model.UntagResourceResponse; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginResponse; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -61,6 +64,7 @@ protected ProgressEvent handleRequest( .translateToServiceRequest(Translator::translateToUpdateRequest) .backoffDelay(backOffStrategy) .makeServiceCall(this::callUpdatePlugin) + .stabilize((updateReq, updateResponse, client, model, context) -> isStabilized(request, client, model, logger)) .handleError((describeApplicationRequest, error, client, model, context) -> handleError(describeApplicationRequest, model, error, context, logger, API_UPDATE_PLUGIN)) .progress()) @@ -127,4 +131,39 @@ private UntagResourceResponse callUntagResource(UntagResourceRequest request, Pr return proxyClient.injectCredentialsAndInvokeV2(request, client::untagResource); } + private boolean isStabilized( + final ResourceHandlerRequest request, + ProxyClient proxyClient, + ResourceModel model, + Logger logger + ) { + logger.log("[INFO] Checking for Update Complete for Plugin process in stack: %s with ID: %s, For Account: %s, Application: %s" + .formatted(request.getStackId(), model.getPluginId(), request.getAwsAccountId(), model.getApplicationId()) + ); + + GetPluginResponse getPluginRes = getPlugin(model, proxyClient); + var status = getPluginRes.buildStatus(); + + if (PluginBuildStatus.READY.equals(status)) { + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s has stabilized".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + + return true; + } + + if (PluginBuildStatus.UPDATE_IN_PROGRESS.equals(status)) { + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s is still stabilizing".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + return false; + } + + logger.log("[INFO] %s with ID: %s, for App: %s, stack ID: %s has failed to stabilize".formatted( + ResourceModel.TYPE_NAME, model.getPluginId(), model.getApplicationId(), request.getStackId() + )); + + throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getPluginId(), null); + } + } diff --git a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/CreateHandlerTest.java b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/CreateHandlerTest.java index 1bc9b4c..c4f8693 100644 --- a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/CreateHandlerTest.java +++ b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/CreateHandlerTest.java @@ -1,9 +1,11 @@ package software.amazon.qbusiness.plugin; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -28,6 +30,7 @@ import software.amazon.awssdk.services.qbusiness.model.ConflictException; import software.amazon.awssdk.services.qbusiness.model.CreatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.CreatePluginResponse; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; import software.amazon.awssdk.services.qbusiness.model.QBusinessException; import software.amazon.awssdk.services.qbusiness.model.GetApplicationRequest; import software.amazon.awssdk.services.qbusiness.model.GetDataSourceRequest; @@ -42,6 +45,7 @@ import software.amazon.awssdk.services.qbusiness.model.UpdatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginResponse; import software.amazon.awssdk.services.qbusiness.model.ValidationException; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; @@ -107,6 +111,7 @@ public void setup() { .displayName(PLUGIN_NAME) .type(PLUGIN_TYPE) .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY.toString()) .serverUrl(SERVER_URL) .authConfiguration(serviceAuthConfiguration) .createdAt(Instant.ofEpochMilli(CREATED_TIME).toString()) @@ -147,6 +152,7 @@ public void handleRequest_SimpleSuccess() { .displayName(PLUGIN_NAME) .type(PLUGIN_TYPE) .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) .serverUrl(SERVER_URL) .authConfiguration(cfnAuthConfiguration) .createdAt(Instant.ofEpochMilli(CREATED_TIME)) @@ -164,7 +170,7 @@ public void handleRequest_SimpleSuccess() { final ProgressEvent response = underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); verify(qBusinessClient).createPlugin(any(CreatePluginRequest.class)); - verify(qBusinessClient).getPlugin(any(GetPluginRequest.class)); + verify(qBusinessClient, times(2)).getPlugin(any(GetPluginRequest.class)); verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); verify(qBusinessClient).updatePlugin( argThat( @@ -188,12 +194,116 @@ public void handleRequest_SimpleSuccess() { assertThat(resultModel.getPluginId()).isEqualTo(PLUGIN_ID); assertThat(resultModel.getType()).isEqualTo(PLUGIN_TYPE); assertThat(resultModel.getState()).isEqualTo(PLUGIN_STATE); + assertThat(resultModel.getBuildStatus()).isEqualTo(PluginBuildStatus.READY.toString()); assertThat(resultModel.getDisplayName()).isEqualTo(PLUGIN_NAME); assertThat(resultModel.getAuthConfiguration()).isEqualTo(serviceAuthConfiguration); assertThat(resultModel.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(CREATED_TIME).toString()); assertThat(resultModel.getUpdatedAt()).isEqualTo(Instant.ofEpochMilli(UPDATED_TIME).toString()); } + @Test + public void handleRequest_StabilizeFromCreateInProgressToReady() { + + when(proxyClient.client().createPlugin(any(CreatePluginRequest.class))) + .thenReturn(CreatePluginResponse.builder() + .pluginId(PLUGIN_ID) + .build()); + when(proxyClient.client().getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.CREATE_IN_PROGRESS) + .serverUrl(SERVER_URL) + .authConfiguration(cfnAuthConfiguration) + .createdAt(Instant.ofEpochMilli(CREATED_TIME)) + .updatedAt(Instant.ofEpochMilli(UPDATED_TIME)) + .build()) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(SERVER_URL) + .authConfiguration(cfnAuthConfiguration) + .createdAt(Instant.ofEpochMilli(CREATED_TIME)) + .updatedAt(Instant.ofEpochMilli(UPDATED_TIME)) + .build()); + when(qBusinessClient.updatePlugin(any(UpdatePluginRequest.class))).thenReturn(UpdatePluginResponse.builder().build()); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .tags(List.of(software.amazon.awssdk.services.qbusiness.model.Tag.builder() + .key("Tag 1") + .value("Tag 2") + .build())) + .build()); + + final ProgressEvent response = underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + verify(qBusinessClient).createPlugin(any(CreatePluginRequest.class)); + verify(qBusinessClient, times(3)).getPlugin(any(GetPluginRequest.class)); + verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); + verify(qBusinessClient).updatePlugin( + argThat( + (ArgumentMatcher) t -> t.stateAsString().equals(PLUGIN_STATE) && + t.applicationId().equals(APPLICATION_ID) && + t.pluginId().equals(PLUGIN_ID) + ) + ); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + ResourceModel resultModel = response.getResourceModel(); + + assertThat(resultModel.getApplicationId()).isEqualTo(APPLICATION_ID); + assertThat(resultModel.getPluginId()).isEqualTo(PLUGIN_ID); + assertThat(resultModel.getType()).isEqualTo(PLUGIN_TYPE); + assertThat(resultModel.getState()).isEqualTo(PLUGIN_STATE); + assertThat(resultModel.getBuildStatus()).isEqualTo(PluginBuildStatus.READY.toString()); + assertThat(resultModel.getDisplayName()).isEqualTo(PLUGIN_NAME); + assertThat(resultModel.getAuthConfiguration()).isEqualTo(serviceAuthConfiguration); + assertThat(resultModel.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(CREATED_TIME).toString()); + assertThat(resultModel.getUpdatedAt()).isEqualTo(Instant.ofEpochMilli(UPDATED_TIME).toString()); + } + + @Test + public void handleRequest_ThrowsExpectedErrorWhenStabilizationFails() { + + when(proxyClient.client().createPlugin(any(CreatePluginRequest.class))) + .thenReturn(CreatePluginResponse.builder() + .pluginId(PLUGIN_ID) + .build()); + when(proxyClient.client().getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.CREATE_FAILED) + .serverUrl(SERVER_URL) + .authConfiguration(cfnAuthConfiguration) + .createdAt(Instant.ofEpochMilli(CREATED_TIME)) + .updatedAt(Instant.ofEpochMilli(UPDATED_TIME)) + .build()); + + assertThatThrownBy(() -> underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnNotStabilizedException.class); + + verify(qBusinessClient).createPlugin(any(CreatePluginRequest.class)); + verify(qBusinessClient).getPlugin(any(GetPluginRequest.class)); + } + @Test public void testThatItDoesNotCallUpdatePluginIfStateIsNotSet() { // set up @@ -210,6 +320,7 @@ public void testThatItDoesNotCallUpdatePluginIfStateIsNotSet() { .displayName(PLUGIN_NAME) .type(PLUGIN_TYPE) .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) .serverUrl(SERVER_URL) .authConfiguration(cfnAuthConfiguration) .createdAt(Instant.ofEpochMilli(CREATED_TIME)) @@ -225,7 +336,7 @@ public void testThatItDoesNotCallUpdatePluginIfStateIsNotSet() { // verify results verify(qBusinessClient).createPlugin(any(CreatePluginRequest.class)); - verify(qBusinessClient).getPlugin(any(GetPluginRequest.class)); + verify(qBusinessClient, times(2)).getPlugin(any(GetPluginRequest.class)); verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); assertThat(response).isNotNull(); diff --git a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/DeleteHandlerTest.java b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/DeleteHandlerTest.java index 799ed2b..eb4b1ef 100644 --- a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/DeleteHandlerTest.java +++ b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/DeleteHandlerTest.java @@ -1,9 +1,11 @@ package software.amazon.qbusiness.plugin; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -26,11 +28,16 @@ import software.amazon.awssdk.services.qbusiness.model.ConflictException; import software.amazon.awssdk.services.qbusiness.model.DeletePluginRequest; import software.amazon.awssdk.services.qbusiness.model.DeletePluginResponse; +import software.amazon.awssdk.services.qbusiness.model.GetDataSourceRequest; +import software.amazon.awssdk.services.qbusiness.model.GetPluginRequest; +import software.amazon.awssdk.services.qbusiness.model.GetPluginResponse; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; import software.amazon.awssdk.services.qbusiness.model.QBusinessException; import software.amazon.awssdk.services.qbusiness.model.InternalServerException; import software.amazon.awssdk.services.qbusiness.model.ResourceNotFoundException; import software.amazon.awssdk.services.qbusiness.model.ThrottlingException; import software.amazon.awssdk.services.qbusiness.model.ValidationException; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; @@ -93,10 +100,12 @@ public void handleRequest_SimpleSuccess() { when(proxyClient.client().deletePlugin(any(DeletePluginRequest.class))) .thenReturn(DeletePluginResponse.builder().build()); + when(proxyClient.client().getPlugin(any(GetPluginRequest.class))).thenThrow(ResourceNotFoundException.builder().build()); final ProgressEvent response = underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); verify(qBusinessClient).deletePlugin(any(DeletePluginRequest.class)); + verify(qBusinessClient).getPlugin(any(GetPluginRequest.class)); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); @@ -114,6 +123,44 @@ public void handleRequest_SimpleSuccess() { } + @Test + public void handleRequest_StabilizeFromDeleteInProgressToDeleted() { + + when(proxyClient.client().deletePlugin(any(DeletePluginRequest.class))) + .thenReturn(DeletePluginResponse.builder().build()); + when(proxyClient.client().getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .buildStatus(PluginBuildStatus.DELETE_IN_PROGRESS) + .build()) + .thenThrow(ResourceNotFoundException.builder().build()); + + final ProgressEvent response = underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + verify(qBusinessClient).deletePlugin(any(DeletePluginRequest.class)); + verify(qBusinessClient, times(2)).getPlugin(any(GetPluginRequest.class)); + } + + @Test + public void handleRequest_ThrowsExpectedErrorWhenStabilizationFails() { + + when(proxyClient.client().deletePlugin(any(DeletePluginRequest.class))) + .thenReturn(DeletePluginResponse.builder().build()); + when(proxyClient.client().getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .buildStatus(PluginBuildStatus.DELETE_FAILED) + .build()); + + assertThatThrownBy(() -> underTest.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnNotStabilizedException.class);; + + verify(qBusinessClient).deletePlugin(any(DeletePluginRequest.class)); + verify(qBusinessClient, times(1)).getPlugin(any(GetPluginRequest.class)); + } + private static Stream serviceErrorAndHandlerCodes() { return Stream.of( Arguments.of(ValidationException.builder().build(), HandlerErrorCode.InvalidRequest), diff --git a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/UpdateHandlerTest.java b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/UpdateHandlerTest.java index 54179fa..22ff2e7 100644 --- a/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/UpdateHandlerTest.java +++ b/aws-qbusiness-plugin/src/test/java/software/amazon/qbusiness/plugin/UpdateHandlerTest.java @@ -1,6 +1,7 @@ package software.amazon.qbusiness.plugin; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; @@ -36,6 +37,7 @@ import software.amazon.awssdk.services.qbusiness.model.InternalServerException; import software.amazon.awssdk.services.qbusiness.model.ListTagsForResourceRequest; import software.amazon.awssdk.services.qbusiness.model.ListTagsForResourceResponse; +import software.amazon.awssdk.services.qbusiness.model.PluginBuildStatus; import software.amazon.awssdk.services.qbusiness.model.ResourceNotFoundException; import software.amazon.awssdk.services.qbusiness.model.ServiceQuotaExceededException; import software.amazon.awssdk.services.qbusiness.model.TagResourceRequest; @@ -46,6 +48,7 @@ import software.amazon.awssdk.services.qbusiness.model.UpdatePluginRequest; import software.amazon.awssdk.services.qbusiness.model.UpdatePluginResponse; import software.amazon.awssdk.services.qbusiness.model.ValidationException; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -136,6 +139,7 @@ public void setup() { .displayName(PLUGIN_NAME) .type(PLUGIN_TYPE) .state(PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY.toString()) .serverUrl(SERVER_URL) .authConfiguration(serviceAuthConfiguration) .tags(List.of( @@ -151,6 +155,7 @@ public void setup() { .displayName(UPDATED_PLUGIN_NAME) .type(PLUGIN_TYPE) .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY.toString()) .serverUrl(UPDATED_SERVER_URL) .authConfiguration(updatedServiceAuthConfiguration) .tags(List.of( @@ -188,16 +193,6 @@ public void setup() { when(proxyClient.client().updatePlugin(any(UpdatePluginRequest.class))) .thenReturn(UpdatePluginResponse.builder() .build()); - when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) - .thenReturn(GetPluginResponse.builder() - .applicationId(APPLICATION_ID) - .pluginId(PLUGIN_ID) - .displayName(UPDATED_PLUGIN_NAME) - .type(PLUGIN_TYPE) - .state(UPDATED_PLUGIN_STATE) - .serverUrl(UPDATED_SERVER_URL) - .authConfiguration(updatedCfnAuthConfiguration) - .build()); when(qBusinessClient.listTagsForResource(any(ListTagsForResourceRequest.class))) .thenReturn(ListTagsForResourceResponse.builder().tags(List.of()).build()); @@ -212,7 +207,17 @@ public void tear_down() throws Exception { @Test public void handleRequest_SimpleSuccess() { - + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); when(qBusinessClient.tagResource(any(TagResourceRequest.class))) .thenReturn(TagResourceResponse.builder().build()); when(qBusinessClient.untagResource(any(UntagResourceRequest.class))) @@ -237,7 +242,7 @@ proxy, request, new CallbackContext(), proxyClient, logger assertThat(resultProgress.getResourceModel().getState()).isEqualTo(UPDATED_PLUGIN_STATE); assertThat(resultProgress.getResourceModel().getServerUrl()).isEqualTo(UPDATED_SERVER_URL); - verify(qBusinessClient).getPlugin( + verify(qBusinessClient, times(2)).getPlugin( argThat((ArgumentMatcher) t -> t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) ) @@ -266,6 +271,109 @@ proxy, request, new CallbackContext(), proxyClient, logger )); } + @Test + public void handleRequest_StabilizeFromUpdateInProgressToReady() { + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.UPDATE_IN_PROGRESS) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); + + when(qBusinessClient.tagResource(any(TagResourceRequest.class))) + .thenReturn(TagResourceResponse.builder().build()); + when(qBusinessClient.untagResource(any(UntagResourceRequest.class))) + .thenReturn(UntagResourceResponse.builder().build()); + final ProgressEvent resultProgress = underTest.handleRequest( + proxy, request, new CallbackContext(), proxyClient, logger + ); + + assertThat(resultProgress).isNotNull(); + assertThat(resultProgress.isSuccess()).isTrue(); + ArgumentCaptor updatePluginReqCaptor = ArgumentCaptor.forClass(UpdatePluginRequest.class); + verify(qBusinessClient).updatePlugin(updatePluginReqCaptor.capture()); + assertThat(resultProgress.getResourceModel().getApplicationId()).isEqualTo(APPLICATION_ID); + assertThat(resultProgress.getResourceModel().getPluginId()).isEqualTo(PLUGIN_ID); + assertThat(resultProgress.getResourceModel().getAuthConfiguration().getBasicAuthConfiguration().getRoleArn()) + .isEqualTo(AuthConfigHelper.convertToServiceAuthConfig(updatedModel.getAuthConfiguration()) + .basicAuthConfiguration().roleArn()); + assertThat(resultProgress.getResourceModel().getAuthConfiguration().getBasicAuthConfiguration().getSecretArn()) + .isEqualTo(AuthConfigHelper.convertToServiceAuthConfig(updatedModel.getAuthConfiguration()) + .basicAuthConfiguration().secretArn()); + assertThat(resultProgress.getResourceModel().getDisplayName()).isEqualTo(UPDATED_PLUGIN_NAME); + assertThat(resultProgress.getResourceModel().getState()).isEqualTo(UPDATED_PLUGIN_STATE); + assertThat(resultProgress.getResourceModel().getServerUrl()).isEqualTo(UPDATED_SERVER_URL); + + verify(qBusinessClient, times(3)).getPlugin( + argThat((ArgumentMatcher) t -> + t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) + ) + ); + verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); + + var tagReqCaptor = ArgumentCaptor.forClass(TagResourceRequest.class); + var untagReqCaptor = ArgumentCaptor.forClass(UntagResourceRequest.class); + verify(qBusinessClient).tagResource(tagReqCaptor.capture()); + verify(qBusinessClient).untagResource(untagReqCaptor.capture()); + + var tagResourceRequest = tagReqCaptor.getValue(); + Map tagsInTagResourceReq = tagResourceRequest.tags().stream() + .collect(Collectors.toMap(tag -> tag.key(), tag -> tag.value())); + assertThat(tagsInTagResourceReq).containsOnly( + Map.entry("iwillchange", "nowanewvalue"), + Map.entry("iamnew", "overhere"), + Map.entry("stackchange", "whatwhenwhere"), + Map.entry("stacknewaddition", "newvalue") + ); + + var untagResourceReq = untagReqCaptor.getValue(); + assertThat(untagResourceReq.tagKeys()).containsOnlyOnceElementsOf(List.of( + "toremove", + "stack-i-remove" + )); + } + + @Test + public void handleRequest_ThrowsExpectedErrorWhenStabilizationFails() { + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.UPDATE_FAILED) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); + + assertThatThrownBy(() -> underTest.handleRequest( + proxy, request, new CallbackContext(), proxyClient, logger + )).isInstanceOf(CfnNotStabilizedException.class); + + verify(qBusinessClient).updatePlugin(any(UpdatePluginRequest.class)); + verify(qBusinessClient, times(1)).getPlugin( + argThat((ArgumentMatcher) t -> + t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) + ) + ); + } + private static Stream serviceErrorAndHandlerCodes() { return Stream.of( Arguments.of(ValidationException.builder().build(), HandlerErrorCode.InvalidRequest), @@ -280,6 +388,17 @@ private static Stream serviceErrorAndHandlerCodes() { @Test public void testThatItDoesntTagAndUnTag() { + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); when(qBusinessClient.tagResource(any(TagResourceRequest.class))) .thenReturn(TagResourceResponse.builder().build()); request.setPreviousResourceTags(Map.of( @@ -303,7 +422,7 @@ proxy, request, new CallbackContext(), proxyClient, logger assertThat(resultProgress.isSuccess()).isTrue(); verify(qBusinessClient).updatePlugin(any(UpdatePluginRequest.class)); - verify(qBusinessClient).getPlugin( + verify(qBusinessClient, times(2)).getPlugin( argThat((ArgumentMatcher) t -> t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) ) @@ -325,6 +444,17 @@ proxy, request, new CallbackContext(), proxyClient, logger @Test public void testThatItCallsUnTagButSkipsCallingTag() { + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); when(qBusinessClient.untagResource(any(UntagResourceRequest.class))) .thenReturn(UntagResourceResponse.builder().build()); model.setTags(List.of( @@ -350,7 +480,7 @@ proxy, request, new CallbackContext(), proxyClient, logger assertThat(resultProgress).isNotNull(); assertThat(resultProgress.isSuccess()).isTrue(); verify(qBusinessClient).updatePlugin(any(UpdatePluginRequest.class)); - verify(qBusinessClient).getPlugin( + verify(qBusinessClient, times(2)).getPlugin( argThat((ArgumentMatcher) t -> t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) ) @@ -398,6 +528,17 @@ public void testThatItDoesntTagAndUnTagWhenTagFieldsAreNull( Map reqTags, Map sysTags ) { + when(qBusinessClient.getPlugin(any(GetPluginRequest.class))) + .thenReturn(GetPluginResponse.builder() + .applicationId(APPLICATION_ID) + .pluginId(PLUGIN_ID) + .displayName(UPDATED_PLUGIN_NAME) + .type(PLUGIN_TYPE) + .state(UPDATED_PLUGIN_STATE) + .buildStatus(PluginBuildStatus.READY) + .serverUrl(UPDATED_SERVER_URL) + .authConfiguration(updatedCfnAuthConfiguration) + .build()); // set up test scenario model.setTags(modelTags); updatedModel.setTags(modelTags); @@ -415,7 +556,7 @@ proxy, request, new CallbackContext(), proxyClient, logger assertThat(resultProgress).isNotNull(); assertThat(resultProgress.isSuccess()).isTrue(); verify(qBusinessClient).updatePlugin(any(UpdatePluginRequest.class)); - verify(qBusinessClient).getPlugin( + verify(qBusinessClient, times(2)).getPlugin( argThat((ArgumentMatcher) t -> t.applicationId().equals(APPLICATION_ID) && t.pluginId().equals(PLUGIN_ID) ) diff --git a/aws-qbusiness-retriever/aws-qbusiness-retriever.json b/aws-qbusiness-retriever/aws-qbusiness-retriever.json index d62ffe8..faeafb4 100755 --- a/aws-qbusiness-retriever/aws-qbusiness-retriever.json +++ b/aws-qbusiness-retriever/aws-qbusiness-retriever.json @@ -157,6 +157,7 @@ } }, "required": [ + "ApplicationId", "Configuration", "DisplayName", "Type" diff --git a/aws-qbusiness-retriever/pom.xml b/aws-qbusiness-retriever/pom.xml index 57c78f7..0ed2e56 100644 --- a/aws-qbusiness-retriever/pom.xml +++ b/aws-qbusiness-retriever/pom.xml @@ -37,13 +37,13 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-webexperience/aws-qbusiness-webexperience.json b/aws-qbusiness-webexperience/aws-qbusiness-webexperience.json index 31a675b..7a5d244 100755 --- a/aws-qbusiness-webexperience/aws-qbusiness-webexperience.json +++ b/aws-qbusiness-webexperience/aws-qbusiness-webexperience.json @@ -1,41 +1,7 @@ { "typeName": "AWS::QBusiness::WebExperience", "description": "Definition of AWS::QBusiness::WebExperience Resource Type", - "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-qbusiness", "definitions": { - "SamlConfiguration": { - "type": "object", - "properties": { - "MetadataXML": { - "type": "string", - "maxLength": 10000000, - "minLength": 1000, - "pattern": "^.*$" - }, - "RoleArn": { - "type": "string", - "maxLength": 1284, - "minLength": 0, - "pattern": "^arn:[a-z0-9-\\.]{1,63}:[a-z0-9-\\.]{0,63}:[a-z0-9-\\.]{0,63}:[a-z0-9-\\.]{0,63}:[^/].{0,1023}$" - }, - "UserIdAttribute": { - "type": "string", - "maxLength": 256, - "minLength": 1 - }, - "UserGroupAttribute": { - "type": "string", - "maxLength": 256, - "minLength": 1 - } - }, - "required": [ - "MetadataXML", - "RoleArn", - "UserIdAttribute" - ], - "additionalProperties": false - }, "Tag": { "type": "object", "properties": { @@ -56,23 +22,6 @@ ], "additionalProperties": false }, - "WebExperienceAuthConfiguration": { - "oneOf": [ - { - "type": "object", - "title": "SamlConfiguration", - "properties": { - "SamlConfiguration": { - "$ref": "#/definitions/SamlConfiguration" - } - }, - "required": [ - "SamlConfiguration" - ], - "additionalProperties": false - } - ] - }, "WebExperienceSamplePromptsControlMode": { "type": "string", "enum": [ @@ -98,9 +47,6 @@ "minLength": 36, "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-]{35}$" }, - "AuthenticationConfiguration": { - "$ref": "#/definitions/WebExperienceAuthConfiguration" - }, "CreatedAt": { "type": "string", "format": "date-time" @@ -111,6 +57,12 @@ "minLength": 1, "pattern": "^(https?|ftp|file)://([^\\s]*)$" }, + "RoleArn": { + "type": "string", + "maxLength": 1284, + "minLength": 0, + "pattern": "^arn:[a-z0-9-\\.]{1,63}:[a-z0-9-\\.]{0,63}:[a-z0-9-\\.]{0,63}:[a-z0-9-\\.]{0,63}:[^/].{0,1023}$" + }, "SamplePromptsControlMode": { "$ref": "#/definitions/WebExperienceSamplePromptsControlMode" }, @@ -160,6 +112,9 @@ "minLength": 0 } }, + "required": [ + "ApplicationId" + ], "readOnlyProperties": [ "/properties/CreatedAt", "/properties/DefaultEndpoint", @@ -180,10 +135,11 @@ "permissions": [ "iam:PassRole", "qbusiness:CreateWebExperience", - "qbusiness:UpdateWebExperience", "qbusiness:GetWebExperience", "qbusiness:ListTagsForResource", - "qbusiness:TagResource" + "qbusiness:TagResource", + "sso:PutApplicationGrant", + "sso:UpdateApplication" ] }, "read": { @@ -199,7 +155,9 @@ "qbusiness:ListTagsForResource", "qbusiness:TagResource", "qbusiness:UntagResource", - "qbusiness:UpdateWebExperience" + "qbusiness:UpdateWebExperience", + "sso:PutApplicationGrant", + "sso:UpdateApplication" ] }, "delete": { @@ -227,5 +185,6 @@ "tagging": { "taggable": true }, + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-qbusiness", "additionalProperties": false } diff --git a/aws-qbusiness-webexperience/pom.xml b/aws-qbusiness-webexperience/pom.xml index 5f1dcb1..719fbbf 100644 --- a/aws-qbusiness-webexperience/pom.xml +++ b/aws-qbusiness-webexperience/pom.xml @@ -37,13 +37,13 @@ software.amazon.awssdk qbusiness - 2.23.12 + 2.25.42 software.amazon.awssdk sdk-core - 2.23.12 + 2.25.42 diff --git a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/CreateHandler.java b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/CreateHandler.java index c663126..f666597 100644 --- a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/CreateHandler.java +++ b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/CreateHandler.java @@ -5,8 +5,6 @@ import software.amazon.awssdk.services.qbusiness.model.CreateWebExperienceResponse; import software.amazon.awssdk.services.qbusiness.model.ErrorDetail; import software.amazon.awssdk.services.qbusiness.model.GetWebExperienceResponse; -import software.amazon.awssdk.services.qbusiness.model.UpdateWebExperienceRequest; -import software.amazon.awssdk.services.qbusiness.model.UpdateWebExperienceResponse; import software.amazon.awssdk.services.qbusiness.model.WebExperienceStatus; import software.amazon.awssdk.utils.StringUtils; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; @@ -21,7 +19,6 @@ import java.util.Objects; import static software.amazon.qbusiness.webexperience.Constants.API_CREATE_WEB_EXPERIENCE; -import static software.amazon.qbusiness.webexperience.Constants.API_UPDATE_WEB_EXPERIENCE; public class CreateHandler extends BaseHandlerStd { @@ -60,29 +57,11 @@ protected ProgressEvent handleRequest( .backoffDelay(backOffStrategy) .makeServiceCall((awsRequest, clientProxyClient) -> callCreateWebExperience(awsRequest, clientProxyClient, progress.getResourceModel())) - .stabilize((awsReq, response, clientProxyClient, model, context) -> isCreateStabilized(clientProxyClient, model, logger)) + .stabilize((awsReq, response, clientProxyClient, model, context) -> isStabilized(clientProxyClient, model, logger)) .handleError((createReq, error, client, model, context) -> handleError(createReq, model, error, context, logger, API_CREATE_WEB_EXPERIENCE)) .progress() ) - .then(progress -> { - // if authentication configuration was not set, let's short circuit - if (request.getDesiredResourceState().getAuthenticationConfiguration() == null) { - return readHandler(proxy, request, callbackContext, proxyClient); - } - - // customer has set an authentication configuration, let's set up a call to Update. - return proxy.initiate("AWS-QBusiness-WebExperience::PostCreateUpdate", - proxyClient, progress.getResourceModel(), progress.getCallbackContext() - ) - .translateToServiceRequest(Translator::translateToPostCreateUpdateRequest) - .backoffDelay(backOffStrategy) - .makeServiceCall(this::callUpdateWebExperience) - .stabilize((awsReq, response, clientProxyClient, model, context) -> isUpdateStabilized(clientProxyClient, model, logger)) - .handleError((createReq, error, client, model, context) -> - handleError(createReq, model, error, context, logger, API_UPDATE_WEB_EXPERIENCE)) - .progress(); - }) .then(progress -> readHandler(proxy, request, callbackContext, proxyClient)); } @@ -94,56 +73,32 @@ private ProgressEvent readHandler( return new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger); } - private boolean isCreateStabilized( + private boolean isStabilized( final ProxyClient proxyClient, final ResourceModel model, final Logger logger) { final GetWebExperienceResponse getWebExperienceResponse = getWebExperience(model, proxyClient, logger); final String status = getWebExperienceResponse.statusAsString(); + final String roleArn = getWebExperienceResponse.roleArn(); - // CreateWebExperience does not take AuthenticationConfiguration. In this case, the status becomes - // PENDING_AUTH_CONFIG. See https://tiny.amazon.com/1geblgev - if (WebExperienceStatus.PENDING_AUTH_CONFIG.toString().equals(status)) { + if (WebExperienceStatus.ACTIVE.toString().equals(status)) { logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s has stabilized for create operation" - .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); + .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); return true; } - if (!WebExperienceStatus.FAILED.toString().equals(status)) { - logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s is still stabilizing for create operation." + // If RoleArn is not passed, the status becomes + // PENDING_AUTH_CONFIG. See https://tiny.amazon.com/4i460dfb + if (roleArn == null && WebExperienceStatus.PENDING_AUTH_CONFIG.toString().equals(status)) { + logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s has stabilized for create operation" .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); - return false; - } - - // handle failed state - - RuntimeException causeMessage = null; - ErrorDetail error = getWebExperienceResponse.error(); - if (Objects.nonNull(error) && StringUtils.isNotBlank(error.errorMessage())) { - causeMessage = new RuntimeException(error.errorMessage()); - } - - throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getPrimaryIdentifier().toString(), causeMessage); - } - - private boolean isUpdateStabilized( - final ProxyClient proxyClient, - final ResourceModel model, - final Logger logger) { - final GetWebExperienceResponse getWebExperienceResponse = getWebExperience(model, proxyClient, logger); - - final String status = getWebExperienceResponse.statusAsString(); - - if (WebExperienceStatus.ACTIVE.toString().equals(status)) { - logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s has stabilized for update operation" - .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); return true; } if (!WebExperienceStatus.FAILED.toString().equals(status)) { - logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s is still stabilizing for update operation." - .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); + logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s is still stabilizing for create operation." + .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); return false; } @@ -158,7 +113,6 @@ private boolean isUpdateStabilized( throw new CfnNotStabilizedException(ResourceModel.TYPE_NAME, model.getPrimaryIdentifier().toString(), causeMessage); } - private CreateWebExperienceResponse callCreateWebExperience( final CreateWebExperienceRequest request, final ProxyClient proxyClient, @@ -167,11 +121,4 @@ private CreateWebExperienceResponse callCreateWebExperience( model.setWebExperienceId(response.webExperienceId()); return response; } - - private UpdateWebExperienceResponse callUpdateWebExperience( - UpdateWebExperienceRequest updateReq, - final ProxyClient proxyClient - ) { - return proxyClient.injectCredentialsAndInvokeV2(updateReq, proxyClient.client()::updateWebExperience); - } } diff --git a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/Translator.java b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/Translator.java index cad5d32..200d8b1 100644 --- a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/Translator.java +++ b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/Translator.java @@ -43,6 +43,7 @@ static CreateWebExperienceRequest translateToCreateRequest(final String idempote return CreateWebExperienceRequest.builder() .clientToken(idempotentToken) .applicationId(model.getApplicationId()) + .roleArn(model.getRoleArn()) .title(model.getTitle()) .subtitle(model.getSubtitle()) .welcomeMessage(model.getWelcomeMessage()) @@ -50,14 +51,6 @@ static CreateWebExperienceRequest translateToCreateRequest(final String idempote .build(); } - static UpdateWebExperienceRequest translateToPostCreateUpdateRequest(final ResourceModel model) { - return UpdateWebExperienceRequest.builder() - .applicationId(model.getApplicationId()) - .webExperienceId(model.getWebExperienceId()) - .authenticationConfiguration(toServiceAuthenticationConfiguration(model.getAuthenticationConfiguration())) - .build(); - } - /** * Request to read a resource * @@ -102,7 +95,7 @@ static ResourceModel translateFromReadResponse(final GetWebExperienceResponse aw .subtitle(awsResponse.subtitle()) .welcomeMessage(awsResponse.welcomeMessage()) .samplePromptsControlMode(awsResponse.samplePromptsControlModeAsString()) - .authenticationConfiguration(fromServiceAuthenticationConfiguration(awsResponse.authenticationConfiguration())) + .roleArn(awsResponse.roleArn()) .defaultEndpoint(awsResponse.defaultEndpoint()) .createdAt(instantToString(awsResponse.createdAt())) .updatedAt(instantToString(awsResponse.updatedAt())) @@ -151,7 +144,7 @@ static UpdateWebExperienceRequest translateToUpdateRequest(final ResourceModel m .webExperienceId(model.getWebExperienceId()) .title(model.getTitle()) .subtitle(model.getSubtitle()) - .authenticationConfiguration(toServiceAuthenticationConfiguration(model.getAuthenticationConfiguration())) + .roleArn(model.getRoleArn()) .build(); } @@ -274,53 +267,6 @@ static UntagResourceRequest untagResourceRequest( .build(); } - - private static WebExperienceAuthConfiguration fromServiceAuthenticationConfiguration( - final software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration webExperienceAuthConfiguration) { - if (Objects.isNull(webExperienceAuthConfiguration)) { - return null; - } - - return WebExperienceAuthConfiguration.builder() - .samlConfiguration(fromServiceWebExperienceAuthConfiguration(webExperienceAuthConfiguration.samlConfiguration())) - .build(); - } - - private static SamlConfiguration fromServiceWebExperienceAuthConfiguration( - final software.amazon.awssdk.services.qbusiness.model.SamlConfiguration samlConfigurationOptions) { - return SamlConfiguration.builder() - .metadataXML(samlConfigurationOptions.metadataXML()) - .roleArn(samlConfigurationOptions.roleArn()) - .userIdAttribute(samlConfigurationOptions.userIdAttribute()) - .userGroupAttribute(samlConfigurationOptions.userGroupAttribute()) - .build(); - } - - private static software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration - toServiceAuthenticationConfiguration(final WebExperienceAuthConfiguration authenticationConfiguration) { - if (authenticationConfiguration == null || authenticationConfiguration.getSamlConfiguration() == null) { - return null; - } - - return software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(toServiceSamlConfigurationOptions(authenticationConfiguration.getSamlConfiguration())) - .build(); - } - - private static software.amazon.awssdk.services.qbusiness.model.SamlConfiguration - toServiceSamlConfigurationOptions(final SamlConfiguration samlConfigurationOptions) { - if (samlConfigurationOptions == null) { - return null; - } - - return software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML(samlConfigurationOptions.getMetadataXML()) - .roleArn(samlConfigurationOptions.getRoleArn()) - .userIdAttribute(samlConfigurationOptions.getUserIdAttribute()) - .userGroupAttribute(samlConfigurationOptions.getUserGroupAttribute()) - .build(); - } - private static String instantToString(Instant instant) { return Optional.ofNullable(instant) .map(Instant::toString) diff --git a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/UpdateHandler.java b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/UpdateHandler.java index 89fae60..b29eb49 100644 --- a/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/UpdateHandler.java +++ b/aws-qbusiness-webexperience/src/main/java/software/amazon/qbusiness/webexperience/UpdateHandler.java @@ -10,7 +10,6 @@ import software.amazon.awssdk.services.qbusiness.model.UntagResourceResponse; import software.amazon.awssdk.services.qbusiness.model.UpdateWebExperienceRequest; import software.amazon.awssdk.services.qbusiness.model.UpdateWebExperienceResponse; -import software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration; import software.amazon.awssdk.services.qbusiness.model.WebExperienceStatus; import software.amazon.awssdk.utils.StringUtils; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; @@ -136,7 +135,7 @@ private boolean isStabilized( final ResourceModel model) { final GetWebExperienceResponse getWebExperienceResponse = getWebExperience(model, proxyClient, logger); final WebExperienceStatus status = getWebExperienceResponse.status(); - final WebExperienceAuthConfiguration authConfiguration = getWebExperienceResponse.authenticationConfiguration(); + final String roleArn = getWebExperienceResponse.roleArn(); if (WebExperienceStatus.ACTIVE.equals(status)) { logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s has stabilized." @@ -144,7 +143,7 @@ private boolean isStabilized( return true; } - if (authConfiguration == null && WebExperienceStatus.PENDING_AUTH_CONFIG.equals(status)) { + if (roleArn == null && WebExperienceStatus.PENDING_AUTH_CONFIG.equals(status)) { logger.log("[INFO] %s with ApplicationId: %s and WebExperienceId: %s has stabilized." .formatted(ResourceModel.TYPE_NAME, model.getApplicationId(), model.getWebExperienceId())); return true; diff --git a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/CreateHandlerTest.java b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/CreateHandlerTest.java index ade004a..f0df558 100644 --- a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/CreateHandlerTest.java +++ b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/CreateHandlerTest.java @@ -41,7 +41,6 @@ import software.amazon.awssdk.services.qbusiness.model.ResourceNotFoundException; import software.amazon.awssdk.services.qbusiness.model.ServiceQuotaExceededException; import software.amazon.awssdk.services.qbusiness.model.ThrottlingException; -import software.amazon.awssdk.services.qbusiness.model.UpdateWebExperienceRequest; import software.amazon.awssdk.services.qbusiness.model.ValidationException; import software.amazon.awssdk.services.qbusiness.model.WebExperienceStatus; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; @@ -89,14 +88,7 @@ public void setup() { .applicationId(APP_ID) .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") - .authenticationConfiguration(WebExperienceAuthConfiguration.builder() - .samlConfiguration(SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .build(); testRequest = ResourceHandlerRequest.builder() @@ -128,32 +120,20 @@ public void handleRequest_SimpleSuccess() { .tags(List.of()) .build()); - GetWebExperienceResponse baseResponse = GetWebExperienceResponse.builder() + GetWebExperienceResponse response = GetWebExperienceResponse.builder() .applicationId(APP_ID) .webExperienceId(WEB_EXPERIENCE_ID) .createdAt(Instant.ofEpochMilli(1697824935000L)) .updatedAt(Instant.ofEpochMilli(1697839335000L)) .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") - .status(WebExperienceStatus.PENDING_AUTH_CONFIG) - .defaultEndpoint("Endpoint") - .build(); - - GetWebExperienceResponse responseWithAuth = baseResponse.toBuilder() .status(WebExperienceStatus.ACTIVE) - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") + .defaultEndpoint("Endpoint") .build(); when(qBusinessClient.getWebExperience(any(GetWebExperienceRequest.class))) - .thenReturn(baseResponse) - .thenReturn(responseWithAuth); + .thenReturn(response); // call method under test final ProgressEvent resultProgress = underTest.handleRequest( @@ -171,29 +151,19 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultModel.getTitle()).isEqualTo("This is a title of the web experience."); assertThat(resultModel.getSubtitle()).isEqualTo("This is a subtitle of the web experience."); assertThat(resultModel.getStatus()).isEqualTo(WebExperienceStatus.ACTIVE.toString()); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getMetadataXML()).isEqualTo("XML"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getRoleArn()).isEqualTo("RoleARN"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserIdAttribute()).isEqualTo("UserAttribute"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserGroupAttribute()) - .isEqualTo("UserGroupAttribute"); + assertThat(resultModel.getRoleArn()).isEqualTo("RoleArn"); assertThat(resultModel.getDefaultEndpoint()).isEqualTo("Endpoint"); verify(qBusinessClient).createWebExperience(any(CreateWebExperienceRequest.class)); - var updateReqCaptor = ArgumentCaptor.forClass(UpdateWebExperienceRequest.class); - verify(qBusinessClient).updateWebExperience(updateReqCaptor.capture()); - verify(qBusinessClient, times(3)).getWebExperience( + verify(qBusinessClient, times(2)).getWebExperience( argThat((ArgumentMatcher) t -> t.applicationId().equals(APP_ID) && t.webExperienceId().equals(WEB_EXPERIENCE_ID)) ); verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); - - UpdateWebExperienceRequest updateRequest = updateReqCaptor.getValue(); - assertThat(updateRequest.applicationId()).isEqualTo(APP_ID); - assertThat(updateRequest.webExperienceId()).isEqualTo(WEB_EXPERIENCE_ID); } @Test - public void testItSkipsCallingUpdate() { + public void handleRequest_WithoutRoleArn() { // set up when(qBusinessClient.createWebExperience(any(CreateWebExperienceRequest.class))) .thenReturn(CreateWebExperienceResponse.builder() @@ -206,7 +176,7 @@ public void testItSkipsCallingUpdate() { .build()); createModel = createModel.toBuilder() - .authenticationConfiguration(null) + .roleArn(null) .build(); testRequest.setDesiredResourceState(createModel); GetWebExperienceResponse response = GetWebExperienceResponse.builder() @@ -261,21 +231,13 @@ public void handleRequestFromProcessingStateToActive() { .updatedAt(Instant.ofEpochMilli(1697839335000L)) .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .defaultEndpoint("Endpoint") .build(); when(qBusinessClient.getWebExperience(any(GetWebExperienceRequest.class))) .thenReturn( getResponse.toBuilder().status(WebExperienceStatus.CREATING).build(), - getResponse.toBuilder().status(WebExperienceStatus.PENDING_AUTH_CONFIG).build(), getResponse.toBuilder().status(WebExperienceStatus.ACTIVE).build() ); @@ -288,10 +250,9 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultProgress).isNotNull(); assertThat(resultProgress.isSuccess()).isTrue(); verify(qBusinessClient).createWebExperience(any(CreateWebExperienceRequest.class)); - verify(qBusinessClient, times(4)).getWebExperience( + verify(qBusinessClient, times(3)).getWebExperience( argThat((ArgumentMatcher) t -> t.applicationId().equals(APP_ID) && t.webExperienceId().equals(WEB_EXPERIENCE_ID)) ); - verify(qBusinessClient).updateWebExperience(any(UpdateWebExperienceRequest.class)); verify(qBusinessClient).listTagsForResource(any(ListTagsForResourceRequest.class)); } @@ -314,14 +275,7 @@ public void testItFailsWithErrorMessageWhenGetReturnsFailStatus() { .subtitle("This is a subtitle of the web experience.") .error(ErrorDetail.builder().errorMessage("There was a problem in get web experience.").build()) .status(WebExperienceStatus.FAILED) - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("roleArn") .defaultEndpoint("Endpoint") .build()); diff --git a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/ReadHandlerTest.java b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/ReadHandlerTest.java index eba8e49..733314e 100644 --- a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/ReadHandlerTest.java +++ b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/ReadHandlerTest.java @@ -101,14 +101,7 @@ public void handleRequest_SimpleSuccess() { .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") .status(WebExperienceStatus.ACTIVE) - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .defaultEndpoint("Endpoint") .build()); when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) @@ -141,11 +134,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultModel.getTitle()).isEqualTo("This is a title of the web experience."); assertThat(resultModel.getSubtitle()).isEqualTo("This is a subtitle of the web experience."); assertThat(resultModel.getStatus()).isEqualTo(WebExperienceStatus.ACTIVE.toString()); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getMetadataXML()).isEqualTo("XML"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getRoleArn()).isEqualTo("RoleARN"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserIdAttribute()).isEqualTo("UserAttribute"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserGroupAttribute()) - .isEqualTo("UserGroupAttribute"); + assertThat(resultModel.getRoleArn()).isEqualTo("RoleArn"); assertThat(resultModel.getDefaultEndpoint()).isEqualTo("Endpoint"); var tags = resultModel.getTags().stream().map(tag -> Map.entry(tag.getKey(), tag.getValue())).toList(); @@ -163,14 +152,7 @@ public void handleRequest_SimpleSuccess_withMissingProperties() { .webExperienceId(WEB_EXPERIENCE_ID) .title("This is a title of the web experience.") .status(WebExperienceStatus.ACTIVE) - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .build()); // call method under test @@ -195,11 +177,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(resultModel.getTitle()).isEqualTo("This is a title of the web experience."); assertThat(resultModel.getApplicationId()).isEqualTo(APP_ID); assertThat(resultModel.getWebExperienceId()).isEqualTo(WEB_EXPERIENCE_ID); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getMetadataXML()).isEqualTo("XML"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getRoleArn()).isEqualTo("RoleARN"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserIdAttribute()).isEqualTo("UserAttribute"); - assertThat(resultModel.getAuthenticationConfiguration().getSamlConfiguration().getUserGroupAttribute()) - .isEqualTo("UserGroupAttribute"); + assertThat(resultModel.getRoleArn()).isEqualTo("RoleArn"); } private static Stream serviceErrorAndExpectedCfnCode() { @@ -245,14 +223,7 @@ public void testThatItReturnsExpectedErrorCodeWhenListTagsForResourceFails( .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") .status(WebExperienceStatus.ACTIVE) - .authenticationConfiguration(software.amazon.awssdk.services.qbusiness.model.WebExperienceAuthConfiguration.builder() - .samlConfiguration(software.amazon.awssdk.services.qbusiness.model.SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .defaultEndpoint("Endpoint") .build()); diff --git a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/UpdateHandlerTest.java b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/UpdateHandlerTest.java index dbdc3c7..97e05f3 100644 --- a/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/UpdateHandlerTest.java +++ b/aws-qbusiness-webexperience/src/test/java/software/amazon/qbusiness/webexperience/UpdateHandlerTest.java @@ -88,14 +88,7 @@ public void setup() { .title("This is a title of the web experience.") .subtitle("This is a subtitle of the web experience.") .status(WebExperienceStatus.ACTIVE.toString()) - .authenticationConfiguration(WebExperienceAuthConfiguration.builder() - .samlConfiguration(SamlConfiguration.builder() - .metadataXML("XML") - .roleArn("RoleARN") - .userIdAttribute("UserAttribute") - .userGroupAttribute("UserGroupAttribute") - .build()) - .build()) + .roleArn("RoleArn") .defaultEndpoint("Endpoint") .tags(List.of( Tag.builder().key("remain").value("thesame").build(), @@ -109,14 +102,7 @@ public void setup() { .webExperienceId(WEB_EXPERIENCE_ID) .title("This is a new title of the web experience.") .subtitle("This is a new subtitle of the web experience.") - .authenticationConfiguration(WebExperienceAuthConfiguration.builder() - .samlConfiguration(SamlConfiguration.builder() - .metadataXML("XML2") - .roleArn("RoleARN2") - .userIdAttribute("UserAttribute2") - .userGroupAttribute("UserGroupAttribute2") - .build()) - .build()) + .roleArn("RoleArn") .tags(List.of( Tag.builder().key("remain").value("thesame").build(), Tag.builder().key("iwillchange").value("nowanewvalue").build(), @@ -191,11 +177,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger assertThat(updateAppRequest.webExperienceId()).isEqualTo(WEB_EXPERIENCE_ID); assertThat(updateAppRequest.title()).isEqualTo("This is a new title of the web experience."); assertThat(updateAppRequest.subtitle()).isEqualTo("This is a new subtitle of the web experience."); - assertThat(updateAppRequest.authenticationConfiguration().samlConfiguration().metadataXML()).isEqualTo("XML2"); - assertThat(updateAppRequest.authenticationConfiguration().samlConfiguration().roleArn()).isEqualTo("RoleARN2"); - assertThat(updateAppRequest.authenticationConfiguration().samlConfiguration().userIdAttribute()).isEqualTo("UserAttribute2"); - assertThat(updateAppRequest.authenticationConfiguration().samlConfiguration().userGroupAttribute()) - .isEqualTo("UserGroupAttribute2"); + assertThat(updateAppRequest.roleArn()).isEqualTo("RoleArn"); verify(sdkClient, times(2)).getWebExperience( argThat((ArgumentMatcher) t -> t.applicationId().equals(APP_ID) && t.webExperienceId().equals(WEB_EXPERIENCE_ID)) @@ -225,7 +207,7 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger } @Test - public void handleRequest_WithoutAuthContextSuccess() { + public void handleRequest_WithoutRoleArnSuccess() { // call method under test previousModel = ResourceModel.builder() .applicationId(APP_ID)