From 5f0a5dde67bc0e7c99bd8f9e9b7c447e69ce62fa Mon Sep 17 00:00:00 2001 From: Alexis de Talhouët Date: Sat, 29 Jun 2019 00:28:40 -0400 Subject: Enforce resolutionKey or resourceId/resourceType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are three existing ways to perform the resolution: either we don't store the results at all, whether for resource or template either we store using the resolution key. The combination of blueprintName, blueprintVersion, artifactName and resolutionKey has to be unique. If it is re-used, it is considered as a new attempt for that specific resolution request, and process will only try to resolve resources not marked at SUCCESS in the database. either we store using the resourceId and resourceType. As previous point, the combination of blueprintName, blueprintVersion, artifactName and resolutionKey has to be unique. If it is re-used, it is considered as a new attempt for that specific resolution request, and process will only try to resolve resources not marked at SUCCESS in the database. TBD: add uni tests Issue-ID: CCSDK-1423 Change-Id: I6b7198453cf0002edfa7a0c9ea3179555211b5dc Signed-off-by: Alexis de Talhouët --- .../executor/ComponentRemotePythonExecutorTest.kt | 78 +++++------ .../resolution/ResourceResolutionComponent.kt | 52 +++++--- .../resolution/ResourceResolutionService.kt | 111 +++++++++++++--- .../resource/resolution/db/ResourceResolution.kt | 57 ++++---- .../resolution/db/ResourceResolutionDBService.kt | 68 ++++++++-- .../resolution/db/ResourceResolutionRepository.kt | 15 +++ .../resource/resolution/db/TemplateResolution.kt | 31 +++-- .../resolution/db/TemplateResolutionRepository.kt | 35 ++++- .../resolution/db/TemplateResolutionService.kt | 147 +++++++++++++++------ .../db/BlueprintProcessorCatalogServiceImpl.kt | 5 - .../resource/api/ResolutionException.kt | 20 +++ .../resource/api/ResourceController.kt | 7 +- .../resource/api/ResourceException.kt | 20 --- .../resource/api/ResourceExceptionHandler.kt | 2 +- .../resource/api/TemplateController.kt | 84 ++++++++++-- .../api/ResourceControllerTest.kt | 20 +-- .../resource/api/TemplateControllerTest.kt | 9 +- 17 files changed, 531 insertions(+), 230 deletions(-) create mode 100644 ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResolutionException.kt delete mode 100644 ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceException.kt (limited to 'ms') diff --git a/ms/blueprintsprocessor/functions/python-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/python/executor/ComponentRemotePythonExecutorTest.kt b/ms/blueprintsprocessor/functions/python-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/python/executor/ComponentRemotePythonExecutorTest.kt index 166d7b136..31aa7c7c1 100644 --- a/ms/blueprintsprocessor/functions/python-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/python/executor/ComponentRemotePythonExecutorTest.kt +++ b/ms/blueprintsprocessor/functions/python-executor/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/python/executor/ComponentRemotePythonExecutorTest.kt @@ -48,11 +48,12 @@ class ComponentRemotePythonExecutorTest { val componentRemotePythonExecutor = ComponentRemotePythonExecutor(remoteScriptExecutionService) - val executionServiceInput = JacksonUtils.readValueFromClassPathFile("payload/requests/sample-activate-request.json", + val executionServiceInput = + JacksonUtils.readValueFromClassPathFile("payload/requests/sample-activate-request.json", ExecutionServiceInput::class.java)!! val bluePrintRuntimeService = BluePrintMetadataUtils.getBluePrintRuntime("123456-1000", - "./../../../../components/model-catalog/blueprint-model/test-blueprint/remote_scripts") + "./../../../../components/model-catalog/blueprint-model/test-blueprint/remote_scripts") val stepMetaData: MutableMap = hashMapOf() stepMetaData.putJsonElement(BluePrintConstants.PROPERTY_CURRENT_NODE_TEMPLATE, "execute-remote-python") @@ -76,14 +77,17 @@ class ComponentRemotePythonExecutorTest { fun testComponentRemotePythonExecutorProcessNB() { runBlocking { val remoteScriptExecutionService = - MockRemoteScriptExecutionService() + MockRemoteScriptExecutionService() val componentRemotePythonExecutor = ComponentRemotePythonExecutor( - remoteScriptExecutionService) + remoteScriptExecutionService) val bluePrintRuntime = mockk( - "123456-1000") - val input = getMockedOutput(bluePrintRuntime) + "123456-1000") + + every { bluePrintRuntime.setNodeTemplateAttributeValue(any(), any(), any()) } answers {} + + val input = getMockedOutput(bluePrintRuntime) componentRemotePythonExecutor.bluePrintRuntimeService = - bluePrintRuntime + bluePrintRuntime componentRemotePythonExecutor.applyNB(input) } } @@ -96,13 +100,13 @@ class ComponentRemotePythonExecutorTest { val stepMetaData: MutableMap = hashMapOf() stepMetaData.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_NODE_TEMPLATE, - "execute-remote-python") + BluePrintConstants.PROPERTY_CURRENT_NODE_TEMPLATE, + "execute-remote-python") stepMetaData.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_INTERFACE, - "ComponentRemotePythonExecutor") + BluePrintConstants.PROPERTY_CURRENT_INTERFACE, + "ComponentRemotePythonExecutor") stepMetaData.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_OPERATION, "process") + BluePrintConstants.PROPERTY_CURRENT_OPERATION, "process") val mapper = ObjectMapper() val rootNode = mapper.createObjectNode() @@ -111,13 +115,13 @@ class ComponentRemotePythonExecutorTest { val operationalInputs: MutableMap = hashMapOf() operationalInputs.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_NODE_TEMPLATE, - "execute-remote-python") + BluePrintConstants.PROPERTY_CURRENT_NODE_TEMPLATE, + "execute-remote-python") operationalInputs.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_INTERFACE, - "ComponentRemotePythonExecutor") + BluePrintConstants.PROPERTY_CURRENT_INTERFACE, + "ComponentRemotePythonExecutor") operationalInputs.putJsonElement( - BluePrintConstants.PROPERTY_CURRENT_OPERATION, "process") + BluePrintConstants.PROPERTY_CURRENT_OPERATION, "process") operationalInputs.putJsonElement("endpoint-selector", "aai") operationalInputs.putJsonElement("dynamic-properties", rootNode) operationalInputs.putJsonElement("command", "./run.sh") @@ -125,8 +129,8 @@ class ComponentRemotePythonExecutorTest { every { svc.resolveNodeTemplateInterfaceOperationInputs( - "execute-remote-python", - "ComponentRemotePythonExecutor", "process") + "execute-remote-python", + "ComponentRemotePythonExecutor", "process") } returns operationalInputs val stepInputData = StepData().apply { @@ -135,42 +139,42 @@ class ComponentRemotePythonExecutorTest { } val executionServiceInput = JacksonUtils - .readValueFromClassPathFile( - "payload/requests/sample-remote-python-request.json", - ExecutionServiceInput::class.java)!! + .readValueFromClassPathFile( + "payload/requests/sample-remote-python-request.json", + ExecutionServiceInput::class.java)!! executionServiceInput.stepData = stepInputData val operationOutputs = hashMapOf() every { svc.resolveNodeTemplateInterfaceOperationOutputs( - "execute-remote-python", - "ComponentRemotePythonExecutor", "process") + "execute-remote-python", + "ComponentRemotePythonExecutor", "process") } returns operationOutputs val bluePrintRuntimeService = BluePrintMetadataUtils - .getBluePrintRuntime("123456-1000", - "./../../../../components/model-" + - "catalog/blueprint-model/test-blueprint/" + - "remote_scripts") + .getBluePrintRuntime("123456-1000", + "./../../../../components/model-" + + "catalog/blueprint-model/test-blueprint/" + + "remote_scripts") every { svc.resolveNodeTemplateArtifactDefinition( - "execute-remote-python", "component-script") - } returns bluePrintRuntimeService.resolveNodeTemplateArtifactDefinition( "execute-remote-python", "component-script") + } returns bluePrintRuntimeService.resolveNodeTemplateArtifactDefinition( + "execute-remote-python", "component-script") every { svc.setNodeTemplateAttributeValue( - "execute-remote-python", "prepare-environment-logs", - "prepared successfully".asJsonPrimitive()) + "execute-remote-python", "prepare-environment-logs", + "prepared successfully".asJsonPrimitive()) } returns Unit every { svc.setNodeTemplateAttributeValue( - "execute-remote-python", - "execute-command-logs", "N/A".asJsonPrimitive()) + "execute-remote-python", + "execute-command-logs", "N/A".asJsonPrimitive()) } returns Unit every { svc.setNodeTemplateAttributeValue( - "execute-remote-python", - "execute-command-logs", - "processed successfully".asJsonPrimitive()) + "execute-remote-python", + "execute-command-logs", + "processed successfully".asJsonPrimitive()) } returns Unit every { diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt index 2039d2e5a..fd14cc8c1 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt @@ -21,11 +21,13 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.utils.ResourceAssignmentUtils import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode import org.onap.ccsdk.cds.controllerblueprints.core.asObjectNode import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.annotation.Scope +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component @Component("component-resource-resolution") @@ -35,41 +37,51 @@ open class ResourceResolutionComponent(private val resourceResolutionService: Re override suspend fun processNB(executionRequest: ExecutionServiceInput) { - val occurrence = getOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE) - val resolutionKey = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY) - val storeResult = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT) - val resourceId = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID) - val resourceType = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE) - + val occurrence = getOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE).asInt() + val resolutionKey = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY)?.asText() ?: "" + val storeResult = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)?.asBoolean() ?: false + val resourceId = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID)?.asText() ?: "" + val resourceType = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE)?.asText() ?: "" val properties: MutableMap = mutableMapOf() - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] = storeResult?.asBoolean() ?: false - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = resolutionKey?.asText() ?: "" - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId?.asText() ?: "" - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType?.asText() ?: "" + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] = storeResult + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = resolutionKey + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] = occurrence + + val jsonResponse = JsonNodeFactory.instance.objectNode() + + // validate inputs if we need to store the resource and template resolution. + if (storeResult) { + if (resolutionKey.isNotEmpty() && (resourceId.isNotEmpty() || resourceType.isNotEmpty())) { + throw BluePrintProcessorException("Can't proceed with the resolution: either provide resolution-key OR combination of resource-id and resource-type.") + } else if ((resourceType.isNotEmpty() && resourceId.isEmpty()) || (resourceType.isEmpty() && resourceId.isNotEmpty())) { + throw BluePrintProcessorException("Can't proceed with the resolution: both resource-id and resource-type should be provided, one of them is missing.") + } else if (resourceType.isEmpty() && resourceId.isEmpty() && resolutionKey.isEmpty()) { + throw BluePrintProcessorException("Can't proceed with the resolution: can't persist resolution without a correlation key. " + + "Either provide a resolution-key OR combination of resource-id and resource-type OR set `storeResult` to false.") + } + } val artifactPrefixNamesNode = getOperationInput(ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES) val artifactPrefixNames = JacksonUtils.getListFromJsonNode(artifactPrefixNamesNode, String::class.java) - val resourceAssignmentRuntimeService = - ResourceAssignmentUtils.transformToRARuntimeService(bluePrintRuntimeService, artifactPrefixNames.toString()) - - val jsonResponse = JsonNodeFactory.instance.objectNode() - for (j in 1..occurrence.asInt()) { - val key = resolutionKey?.asText() + "-" + j - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = key + for (j in 1..occurrence) { + properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] = j - val response = resourceResolutionService.resolveResources(resourceAssignmentRuntimeService, + val response = resourceResolutionService.resolveResources(bluePrintRuntimeService, nodeTemplateName, artifactPrefixNames, properties) // provide indexed result in output if we have multiple resolution - if (occurrence.asInt() != 1) { - jsonResponse.set(key, response.asJsonNode()) + if (occurrence != 1) { + jsonResponse.set(Integer.toString(j), response.asJsonNode()) } else { jsonResponse.setAll(response.asObjectNode()) } + } // Set Output Attributes diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt index 1270298a8..e08ac520a 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt @@ -20,13 +20,14 @@ package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolution import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolutionDBService import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.processor.ResourceAssignmentProcessor import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.utils.ResourceAssignmentUtils import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException -import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive import org.onap.ccsdk.cds.controllerblueprints.core.checkNotEmpty import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintTemplateService @@ -60,7 +61,7 @@ interface ResourceResolutionService { @Service(ResourceResolutionConstants.SERVICE_RESOURCE_RESOLUTION) open class ResourceResolutionServiceImpl(private var applicationContext: ApplicationContext, - private var resolutionResultService: TemplateResolutionService, + private var templateResolutionDBService: TemplateResolutionService, private var blueprintTemplateService: BluePrintTemplateService, private var resourceResolutionDBService: ResourceResolutionDBService) : ResourceResolutionService { @@ -76,22 +77,30 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica override suspend fun resolveFromDatabase(bluePrintRuntimeService: BluePrintRuntimeService<*>, artifactTemplate: String, resolutionKey: String): String { - return resolutionResultService.read(bluePrintRuntimeService, artifactTemplate, resolutionKey) + return templateResolutionDBService.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName( + bluePrintRuntimeService, + artifactTemplate, + resolutionKey) } override suspend fun resolveResources(bluePrintRuntimeService: BluePrintRuntimeService<*>, nodeTemplateName: String, artifactNames: List, properties: Map): MutableMap { + + val resourceAssignmentRuntimeService = + ResourceAssignmentUtils.transformToRARuntimeService(bluePrintRuntimeService, artifactNames.toString()) + val resolvedParams: MutableMap = hashMapOf() artifactNames.forEach { artifactName -> - val resolvedContent = resolveResources(bluePrintRuntimeService, nodeTemplateName, + val resolvedContent = resolveResources(resourceAssignmentRuntimeService, nodeTemplateName, artifactName, properties) resolvedParams[artifactName] = resolvedContent } return resolvedParams } + override suspend fun resolveResources(bluePrintRuntimeService: BluePrintRuntimeService<*>, nodeTemplateName: String, artifactPrefix: String, properties: Map): String { @@ -111,6 +120,13 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica as? MutableList ?: throw BluePrintProcessorException("couldn't get Dictionary Definitions") + if (isToStore(properties)) { + val existingResourceResolution = isNewResolution(bluePrintRuntimeService, properties, artifactPrefix) + if (existingResourceResolution.isNotEmpty()) { + updateResourceAssignmentWithExisting(existingResourceResolution, resourceAssignments) + } + } + // Get the Resource Dictionary Name val resourceDefinitions: MutableMap = ResourceAssignmentUtils .resourceDefinitions(bluePrintRuntimeService.bluePrintContext().rootPath) @@ -128,10 +144,9 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica resolvedContent = blueprintTemplateService.generateContent(bluePrintRuntimeService, nodeTemplateName, artifactTemplate, resolvedParamJsonContent) - if (properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT) - && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean) { - resolutionResultService.write(properties, resolvedContent, bluePrintRuntimeService, artifactPrefix) - log.info("template resolution saved into database successfully : ($properties)") + if (isToStore(properties)) { + templateResolutionDBService.write(properties, resolvedContent, bluePrintRuntimeService, artifactPrefix) + log.info("Template resolution saved into database successfully : ($properties)") } return resolvedContent @@ -154,7 +169,9 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica coroutineScope { bulkSequenced.forEach { batchResourceAssignments -> // Execute Non Dependent Assignments in parallel ( ie asynchronously ) - val deferred = batchResourceAssignments.filter { it.name != "*" && it.name != "start" } + val deferred = batchResourceAssignments + .filter { it.name != "*" && it.name != "start" } + .filter { it.status != BluePrintConstants.STATUS_SUCCESS } .map { resourceAssignment -> async { val dictionaryName = resourceAssignment.dictionaryName @@ -197,12 +214,6 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica } - private fun isToStore(properties: Map): Boolean { - return properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT) - && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean - } - - /** * If the Source instance is "input", then it is not mandatory to have source Resource Definition, So it can * derive the default input processor. @@ -233,4 +244,74 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica return processorName } + + // Check whether to store or not the resolution of resource and template + private fun isToStore(properties: Map): Boolean { + return properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT) + && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean + } + + // Check whether resolution already exist in the database for the specified resolution-key or resourceId/resourceType + private suspend fun isNewResolution(bluePrintRuntimeService: BluePrintRuntimeService<*>, + properties: Map, + artifactPrefix: String): List { + val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int + val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String + val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String + val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String + + if (resolutionKey.isNotEmpty()) { + val existingResourceAssignments = + resourceResolutionDBService.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence( + bluePrintRuntimeService, + resolutionKey, + occurrence, + artifactPrefix) + if (existingResourceAssignments.isNotEmpty()) { + log.info("Resolution with resolutionKey=($resolutionKey) already exist - will resolve all resources not already resolved.", + resolutionKey) + } + return existingResourceAssignments + } else if (resourceId.isNotEmpty() && resourceType.isNotEmpty()) { + val existingResourceAssignments = + resourceResolutionDBService.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence( + bluePrintRuntimeService, + resourceId, + resourceType, + + occurrence, + artifactPrefix) + if (existingResourceAssignments.isNotEmpty()) { + log.info("Resolution with resourceId=($resourceId) and resourceType=($resourceType) already exist - will resolve " + + "all resources not already resolved.") + } + return existingResourceAssignments + } + return emptyList() + } + + // Update the resource assignment list with the status of the resource that have already been resolved + private fun updateResourceAssignmentWithExisting(resourceResolutionList: List, + resourceAssignmentList: MutableList) { + resourceResolutionList.forEach { resourceResolution -> + if (resourceResolution.status == BluePrintConstants.STATUS_SUCCESS) { + resourceAssignmentList.forEach { + if (compareOne(resourceResolution, it)) { + log.info("Resource ({}) already resolve: value=({})", it.name, resourceResolution.value) + it.property!!.value = resourceResolution.value!!.asJsonPrimitive() + it.status = resourceResolution.status + } + } + } + } + } + + // Comparision between what we have in the database vs what we have to assign. + private fun compareOne(resourceResolution: ResourceResolution, resourceAssignment: ResourceAssignment): Boolean { + return (resourceResolution.name == resourceAssignment.name + && resourceResolution.dictionaryName == resourceAssignment.dictionaryName + && resourceResolution.dictionarySource == resourceAssignment.dictionarySource + && resourceResolution.dictionaryVersion == resourceAssignment.version) + } + } diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt index fa922cde5..baabfd913 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt @@ -24,6 +24,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.io.Serializable import java.util.* import javax.persistence.Column +import javax.persistence.ElementCollection import javax.persistence.Entity import javax.persistence.EntityListeners import javax.persistence.Id @@ -38,23 +39,6 @@ import javax.persistence.TemporalType @Proxy(lazy = false) class ResourceResolution : Serializable { - @Id - @Column(name = "resource_resolution_id") - var id: String? = null - - @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.", - required = true) - @Column(name = "resolution_key", nullable = false) - var resolutionKey: String? = null - - @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF") - @Column(name = "resource_type", nullable = false) - var resourceType: String? = null - - @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true) - @Column(name = "resource_id", nullable = false) - var resourceId: String? = null - @get:ApiModelProperty(value = "Name of the CBA.", required = true) @Column(name = "blueprint_name", nullable = false) var blueprintName: String? = null @@ -67,19 +51,43 @@ class ResourceResolution : Serializable { @Column(name = "artifact_name", nullable = false) var artifactName: String? = null + @get:ApiModelProperty(value = "Name of the resource.", required = true) + @Column(name = "name", nullable = false) + var name: String? = null + + @get:ApiModelProperty(value = "Value of the resolution.", required = true) + @Lob + @Column(name = "value", nullable = false) + var value: String? = null + @get:ApiModelProperty(value = "Whether success of failure.", required = true) @Column(name = "status", nullable = false) var status: String? = null - @get:ApiModelProperty(value = "Name of the resource.", required = true) - @Column(name = "name", nullable = false) - var name: String? = null + @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.", + required = true) + @Column(name = "resolution_key", nullable = false) + var resolutionKey: String? = null + + @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF") + @Column(name = "resource_type", nullable = false) + var resourceType: String? = null + + @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true) + @Column(name = "resource_id", nullable = false) + var resourceId: String? = null + + @get:ApiModelProperty(value = "If resolution occurred multiple time, this field provides the index.", + required = true) + @Column(name = "occurrence", nullable = false) + var occurrence: Int = 0 @get:ApiModelProperty(value = "Name of the data dictionary used for the resolution.", required = true) @Column(name = "dictionary_name", nullable = false) var dictionaryName: String? = null - @get:ApiModelProperty(value = "Source associated with the data dictionary used for the resolution.", required = true) + @get:ApiModelProperty(value = "Source associated with the data dictionary used for the resolution.", + required = true) @Column(name = "dictionary_status", nullable = false) var dictionarySource: String? = null @@ -87,10 +95,9 @@ class ResourceResolution : Serializable { @Column(name = "dictionary_version", nullable = false) var dictionaryVersion: Int = 0 - @get:ApiModelProperty(value = "Value of the resolution.", required = true) - @Lob - @Column(name = "value", nullable = false) - var value: String? = null + @Id + @Column(name = "resource_resolution_id") + var id: String? = null @get:ApiModelProperty(value = "Creation date of the resolution.", required = true) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt index c6ffde742..e9679eeba 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt @@ -25,6 +25,7 @@ import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResourceAssignment import org.slf4j.LoggerFactory import org.springframework.dao.DataIntegrityViolationException +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.stereotype.Service import java.util.* @@ -33,6 +34,49 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso private val log = LoggerFactory.getLogger(ResourceResolutionDBService::class.toString()) + suspend fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence( + bluePrintRuntimeService: BluePrintRuntimeService<*>, key: String, + occurrence: Int, artifactPrefix: String): List { + return try { + val metadata = bluePrintRuntimeService.bluePrintContext().metadata!! + + val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! + val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! + + resourceResolutionRepository.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence( + blueprintName, + blueprintVersion, + artifactPrefix, + key, + occurrence) + } catch (e: EmptyResultDataAccessException) { + emptyList() + } + } + + suspend fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence( + bluePrintRuntimeService: BluePrintRuntimeService<*>, resourceId: String, + resourceType: String, occurrence: Int, + artifactPrefix: String): List { + return try { + + val metadata = bluePrintRuntimeService.bluePrintContext().metadata!! + + val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! + val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! + + resourceResolutionRepository.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence( + blueprintName, + blueprintVersion, + artifactPrefix, + resourceId, + resourceType, + occurrence) + } catch (e: EmptyResultDataAccessException) { + emptyList() + } + } + suspend fun readValue(blueprintName: String, blueprintVersion: String, artifactPrefix: String, @@ -81,12 +125,11 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! - val resolutionKey = - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY].toString() - val resourceType = - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE].toString() - val resourceId = - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID].toString() + + val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String + val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String + val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String + val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int write(blueprintName, blueprintVersion, @@ -94,7 +137,8 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso resourceId, resourceType, artifactPrefix, - resourceAssignment) + resourceAssignment, + occurrence) } suspend fun write(blueprintName: String, @@ -103,20 +147,22 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso resourceId: String, resourceType: String, artifactPrefix: String, - resourceAssignment: ResourceAssignment): ResourceResolution = withContext(Dispatchers.IO) { + resourceAssignment: ResourceAssignment, + occurrence: Int = 0): ResourceResolution = withContext(Dispatchers.IO) { val resourceResolution = ResourceResolution() resourceResolution.id = UUID.randomUUID().toString() resourceResolution.artifactName = artifactPrefix + resourceResolution.occurrence = occurrence resourceResolution.blueprintVersion = blueprintVersion resourceResolution.blueprintName = blueprintName resourceResolution.resolutionKey = resolutionKey resourceResolution.resourceType = resourceType resourceResolution.resourceId = resourceId - if (BluePrintConstants.STATUS_FAILURE == resourceAssignment.status) { - resourceResolution.value = "" - } else { + if (BluePrintConstants.STATUS_SUCCESS == resourceAssignment.status) { resourceResolution.value = JacksonUtils.getValue(resourceAssignment.property?.value!!).toString() + } else { + resourceResolution.value = "" } resourceResolution.name = resourceAssignment.name resourceResolution.dictionaryName = resourceAssignment.dictionaryName diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt index d2cbf8411..429041e14 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt @@ -34,4 +34,19 @@ interface ResourceResolutionRepository : JpaRepository + + fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence( + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + resolutionKey: String, + occurrence: Int): List + + fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence( + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + resourceId: String, + resourceType: String, + occurrence: Int): List } \ No newline at end of file diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt index ae24a9ac2..70aadb4b1 100755 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt @@ -38,15 +38,6 @@ import javax.persistence.TemporalType @Proxy(lazy = false) class TemplateResolution : Serializable { - @Id - @Column(name = "template_resolution_id") - var id: String? = null - - @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.", - required = true) - @Column(name = "resolution_key", nullable = false) - var resolutionKey: String? = null - @get:ApiModelProperty(value = "Name of the CBA.", required = true) @Column(name = "blueprint_name", nullable = false) var blueprintName: String? = null @@ -64,6 +55,28 @@ class TemplateResolution : Serializable { @Column(name = "result", nullable = false) var result: String? = null + @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.", + required = true) + @Column(name = "resolution_key", nullable = false) + var resolutionKey: String? = null + + @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF") + @Column(name = "resource_type", nullable = false) + var resourceType: String? = null + + @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true) + @Column(name = "resource_id", nullable = false) + var resourceId: String? = null + + @get:ApiModelProperty(value = "If resolution occurred multiple time, this field provides the index.", + required = true) + @Column(name = "occurrence", nullable = false) + var occurrence: Int = 0 + + @Id + @Column(name = "template_resolution_id") + var id: String? = null + @get:ApiModelProperty(value = "Creation date of the resolution.", required = true) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") @LastModifiedDate diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt index 136b5e952..440663f25 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt @@ -16,10 +16,39 @@ package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db import org.springframework.data.jpa.repository.JpaRepository +import javax.transaction.Transactional interface TemplateResolutionRepository : JpaRepository { - fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(key: String, blueprintName: String?, - blueprintVersion: String?, - artifactName: String): TemplateResolution + fun findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resourceId: String, + resourceType: String, + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + occurrence: Int): TemplateResolution ? + + fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + key: String, + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + occurrence: Int): TemplateResolution ? + + @Transactional + fun deleteByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resourceId: String, + resourceType: String, + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + occurrence: Int) + + @Transactional + fun deleteByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + key: String, + blueprintName: String?, + blueprintVersion: String?, + artifactName: String, + occurrence: Int) } diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt index c4e36401f..44662965d 100644 --- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt @@ -21,36 +21,65 @@ import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.Reso import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintException import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService +import org.slf4j.LoggerFactory import org.springframework.dao.DataIntegrityViolationException +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.stereotype.Service import java.util.* @Service class TemplateResolutionService(private val templateResolutionRepository: TemplateResolutionRepository) { - suspend fun read(bluePrintRuntimeService: BluePrintRuntimeService<*>, - artifactPrefix: String, - resolutionKey: String): String = withContext(Dispatchers.IO) { + private val log = LoggerFactory.getLogger(TemplateResolutionService::class.toString()) - val metadata = bluePrintRuntimeService.bluePrintContext().metadata!! + suspend fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName( + bluePrintRuntimeService: BluePrintRuntimeService<*>, + artifactPrefix: String, + resolutionKey: String): String = + withContext(Dispatchers.IO) { - val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! - val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! + val metadata = bluePrintRuntimeService.bluePrintContext().metadata!! - read(blueprintName, blueprintVersion, artifactPrefix, resolutionKey) - } + val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! + val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! - suspend fun read(blueprintName: String, - blueprintVersion: String, - artifactPrefix: String, - resolutionKey: String): String = withContext(Dispatchers.IO) { + findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName, + blueprintVersion, + artifactPrefix, + resolutionKey) + } - templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName( - resolutionKey, - blueprintName, - blueprintVersion, - artifactPrefix).result!! - } + suspend fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName: String, + blueprintVersion: String, + artifactPrefix: String, + resolutionKey: String, + occurrence: Int = 0): String = + withContext(Dispatchers.IO) { + + templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resolutionKey, + blueprintName, + blueprintVersion, + artifactPrefix, + occurrence)?.result ?: throw EmptyResultDataAccessException(1) + } + + suspend fun findByResoureIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName: String, + blueprintVersion: String, + artifactPrefix: String, + resourceId: String, + resourceType: String, + occurrence: Int = 0): String = + withContext(Dispatchers.IO) { + + templateResolutionRepository.findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resourceId, + resourceType, + blueprintName, + blueprintVersion, + artifactPrefix, + occurrence)?.result!! + } suspend fun write(properties: Map, result: String, bluePrintRuntimeService: BluePrintRuntimeService<*>, @@ -60,30 +89,68 @@ class TemplateResolutionService(private val templateResolutionRepository: Templa val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! - val resolutionKey = - properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY].toString() + val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String + val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String + val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String + val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int - write(blueprintName, blueprintVersion, resolutionKey, artifactPrefix, result) + write(blueprintName, + blueprintVersion, + artifactPrefix, + result, + occurrence, + resolutionKey, + resourceId, + resourceType) } - suspend fun write(blueprintName: String, - blueprintVersion: String, - resolutionKey: String, - artifactPrefix: String, - template: String): TemplateResolution = withContext(Dispatchers.IO) { - - val resourceResolutionResult = TemplateResolution() - resourceResolutionResult.id = UUID.randomUUID().toString() - resourceResolutionResult.artifactName = artifactPrefix - resourceResolutionResult.blueprintVersion = blueprintVersion - resourceResolutionResult.blueprintName = blueprintName - resourceResolutionResult.resolutionKey = resolutionKey - resourceResolutionResult.result = template - - try { - templateResolutionRepository.saveAndFlush(resourceResolutionResult) - } catch (ex: DataIntegrityViolationException) { - throw BluePrintException("Failed to store resource api result.", ex) + suspend fun write(blueprintName: String, blueprintVersion: String, artifactPrefix: String, + template: String, occurrence: Int = 0, resolutionKey: String = "", resourceId: String = "", + resourceType: String = ""): TemplateResolution = + withContext(Dispatchers.IO) { + + val resourceResolutionResult = TemplateResolution() + resourceResolutionResult.id = UUID.randomUUID().toString() + resourceResolutionResult.artifactName = artifactPrefix + resourceResolutionResult.blueprintVersion = blueprintVersion + resourceResolutionResult.blueprintName = blueprintName + resourceResolutionResult.resolutionKey = resolutionKey + resourceResolutionResult.resourceId = resourceId + resourceResolutionResult.resourceType = resourceType + resourceResolutionResult.result = template + resourceResolutionResult.occurrence = occurrence + + // Overwrite template resolution-key of resourceId/resourceType already existant + if (resolutionKey.isNotEmpty()) { + templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resolutionKey, blueprintName, blueprintVersion, artifactPrefix, occurrence)?.let { + log.info("Overwriting template resolution for blueprintName=($blueprintVersion), blueprintVersion=($blueprintName), " + + "artifactName=($artifactPrefix) and resolutionKey=($resolutionKey)") + templateResolutionRepository.deleteByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resolutionKey, + blueprintName, + blueprintVersion, + artifactPrefix, + occurrence) + } + } else if (resourceId.isNotEmpty() && resourceType.isNotEmpty()) { + templateResolutionRepository.findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resourceId, resourceType, blueprintName, blueprintVersion, artifactPrefix, occurrence)?.let { + log.info("Overwriting template resolution for blueprintName=($blueprintVersion), blueprintVersion=($blueprintName), " + + "artifactName=($artifactPrefix), resourceId=($resourceId) and resourceType=($resourceType)") + templateResolutionRepository.deleteByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence( + resourceId, + resourceType, + blueprintName, + blueprintVersion, + artifactPrefix, + occurrence) + } + } + try { + templateResolutionRepository.saveAndFlush(resourceResolutionResult) + } catch (ex: DataIntegrityViolationException) { + throw BluePrintException("Failed to store resource api result.", ex) + } } - } } \ No newline at end of file diff --git a/ms/blueprintsprocessor/modules/commons/db-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/db/BlueprintProcessorCatalogServiceImpl.kt b/ms/blueprintsprocessor/modules/commons/db-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/db/BlueprintProcessorCatalogServiceImpl.kt index 452dd0f5c..3e1f0715b 100755 --- a/ms/blueprintsprocessor/modules/commons/db-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/db/BlueprintProcessorCatalogServiceImpl.kt +++ b/ms/blueprintsprocessor/modules/commons/db-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/db/BlueprintProcessorCatalogServiceImpl.kt @@ -45,11 +45,6 @@ class BlueprintProcessorCatalogServiceImpl(bluePrintRuntimeValidatorService: Blu private val log = LoggerFactory.getLogger(BlueprintProcessorCatalogServiceImpl::class.toString()) - init { - - log.info("BlueprintProcessorCatalogServiceImpl initialized") - } - override suspend fun delete(name: String, version: String) { // Cleaning Deployed Blueprint deleteNBDir(bluePrintPathConfiguration.blueprintDeployPath, name, version) diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResolutionException.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResolutionException.kt new file mode 100644 index 000000000..62e89e260 --- /dev/null +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResolutionException.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.resource.api + +class ResolutionException(message: String) : RuntimeException(message) { + var code: Int = 404 +} diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceController.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceController.kt index 4c8fc193c..3a708a973 100644 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceController.kt +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceController.kt @@ -70,7 +70,7 @@ open class ResourceController(private var resourceResolutionDBService: ResourceR : ResponseEntity> = runBlocking { if ((resolutionKey.isNotEmpty() || artifactName.isNotEmpty()) && (resourceId.isNotEmpty() || resourceType.isNotEmpty())) { - throw ResourceException("Either retrieve resolved value using artifact name and resolution-key OR using resource-id and resource-type.") + throw ResolutionException("Either retrieve resolved value using artifact name and resolution-key OR using resource-id and resource-type.") } else if (resolutionKey.isNotEmpty() && artifactName.isNotEmpty()) { ResponseEntity.ok() .body(resourceResolutionDBService.readWithResolutionKey(bpName, bpVersion, artifactName, resolutionKey)) @@ -81,7 +81,7 @@ open class ResourceController(private var resourceResolutionDBService: ResourceR resourceId, resourceType)) } else { - throw ResourceException("Missing param. Either retrieve resolved value using artifact name and resolution-key OR using resource-id and resource-type.") + throw ResolutionException("Missing param. Either retrieve resolved value using artifact name and resolution-key OR using resource-id and resource-type.") } } @@ -97,8 +97,7 @@ open class ResourceController(private var resourceResolutionDBService: ResourceR @RequestParam(value = "bpName", required = true) bpName: String, @ApiParam(value = "Version of the CBA.", required = true) @RequestParam(value = "bpVersion", required = true) bpVersion: String, - @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", - required = true) + @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", required = true) @RequestParam(value = "artifactName", required = true) artifactName: String, @ApiParam(value = "Resolution Key associated with the resolution.", required = true) @RequestParam(value = "resolutionKey", required = true) resolutionKey: String, diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceException.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceException.kt deleted file mode 100644 index 19c42f2ea..000000000 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceException.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.resource.api - -class ResourceException(message: String) : RuntimeException(message) { - var code: Int = 404 -} diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceExceptionHandler.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceExceptionHandler.kt index f01dccff7..42ff8016c 100644 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceExceptionHandler.kt +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/ResourceExceptionHandler.kt @@ -93,7 +93,7 @@ open class ResourceExceptionHandler { } @ExceptionHandler - fun ResolutionResultsServiceExceptionHandler(e: ResourceException): ResponseEntity { + fun ResolutionResultsServiceExceptionHandler(e: ResolutionException): ResponseEntity { log.error(e.message, e) return ResponseEntity(ErrorMessage(e.message, e.code, debugMsg), HttpStatus.resolve(e.code)) } diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateController.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateController.kt index 83e813079..de5843a66 100644 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateController.kt +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateController.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.runBlocking import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolution import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize @@ -66,15 +67,40 @@ open class TemplateController(private val templateResolutionService: TemplateRes @RequestParam(value = "bpVersion") bpVersion: String, @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", required = true) @RequestParam(value = "artifactName") artifactName: String, - @ApiParam(value = "Resolution Key associated with the resolution.", required = true) + @ApiParam(value = "Resolution Key associated with the resolution.", required = false) @RequestParam(value = "resolutionKey") resolutionKey: String, + @ApiParam(value = "Resource Type associated with the resolution.", required = false) + @RequestParam(value = "resourceType", required = false, defaultValue = "") resourceType: String, + @ApiParam(value = "Resource Id associated with the resolution.", required = false) + @RequestParam(value = "resourceId", required = false, defaultValue = "") resourceId: String, @ApiParam(value = "Expected format of the template being retrieved.", defaultValue = MediaType.TEXT_PLAIN_VALUE, required = true) @RequestParam(value = "format", required = false, defaultValue = MediaType.TEXT_PLAIN_VALUE) format: String) : ResponseEntity = runBlocking { - val result = templateResolutionService.read(bpName, bpVersion, artifactName, resolutionKey) + var result = "" + + if ((resolutionKey.isNotEmpty() || artifactName.isNotEmpty()) && (resourceId.isNotEmpty() || resourceType.isNotEmpty())) { + throw ResolutionException("Either retrieve resolved template using artifact name and resolution-key OR using resource-id and resource-type.") + } else if (resolutionKey.isNotEmpty() && artifactName.isNotEmpty()) { + result = templateResolutionService.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName( + bpName, + bpVersion, + artifactName, + resolutionKey) + } else if (resourceType.isNotEmpty() && resourceId.isNotEmpty()) { + result = + templateResolutionService.findByResoureIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactName( + bpName, + bpVersion, + artifactName, + resourceId, + resourceType) + } else { + throw ResolutionException("Missing param. Either retrieve resolved template using artifact name and resolution-key OR using resource-id and resource-type.") + } + var expectedContentType = format if (expectedContentType.indexOf('/') < 0) { @@ -87,26 +113,56 @@ open class TemplateController(private val templateResolutionService: TemplateRes @PostMapping("/{bpName}/{bpVersion}/{artifactName}/{resolutionKey}", produces = [MediaType.APPLICATION_JSON_VALUE]) - @ApiOperation(value = "Store a resolved template", + @ApiOperation(value = "Store a resolved template w/ resolution-key", notes = "Store a template for a given CBA's action, identified by its blueprint name, blueprint version, " + "artifact name and resolution key.", response = TemplateResolution::class, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody @PreAuthorize("hasRole('USER')") - fun post(@ApiParam(value = "Name of the CBA.", required = true) - @PathVariable(value = "bpName") bpName: String, - @ApiParam(value = "Version of the CBA.", required = true) - @PathVariable(value = "bpVersion") bpVersion: String, - @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", required = true) - @PathVariable(value = "artifactName") artifactName: String, - @ApiParam(value = "Resolution Key associated with the resolution.", required = true) - @PathVariable(value = "resolutionKey") resolutionKey: String, - @ApiParam(value = "Template to store.", required = true) - @RequestBody result: String): ResponseEntity = runBlocking { + fun postWithResolutionKey( + @ApiParam(value = "Name of the CBA.", required = true) + @PathVariable(value = "bpName") bpName: String, + @ApiParam(value = "Version of the CBA.", required = true) + @PathVariable(value = "bpVersion") bpVersion: String, + @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", required = true) + @PathVariable(value = "artifactName") artifactName: String, + @ApiParam(value = "Resolution Key associated with the resolution.", required = true) + @PathVariable(value = "resolutionKey") resolutionKey: String, + @ApiParam(value = "Template to store.", required = true) + @RequestBody result: String): ResponseEntity = runBlocking { + + val resultStored = + templateResolutionService.write(bpName, bpVersion, artifactName, result, resolutionKey = resolutionKey) + + ResponseEntity.ok().body(resultStored) + } + + @PostMapping("/{bpName}/{bpVersion}/{artifactName}/{resourceType}/{resourceId}", + produces = [MediaType.APPLICATION_JSON_VALUE]) + @ApiOperation(value = "Store a resolved template w/ resourceId and resourceType", + notes = "Store a template for a given CBA's action, identified by its blueprint name, blueprint version, " + + "artifact name, resourceId and resourceType.", + response = TemplateResolution::class, + produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + @PreAuthorize("hasRole('USER')") + fun postWithResourceIdAndResourceType( + @ApiParam(value = "Name of the CBA.", required = true) + @PathVariable(value = "bpName") bpName: String, + @ApiParam(value = "Version of the CBA.", required = true) + @PathVariable(value = "bpVersion") bpVersion: String, + @ApiParam(value = "Artifact name for which to retrieve a resolved resource.", required = true) + @PathVariable(value = "artifactName") artifactName: String, + @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 = "Template to store.", required = true) + @RequestBody result: String): ResponseEntity = runBlocking { val resultStored = - templateResolutionService.write(bpName, bpVersion, resolutionKey, artifactName, result) + templateResolutionService.write(bpName, bpVersion, artifactName, result, resourceId = resourceId, resourceType = resourceType) ResponseEntity.ok().body(resultStored) } diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resolutionresults/api/ResourceControllerTest.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resolutionresults/api/ResourceControllerTest.kt index daab7b3bb..9ed97df1a 100644 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resolutionresults/api/ResourceControllerTest.kt +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resolutionresults/api/ResourceControllerTest.kt @@ -16,44 +16,28 @@ package org.onap.ccsdk.cds.blueprintsprocessor.resolutionresults.api -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.module.kotlin.readValue import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintCoreConfiguration import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolution import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolutionDBService import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive import org.onap.ccsdk.cds.controllerblueprints.core.data.PropertyDefinition -import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintCatalogService import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResourceAssignment -import org.python.jline.console.internal.ConsoleRunner.property 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.HttpHeaders -import org.springframework.http.HttpStatus -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.function.Consumer -import kotlin.test.BeforeTest -import org.h2.value.DataType.readValue -import java.util.* -import org.h2.value.DataType.readValue -import org.python.bouncycastle.asn1.x500.style.RFC4519Style.l -import org.h2.value.DataType.readValue import org.onap.ccsdk.cds.blueprintsprocessor.resource.api.ErrorMessage import org.onap.ccsdk.cds.blueprintsprocessor.resource.api.ResourceController -import java.lang.reflect.Array +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants @RunWith(SpringRunner::class) @@ -233,7 +217,7 @@ class ResourceControllerTest { resourceAssignment.dictionaryName = "dd$prefix" resourceAssignment.dictionarySource = "source$prefix" resourceAssignment.version = 2 - resourceAssignment.status = "SUCCESS" + resourceAssignment.status = BluePrintConstants.STATUS_SUCCESS resourceAssignment.property = property return resourceAssignment } diff --git a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateControllerTest.kt b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateControllerTest.kt index 649f6b5fe..c3a718e11 100644 --- a/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateControllerTest.kt +++ b/ms/blueprintsprocessor/modules/inbounds/resource-api/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/resource/api/TemplateControllerTest.kt @@ -17,11 +17,9 @@ package org.onap.ccsdk.cds.blueprintsprocessor.resource.api import kotlinx.coroutines.runBlocking -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintCoreConfiguration -import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService import org.onap.ccsdk.cds.controllerblueprints.core.deleteDir import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintCatalogService import org.slf4j.LoggerFactory @@ -29,18 +27,13 @@ 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.HttpStatus 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.io.File -import java.nio.file.Paths import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.assertTrue @RunWith(SpringRunner::class) @WebFluxTest @@ -137,7 +130,7 @@ class TemplateControllerTest { webTestClient .get() .uri("/api/v1/template?bpName=$blueprintName&bpVersion=$blueprintVersion" + - "&artifactName=$templatePrefix&resolutionKey=bob") + "&artifactName=$templatePrefix&resolutionKey=notFound") .exchange() .expectStatus().isNotFound } -- cgit 1.2.3-korg