diff options
3 files changed, 473 insertions, 0 deletions
diff --git a/components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json b/components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json new file mode 100644 index 000000000..7026d09f6 --- /dev/null +++ b/components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json @@ -0,0 +1,67 @@ +{ + "description": "This is Resource Deletion Component API", + "version": "1.0.0", + "attributes": { + "result": { + "description": "A map of [artifact-prefix]: { nDeletedTemplates, nDeletedResources }", + "required": true, + "type": "map" + }, + "success": { + "required": true, + "type": "boolean" + } + }, + "capabilities": { + "component-node": { + "type": "tosca.capabilities.Node" + } + }, + "interfaces": { + "ResourceDeletionComponent": { + "operations": { + "process": { + "inputs": { + "resolution-key": { + "description": "Resolution key associated with stored assignments and templates. Required if resource-type + resource-id are empty", + "required": false, + "type": "string" + }, + "resource-type": { + "description": "Resource-type associated with stored assignments and templates. Required if resolution-key is empty. Must be used together with resource-id", + "required": false, + "type": "string" + }, + "resource-id": { + "description": "Resource-id associated with stored assignments and templates. Required if resolution-key is empty. Must be used together with resource-type", + "required": false, + "type": "string" + }, + "artifact-prefix-names": { + "required": true, + "description": "Template , Resource Assignment Artifact Prefix names", + "type": "list", + "entry_schema": { + "type": "string" + } + }, + "last-n-occurrences": { + "description": "Only delete last N occurrences", + "required": false, + "default": null, + "type": "integer" + }, + "fail-on-empty": { + "description": "Determines if the component should fail when nothing was deleted", + "required": false, + "default": false, + "type": "boolean" + } + }, + "outputs": {} + } + } + } + }, + "derived_from": "tosca.nodes.Component" +} diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt new file mode 100644 index 000000000..86bcb4c54 --- /dev/null +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt @@ -0,0 +1,130 @@ +/* + * Copyright © 2022 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.functions.resource.resolution + +import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE +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.services.execution.AbstractComponentFunction +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive +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.stereotype.Component + +@Component("component-resource-deletion") +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +open class ResourceDeletionComponent( + private val resourceResolutionDBService: ResourceResolutionDBService, + private val templateResolutionService: TemplateResolutionService +) : AbstractComponentFunction() { + + companion object { + const val INPUT_LAST_N_OCCURRENCES = "last-n-occurrences" + const val INPUT_FAIL_ON_EMPTY = "fail-on-empty" + const val ATTRIBUTE_RESULT = "result" + const val ATTRIBUTE_SUCCESS = "success" + } + + data class DeletionResult(val nDeletedTemplates: Int, val nDeletedResources: Int) + + override suspend fun processNB(executionRequest: ExecutionServiceInput) { + bluePrintRuntimeService.setNodeTemplateAttributeValue( + nodeTemplateName, ATTRIBUTE_RESULT, emptyMap<String, Any>().asJsonNode() + ) + bluePrintRuntimeService.setNodeTemplateAttributeValue( + nodeTemplateName, ATTRIBUTE_SUCCESS, false.asJsonPrimitive() + ) + + val resolutionKey = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY)?.textValue() ?: "" + val resourceId = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOURCE_ID)?.textValue() ?: "" + val resourceType = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE)?.textValue() ?: "" + + val resultMap = when { + resolutionKey.isNotBlank() -> runDelete(byResolutionKey(resolutionKey)) + resourceType.isNotBlank() && resourceId.isNotBlank() -> + runDelete(byResourceTypeAndId(resourceType, resourceId)) + else -> throw BluePrintProcessorException( + "Please use resolution-key OR resource-type + resource-id. Values must not be blank" + ) + } + bluePrintRuntimeService.setNodeTemplateAttributeValue( + nodeTemplateName, ATTRIBUTE_RESULT, resultMap.asObjectNode() + ) + + getOptionalOperationInput(INPUT_FAIL_ON_EMPTY)?.booleanValue().takeIf { it == true }?.let { + resultMap.all { it.value.nDeletedResources == 0 && it.value.nDeletedTemplates == 0 } + .takeIf { it }?.let { + throw BluePrintProcessorException("No templates or resources were deleted") + } + } + + bluePrintRuntimeService.setNodeTemplateAttributeValue( + nodeTemplateName, ATTRIBUTE_SUCCESS, true.asJsonPrimitive() + ) + } + + private suspend fun runDelete(fn: suspend (String, String, String, Int?) -> DeletionResult): + Map<String, DeletionResult> { + val metadata = bluePrintRuntimeService.bluePrintContext().metadata!! + val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!! + val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!! + val artifactPrefixNamesNode = getOperationInput(INPUT_ARTIFACT_PREFIX_NAMES) + val artifactPrefixNames = JacksonUtils.getListFromJsonNode(artifactPrefixNamesNode, String::class.java) + val lastN = getOptionalOperationInput(INPUT_LAST_N_OCCURRENCES)?.let { + if (it.isInt) it.intValue() else null + } + + return artifactPrefixNames.associateWith { fn(blueprintName, blueprintVersion, it, lastN) } + } + + private fun byResolutionKey(resolutionKey: String): + suspend (String, String, String, Int?) -> DeletionResult = { + bpName, bpVersion, artifactName, lastN -> + val nDeleteTemplates = templateResolutionService.deleteTemplates( + bpName, bpVersion, artifactName, resolutionKey, lastN + ) + val nDeletedResources = resourceResolutionDBService.deleteResources( + bpName, bpVersion, artifactName, resolutionKey, lastN + ) + DeletionResult(nDeleteTemplates, nDeletedResources) + } + + private fun byResourceTypeAndId(resourceType: String, resourceId: String): + suspend (String, String, String, Int?) -> DeletionResult = { + bpName, bpVersion, artifactName, lastN -> + val nDeletedTemplates = templateResolutionService.deleteTemplates( + bpName, bpVersion, artifactName, resourceType, resourceId, lastN + ) + val nDeletedResources = resourceResolutionDBService.deleteResources( + bpName, bpVersion, artifactName, resourceType, resourceId, lastN + ) + DeletionResult(nDeletedTemplates, nDeletedResources) + } + + override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) { + addError(runtimeException.message ?: "Failed in ResourceDeletionComponent") + } +} diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt new file mode 100644 index 000000000..fb2587347 --- /dev/null +++ b/ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt @@ -0,0 +1,276 @@ +/* + * Copyright © 2022 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.functions.resource.resolution + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.NullNode +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.ATTRIBUTE_RESULT +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.ATTRIBUTE_SUCCESS +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.INPUT_FAIL_ON_EMPTY +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.INPUT_LAST_N_OCCURRENCES +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID +import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE +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.controllerblueprints.core.BluePrintConstants +import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive +import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType +import org.onap.ccsdk.cds.controllerblueprints.core.data.ServiceTemplate +import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintContext +import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService +import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils + +class ResourceDeletionComponentTest { + + private val blueprintName = "testCBA" + private val blueprintVersion = "1.0.0" + private val artifactNames = listOf("artifact-a", "artifact-b") + private val resolutionKey = "resolutionKey" + private val resourceId = "1" + private val resourceType = "ServiceInstance" + private val nodetemplateName = "resource-deletion" + private val executionRequest = ExecutionServiceInput() + + private lateinit var resourceResolutionDBService: ResourceResolutionDBService + private lateinit var templateResolutionService: TemplateResolutionService + private lateinit var resourceDeletionComponent: ResourceDeletionComponent + private lateinit var bluePrintRuntimeService: BluePrintRuntimeService<*> + + private val props = mutableMapOf<String, JsonNode>() + + private var success = slot<JsonNode>() + private var result = slot<JsonNode>() + + @Before + fun setup() { + bluePrintRuntimeService = spyk() + every { bluePrintRuntimeService.bluePrintContext() }.returns( + BluePrintContext( + ServiceTemplate().apply { + this.metadata = mutableMapOf( + BluePrintConstants.METADATA_TEMPLATE_VERSION to blueprintVersion, + BluePrintConstants.METADATA_TEMPLATE_NAME to blueprintName + ) + } + ) + ) + every { bluePrintRuntimeService.setNodeTemplateAttributeValue(nodetemplateName, ATTRIBUTE_SUCCESS, capture(success)) } + .answers { } + every { bluePrintRuntimeService.setNodeTemplateAttributeValue(nodetemplateName, ATTRIBUTE_RESULT, capture(result)) } + .answers { } + + resourceResolutionDBService = mockk() + templateResolutionService = mockk() + resourceDeletionComponent = ResourceDeletionComponent(resourceResolutionDBService, templateResolutionService) + resourceDeletionComponent.bluePrintRuntimeService = bluePrintRuntimeService + resourceDeletionComponent.nodeTemplateName = nodetemplateName + resourceDeletionComponent.executionServiceInput = executionRequest + resourceDeletionComponent.processId = "12" + resourceDeletionComponent.workflowName = "workflow" + resourceDeletionComponent.stepName = "step" + resourceDeletionComponent.interfaceName = "interfaceName" + resourceDeletionComponent.operationName = "operationName" + + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = NullNode.getInstance() + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = NullNode.getInstance() + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = NullNode.getInstance() + props[ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES] = artifactNames.asJsonType() + props[INPUT_FAIL_ON_EMPTY] = NullNode.getInstance() + props[INPUT_LAST_N_OCCURRENCES] = NullNode.getInstance() + resourceDeletionComponent.operationInputs = props + } + + @Test + fun `using resolution-key`() { + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive() + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(1) + + coEvery { + resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(2) + + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + + val expected = ResourceDeletionComponent.DeletionResult(1, 2).asJsonType() + val result: JsonNode = result.captured + assertEquals(expected, result[artifactNames[0]]) + assertEquals(expected, result[artifactNames[1]]) + assertEquals(true.asJsonPrimitive(), success.captured) + } + + @Test + fun `using resource-type and resource-id`() { + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId.asJsonPrimitive() + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType.asJsonPrimitive() + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resourceType, resourceId, null) + }.returns(2) + + coEvery { + resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resourceType, resourceId, null) + }.returns(4) + + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + + val expected = ResourceDeletionComponent.DeletionResult(2, 4).asJsonType() + val result: JsonNode = result.captured + assertEquals(expected, result[artifactNames[0]]) + assertEquals(expected, result[artifactNames[1]]) + assertEquals(true.asJsonPrimitive(), success.captured) + } + + @Test(expected = BluePrintProcessorException::class) + fun `using resource-type missing resource-id`() { + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType.asJsonPrimitive() + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + } + + @Test(expected = BluePrintProcessorException::class) + fun `using resource-id missing resource-type`() { + props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId.asJsonPrimitive() + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + } + + @Test + fun `attributes present when failing`() { + val threwException = runBlocking { + try { + resourceDeletionComponent.processNB(executionRequest) + false + } catch (e: Exception) { + true + } + } + assertTrue(threwException) + assertEquals(false.asJsonPrimitive(), success.captured) + assertEquals(emptyMap<String, Any>().asJsonNode(), result.captured) + } + + @Test + fun `last-n-occurrences`() { + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive() + props[INPUT_LAST_N_OCCURRENCES] = JacksonUtils.jsonNodeFromObject(3) + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, 3) + }.returns(3) + + coEvery { + resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, 3) + }.returns(6) + + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + + val expected = ResourceDeletionComponent.DeletionResult(3, 6).asJsonType() + val result: JsonNode = result.captured + assertEquals(expected, result[artifactNames[0]]) + assertEquals(expected, result[artifactNames[1]]) + assertEquals(true.asJsonPrimitive(), success.captured) + } + + @Test + fun `fail-on-empty nothing deleted`() { + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive() + props[INPUT_FAIL_ON_EMPTY] = true.asJsonPrimitive() + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(0) + + coEvery { + resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(0) + + val threwException = runBlocking { + try { + resourceDeletionComponent.processNB(executionRequest) + false + } catch (e: BluePrintProcessorException) { + true + } + } + + val expected = ResourceDeletionComponent.DeletionResult(0, 0).asJsonType() + val result: JsonNode = result.captured + assertTrue(threwException) + assertEquals(expected, result[artifactNames[0]]) + assertEquals(expected, result[artifactNames[1]]) + assertEquals(false.asJsonPrimitive(), success.captured) + } + + @Test + fun `fail-on-empty something deleted`() { + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive() + props[INPUT_FAIL_ON_EMPTY] = true.asJsonPrimitive() + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(1) + + coEvery { + resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.returns(1) + + runBlocking { resourceDeletionComponent.processNB(executionRequest) } + + val expected = ResourceDeletionComponent.DeletionResult(1, 1).asJsonType() + val result: JsonNode = result.captured + assertEquals(expected, result[artifactNames[0]]) + assertEquals(expected, result[artifactNames[1]]) + assertEquals(true.asJsonPrimitive(), success.captured) + } + + @Test + fun `db throws exception`() { + props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive() + + coEvery { + templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null) + }.throws(RuntimeException("DB failure!")) + + val threwException = runBlocking { + try { + resourceDeletionComponent.processNB(executionRequest) + false + } catch (e: Exception) { + true + } + } + + assertTrue(threwException) + assertEquals(false.asJsonPrimitive(), success.captured) + assertEquals(emptyMap<String, Any>().asJsonNode(), result.captured) + } +} |