From 34c424689a52614fb414d65899282497fe25b164 Mon Sep 17 00:00:00 2001 From: Serge Simard Date: Thu, 8 Aug 2019 10:55:57 -0400 Subject: Resource Configuration Snapshots Executor and API Issue-ID: CCSDK-1604 Signed-off-by: Serge Simard Change-Id: I349c649e941431b48a309123489d26fb22e0e50a Signed-off-by: Serge Simard --- .../api/ResourceConfigSnapshotController.kt | 124 +++++++++++++++ .../configs/api/ResourceConfigSnapshotException.kt | 20 +++ .../api/ResourceConfigSnapshotExceptionHandler.kt | 118 ++++++++++++++ .../api/ResourceConfigSnapshotControllerTest.kt | 175 +++++++++++++++++++++ .../src/test/resources/application-test.properties | 30 ++++ .../configs-api/src/test/resources/logback.xml | 35 +++++ 6 files changed, 502 insertions(+) create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties create mode 100644 ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml (limited to 'ms/blueprintsprocessor/modules/inbounds/configs-api/src') diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt new file mode 100644 index 000000000..eb7929509 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotController.kt @@ -0,0 +1,124 @@ +/* + * Copyright © 2019 Bell Canada + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.cds.blueprintsprocessor.configs.api + +import com.fasterxml.jackson.databind.JsonNode +import io.swagger.annotations.Api +import io.swagger.annotations.ApiOperation +import io.swagger.annotations.ApiParam +import kotlinx.coroutines.runBlocking +import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot +import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* + +/** + * Exposes Resource Configuration Snapshot API to store and retrieve stored resource configurations. + * + * @author Serge Simard + * @version 1.0 + */ +@RestController +@RequestMapping("/api/v1/configs") +@Api(value = "/api/v1/configs", + description = "Interaction with stored configurations.") +open class ResourceConfigSnapshotController(private val resourceConfigSnapshotService: ResourceConfigSnapshotService) { + + @RequestMapping(path = ["/health-check"], + method = [RequestMethod.GET], + produces = [MediaType.APPLICATION_JSON_VALUE]) + @ResponseBody + @ApiOperation(value = "Health Check", hidden = true) + fun ressCfgSnapshotControllerHealthCheck(): JsonNode = runBlocking { + "Success".asJsonPrimitive() + } + + @RequestMapping(path = [""], + method = [RequestMethod.GET], + produces = [MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE]) + @ApiOperation(value = "Retrieve a resource configuration snapshot.", + notes = "Retrieve a config snapshot, identified by its Resource Id and Type. " + + "An extra 'format' parameter can be passed to tell what content-type is expected.") + @ResponseBody + @PreAuthorize("hasRole('USER')") + fun get( + @ApiParam(value = "Resource Type associated of the resource configuration snapshot.", required = false) + @RequestParam(value = "resourceType", required = true) resourceType: String, + + @ApiParam(value = "Resource Id associated of the resource configuration snapshot.", required = false) + @RequestParam(value = "resourceId", required = true) resourceId: String, + + @ApiParam(value = "Status of the snapshot being retrieved.", defaultValue = "RUNNING", required = false) + @RequestParam(value = "status", required = false, defaultValue = "RUNNING") status: String, + + @ApiParam(value = "Expected format of the snapshot being retrieved.", defaultValue = MediaType.TEXT_PLAIN_VALUE, + required = false) + @RequestParam(value = "format", required = false, defaultValue = MediaType.TEXT_PLAIN_VALUE) format: String) + + : ResponseEntity = runBlocking { + + var configSnapshot = "" + + if (resourceType.isNotEmpty() && resourceId.isNotEmpty()) { + try { + configSnapshot = resourceConfigSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, + resourceType, ResourceConfigSnapshot.Status.valueOf(status.toUpperCase())) + } catch (ex : NoSuchElementException) { + throw ResourceConfigSnapshotException( + "Could not find configuration snapshot entry for type $resourceType and Id $resourceId") + } + } else { + throw IllegalArgumentException("Missing param. You must specify resource-id and resource-type.") + } + + var expectedContentType = format + if (expectedContentType.indexOf('/') < 0) { + expectedContentType = "application/$expectedContentType" + } + val expectedMediaType: MediaType = MediaType.valueOf(expectedContentType) + + ResponseEntity.ok().contentType(expectedMediaType).body(configSnapshot) + } + + @PostMapping("/{resourceType}/{resourceId}/{status}", + produces = [MediaType.APPLICATION_JSON_VALUE]) + @ApiOperation(value = "Store a resource configuration snapshot identified by resourceId, resourceType, status.", + notes = "Store a resource configuration snapshot, identified by its resourceId and resourceType, " + + "and optionally its status, either RUNNING or CANDIDATE.", + response = ResourceConfigSnapshot::class, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + @PreAuthorize("hasRole('USER')") + fun postWithResourceIdAndResourceType( + @ApiParam(value = "Resource Type associated with the resolution.", required = false) + @PathVariable(value = "resourceType", required = true) resourceType: String, + @ApiParam(value = "Resource Id associated with the resolution.", required = false) + @PathVariable(value = "resourceId", required = true) resourceId: String, + @ApiParam(value = "Status of the snapshot being retrieved.", defaultValue = "RUNNING", required = true) + @PathVariable(value = "status", required = true) status: String, + @ApiParam(value = "Config snapshot to store.", required = true) + @RequestBody snapshot: String): ResponseEntity = runBlocking { + + val resultStored = + resourceConfigSnapshotService.write(snapshot, resourceId, resourceType, + ResourceConfigSnapshot.Status.valueOf(status.toUpperCase())) + + ResponseEntity.ok().body(resultStored) + } +} diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt new file mode 100644 index 000000000..1eeea9893 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotException.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2019 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.cds.blueprintsprocessor.configs.api + +class ResourceConfigSnapshotException(message: String) : RuntimeException(message) { + var code: Int = 404 +} diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt new file mode 100644 index 000000000..d21464ef5 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotExceptionHandler.kt @@ -0,0 +1,118 @@ +/* + * Copyright © 2019 Bell Canada + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.cds.blueprintsprocessor.configs.api + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeName +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException +import org.onap.ccsdk.cds.controllerblueprints.core.data.ErrorCode +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.orm.jpa.JpaObjectRetrievalFailureException +import org.springframework.dao.EmptyResultDataAccessException +import org.springframework.dao.IncorrectResultSizeDataAccessException +import org.springframework.web.server.ServerWebInputException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import java.io.Serializable +import java.util.* + +/** + * Handle exceptions in ResourceConfigSnapshot API and provide relevant HTTP status codes and messages + * + * @author Serge Simard + * @version 1.0 + */ +@RestControllerAdvice("org.onap.ccsdk.cds.blueprintsprocessor.configs.api") +open class ResourceConfigSnapshotExceptionHandler { + + private val log = LoggerFactory.getLogger(ResourceConfigSnapshotExceptionHandler::class.toString()) + + private val debugMsg = "Resource_Config_Snapshot_ExceptionHandler_Error_Message" + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: BluePrintProcessorException): ResponseEntity { + val errorCode = ErrorCode.BLUEPRINT_PATH_MISSING + return returnError(e, errorCode) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: ServerWebInputException): ResponseEntity { + val errorCode = ErrorCode.INVALID_REQUEST_FORMAT + return returnError(e, errorCode, false) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: IllegalArgumentException): ResponseEntity { + val errorCode = ErrorCode.INVALID_REQUEST_FORMAT + return returnError(e, errorCode, false) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: IncorrectResultSizeDataAccessException): ResponseEntity { + val errorCode = ErrorCode.DUPLICATE_DATA + return returnError(e, errorCode) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: EmptyResultDataAccessException): ResponseEntity { + val errorCode = ErrorCode.RESOURCE_NOT_FOUND + return returnError(e, errorCode, false) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: JpaObjectRetrievalFailureException): ResponseEntity { + val errorCode = ErrorCode.RESOURCE_NOT_FOUND + return returnError(e, errorCode, false) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: Exception): ResponseEntity { + val errorCode = ErrorCode.GENERIC_FAILURE + return returnError(e, errorCode) + } + + @ExceptionHandler + fun resourceConfigSnapshotExceptionHandler(e: ResourceConfigSnapshotException): ResponseEntity { + val errorCode = ErrorCode.RESOURCE_NOT_FOUND + return returnError(e, errorCode, false) + } + + fun returnError(e: Exception, errorCode: ErrorCode, toBeLogged: Boolean = true): ResponseEntity { + if (toBeLogged) { + log.error(e.message, e) + } else { + log.error(e.message) + } + val errorMessage = + ErrorMessage(errorCode.message(e.message!!), + errorCode.value, + debugMsg) + return ResponseEntity(errorMessage, HttpStatus.resolve(errorCode.httpCode)!!) + } +} + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeName("errorMessage") +@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) +class ErrorMessage(var message: String?, var code: Int?, var debugMessage: String?) : Serializable { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + var timestamp = Date() +} \ No newline at end of file diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt new file mode 100644 index 000000000..c3f18fcba --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/configs/api/ResourceConfigSnapshotControllerTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright © 2019 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.cds.blueprintsprocessor.configs.api + +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintCoreConfiguration +import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintCatalogService +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.security.SecurityProperties +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest +import org.springframework.context.annotation.ComponentScan +import org.springframework.http.MediaType +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters +import java.util.* + +@RunWith(SpringRunner::class) +@WebFluxTest +@ContextConfiguration(classes = [BluePrintCoreConfiguration::class, + BluePrintCatalogService::class, SecurityProperties::class]) +@ComponentScan(basePackages = ["org.onap.ccsdk.cds.blueprintsprocessor", "org.onap.ccsdk.cds.controllerblueprints"]) +@TestPropertySource(locations = ["classpath:application-test.properties"]) +class ResourceConfigSnapshotControllerTest { + + private val log = LoggerFactory.getLogger(ResourceConfigSnapshotControllerTest::class.toString()) + + @Autowired + lateinit var webTestClient: WebTestClient + + val resourceId = "fcaa6ac3ff08" + val resourceType = "PNF" + val snapshotData = "PAYLOAD DATA" + + var requestArguments = "resourceId=$resourceId&resourceType=$resourceType" + + @Test + fun `ping return Success`() { + runBlocking { + webTestClient.get().uri("/api/v1/configs/health-check") + .exchange() + .expectStatus().isOk + .expectBody() + .equals("Success") + } + } + + @Test + fun `update configuration is allowed and updates timestamp`() { + runBlocking { + + webTestClient + .post() + .uri("/api/v1/configs/$resourceType/$resourceId/running") + .body(BodyInserters.fromObject(snapshotData)) + .exchange() + .expectStatus().is2xxSuccessful + .expectBody() + .jsonPath("$.createdDate") + .value { println(it) } + + webTestClient + .post() + .uri("/api/v1/configs/$resourceType/$resourceId/running") + .body(BodyInserters.fromObject(snapshotData)) + .exchange() + .expectStatus().is2xxSuccessful + .expectBody() + .jsonPath("$.createdDate") + .value { println(it)} + } + } + + @Test + fun `get returns requested JSON content-type`() { + runBlocking { + post(resourceType, "22", "RUNNING") + get("json", resourceType,"22", "RUNNING") + } + } + + @Test + fun `get returns requested XML content-type`() { + runBlocking { + post(resourceType, "3", "CANDIDATE") + get("xml", resourceType, "3", "CANDIDATE") + } + } + + @Test + fun `get returns 400 error if missing arg`() { + runBlocking { + val arguments = "artifactName=WRONGARG1&resolutionKey=WRONGARG1" + + webTestClient.get().uri("/api/v1/configs?$arguments") + .exchange() + .expectStatus().isBadRequest + } + } + + @Test + fun `get returns 400 error if wrong Status arg`() { + runBlocking { + val arguments = "resourceId=MISSING&resourceType=PNF&status=TOTALLY_WRONG" + + webTestClient.get().uri("/api/v1/configs?$arguments") + .exchange() + .expectStatus().isBadRequest + } + } + + @Test + fun `get returns 404 if entry not found`() { + runBlocking { + + webTestClient + .get() + .uri("/api/v1/configs?resourceId=MISSING&resourceType=PNF") + .exchange() + .expectStatus().isNotFound + } + } + + private fun post( resourceType: String, resourceId: String, status: String) { + webTestClient + .post() + .uri("/api/v1/configs/$resourceType/$resourceId/$status") + .body(BodyInserters.fromObject(snapshotData)) + .exchange() + .expectStatus().is2xxSuccessful + .expectBody() + } + + private fun get(expectedType : String, resourceType: String, resourceId: String, status: String) { + var requestArguments = "resourceId=$resourceId&resourceType=$resourceType&status=$status" + + if (expectedType.isNotEmpty()) { + requestArguments = "$requestArguments&format=$expectedType" + webTestClient + .get() + .uri("/api/v1/configs?$requestArguments") + .exchange() + .expectStatus().is2xxSuccessful + .expectHeader().contentType(MediaType.valueOf("application/$expectedType")) + .expectBody().equals(snapshotData) + } else { + webTestClient + .get() + .uri("/api/v1/configs?$requestArguments") + .exchange() + .expectStatus().is2xxSuccessful + .expectHeader().contentType(MediaType.TEXT_PLAIN) + .expectBody().equals(snapshotData) + } + } +} \ No newline at end of file diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties new file mode 100644 index 000000000..e02ed89f9 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/application-test.properties @@ -0,0 +1,30 @@ +# Copyright © 2019 Bell Canada. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +blueprintsprocessor.db.primary.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 +blueprintsprocessor.db.primary.username=sa +blueprintsprocessor.db.primary.password= +blueprintsprocessor.db.primary.driverClassName=org.h2.Driver +blueprintsprocessor.db.primary.hibernateHbm2ddlAuto=create-drop +blueprintsprocessor.db.primary.hibernateDDLAuto=update +blueprintsprocessor.db.primary.hibernateNamingStrategy=org.hibernate.cfg.ImprovedNamingStrategy +blueprintsprocessor.db.primary.hibernateDialect=org.hibernate.dialect.H2Dialect +# Controller Blueprints Core Configuration +blueprintsprocessor.blueprintDeployPath=./target/blueprints/deploy +blueprintsprocessor.blueprintWorkingPath=./target/blueprints/work +blueprintsprocessor.blueprintArchivePath=./target/blueprints/archive + +# Python executor +blueprints.processor.functions.python.executor.executionPath=./../../../../components/scripts/python/ccsdk_blueprints +blueprints.processor.functions.python.executor.modulePaths=./../../../../components/scripts/python/ccsdk_blueprints diff --git a/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml new file mode 100644 index 000000000..ed92b8963 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/configs-api/src/test/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{100} - %msg%n + + + + + + + + + + + + + -- cgit 1.2.3-korg