6b575a60aed42b23ceae18e5f21bc4f30d44dc96
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / k8s-profile-upload / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / k8s / profile / upload / K8sProfileUploadComponent.kt
1 /*
2  * Copyright © 2017-2018 AT&T Intellectual Property.
3  * Modifications Copyright © 2019 IBM.
4  * Modifications Copyright © 2020 Orange.
5  * Modifications Copyright © 2020 Deutsche Telekom AG.
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19
20 package org.onap.ccsdk.cds.blueprintsprocessor.functions.k8s.profile.upload
21
22 import com.fasterxml.jackson.databind.JsonNode
23 import com.fasterxml.jackson.databind.node.ArrayNode
24 import com.fasterxml.jackson.databind.node.ObjectNode
25 import org.apache.commons.io.FileUtils
26 import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertiesService
27 import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
28 import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants
29 import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionService
30 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
31 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
32 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
33 import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode
34 import org.onap.ccsdk.cds.controllerblueprints.core.data.ArtifactDefinition
35 import org.onap.ccsdk.cds.controllerblueprints.core.returnNullIfMissing
36 import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintVelocityTemplateService
37 import org.onap.ccsdk.cds.controllerblueprints.core.utils.ArchiveType
38 import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils
39 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
40 import org.slf4j.LoggerFactory
41 import org.springframework.beans.factory.config.ConfigurableBeanFactory
42 import org.springframework.context.annotation.Scope
43 import org.springframework.stereotype.Component
44 import org.yaml.snakeyaml.Yaml
45 import java.io.File
46 import java.nio.file.Files
47 import java.nio.file.Path
48 import java.nio.file.Paths
49
50 @Component("component-k8s-profile-upload")
51 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
52 open class K8sProfileUploadComponent(
53     private var bluePrintPropertiesService: BluePrintPropertiesService,
54     private val resourceResolutionService: ResourceResolutionService
55 ) :
56
57     AbstractComponentFunction() {
58
59     companion object {
60
61         const val INPUT_K8S_PROFILE_NAME = "k8s-rb-profile-name"
62         const val INPUT_K8S_DEFINITION_NAME = "k8s-rb-definition-name"
63         const val INPUT_K8S_DEFINITION_VERSION = "k8s-rb-definition-version"
64         const val INPUT_K8S_PROFILE_NAMESPACE = "k8s-rb-profile-namespace"
65         const val INPUT_K8S_PROFILE_SOURCE = "k8s-rb-profile-source"
66         const val INPUT_RESOURCE_ASSIGNMENT_MAP = "resource-assignment-map"
67         const val INPUT_ARTIFACT_PREFIX_NAMES = "artifact-prefix-names"
68
69         const val OUTPUT_STATUSES = "statuses"
70         const val OUTPUT_SKIPPED = "skipped"
71         const val OUTPUT_UPLOADED = "uploaded"
72         const val OUTPUT_ERROR = "error"
73     }
74
75     private val log = LoggerFactory.getLogger(K8sProfileUploadComponent::class.java)!!
76
77     override suspend fun processNB(executionRequest: ExecutionServiceInput) {
78         log.info("Triggering K8s Profile Upload component logic.")
79
80         val inputParameterNames = arrayOf(
81             INPUT_K8S_PROFILE_NAME,
82             INPUT_K8S_DEFINITION_NAME,
83             INPUT_K8S_DEFINITION_VERSION,
84             INPUT_K8S_PROFILE_NAMESPACE,
85             INPUT_K8S_PROFILE_SOURCE,
86             INPUT_ARTIFACT_PREFIX_NAMES
87         )
88         var outputPrefixStatuses = mutableMapOf<String, String>()
89         var inputParamsMap = mutableMapOf<String, JsonNode?>()
90
91         inputParameterNames.forEach {
92             inputParamsMap[it] = getOptionalOperationInput(it)?.returnNullIfMissing()
93         }
94
95         log.info("Getting the template prefixes")
96         val prefixList: ArrayList<String> = getTemplatePrefixList(inputParamsMap[INPUT_ARTIFACT_PREFIX_NAMES])
97
98         log.info("Iterating over prefixes in resource assignment map.")
99         for (prefix in prefixList) {
100             // Prefilling prefix sucess status
101             outputPrefixStatuses.put(prefix, OUTPUT_SKIPPED)
102             // Resource assignment map is organized by prefixes, in each iteraton we work only
103             // on one section of resource assignment map
104             val prefixNode: JsonNode = operationInputs[INPUT_RESOURCE_ASSIGNMENT_MAP]?.get(prefix) ?: continue
105             val assignmentMapPrefix = JacksonUtils.jsonNode(prefixNode.toPrettyString()) as ObjectNode
106
107             // We are copying the map because for each prefix it might be completed with a different data
108             var prefixInputParamsMap = inputParamsMap.toMutableMap()
109             prefixInputParamsMap.forEach { (inputParamName, value) ->
110                 if (value == null) {
111                     val mapValue = assignmentMapPrefix?.get(inputParamName)
112                     log.debug("$inputParamName value was $value so we fetch $mapValue")
113                     prefixInputParamsMap[inputParamName] = mapValue
114                 }
115             }
116
117             // For clarity we pull out the required fields
118             val profileName: String? = prefixInputParamsMap[INPUT_K8S_PROFILE_NAME]?.returnNullIfMissing()?.asText()
119             val definitionName: String? = prefixInputParamsMap[INPUT_K8S_DEFINITION_NAME]?.returnNullIfMissing()?.asText()
120             val definitionVersion: String? = prefixInputParamsMap[INPUT_K8S_DEFINITION_VERSION]?.returnNullIfMissing()?.asText()
121
122             val k8sProfileUploadConfiguration = K8sProfileUploadConfiguration(bluePrintPropertiesService)
123
124             // Creating API connector
125             var api = K8sPluginApi(
126                 k8sProfileUploadConfiguration.getProperties().username,
127                 k8sProfileUploadConfiguration.getProperties().password,
128                 k8sProfileUploadConfiguration.getProperties().url,
129                 definitionName,
130                 definitionVersion
131             )
132
133             if ((profileName == null) || (definitionName == null) || (definitionVersion == null)) {
134                 log.warn("Prefix $prefix does not have required data for us to continue.")
135             } else if (!api.hasDefinition()) {
136                 log.warn("K8s RB Definition ($definitionName/$definitionVersion) not found ")
137             } else if (profileName == "") {
138                 log.warn("K8s rb profile name is empty! Either define profile name to use or choose default")
139             } else if (api.hasProfile(profileName)) {
140                 log.info("Profile Already Existing - skipping upload")
141             } else {
142                 log.info("Uploading K8s Profile..")
143                 outputPrefixStatuses.put(prefix, OUTPUT_ERROR)
144                 val profileNamespace: String? = prefixInputParamsMap[INPUT_K8S_PROFILE_NAMESPACE]?.returnNullIfMissing()?.asText()
145                 var profileSource: String? = prefixInputParamsMap[INPUT_K8S_PROFILE_SOURCE]?.returnNullIfMissing()?.asText()
146                 if (profileNamespace == null)
147                     throw BluePrintProcessorException("Profile $profileName namespace is missing")
148                 if (profileSource == null) {
149                     profileSource = profileName
150                     log.info("Profile name used instead of profile source")
151                 }
152                 val bluePrintContext = bluePrintRuntimeService.bluePrintContext()
153                 val artifact: ArtifactDefinition = bluePrintContext.nodeTemplateArtifact(nodeTemplateName, profileSource)
154                 if (artifact.type != BluePrintConstants.MODEL_TYPE_ARTIFACT_K8S_PROFILE)
155                     throw BluePrintProcessorException(
156                         "Unexpected profile artifact type for profile source " +
157                             "$profileSource. Expecting: $artifact.type"
158                     )
159                 var profile = K8sProfile()
160                 profile.profileName = profileName
161                 profile.rbName = definitionName
162                 profile.rbVersion = definitionVersion
163                 profile.namespace = profileNamespace
164                 val profileFilePath: Path = prepareProfileFile(profileName, profileSource, artifact.file)
165                 api.createProfile(profile)
166                 api.uploadProfileContent(profile, profileFilePath)
167
168                 log.info("K8s Profile Upload Completed")
169                 outputPrefixStatuses.put(prefix, OUTPUT_UPLOADED)
170             }
171         }
172         bluePrintRuntimeService.setNodeTemplateAttributeValue(
173             nodeTemplateName,
174             OUTPUT_STATUSES,
175             outputPrefixStatuses.asJsonNode()
176         )
177     }
178
179     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
180         bluePrintRuntimeService.getBluePrintError().addError(runtimeException.message!!)
181     }
182
183     private fun getTemplatePrefixList(node: JsonNode?): ArrayList<String> {
184         var result = ArrayList<String>()
185         when (node) {
186             is ArrayNode -> {
187                 val arrayNode = node.toList()
188                 for (prefixNode in arrayNode)
189                     result.add(prefixNode.asText())
190             }
191             is ObjectNode -> {
192                 result.add(node.asText())
193             }
194         }
195         return result
196     }
197
198     private suspend fun prepareProfileFile(k8sRbProfileName: String, ks8ProfileSource: String, ks8ProfileLocation: String): Path {
199         val bluePrintContext = bluePrintRuntimeService.bluePrintContext()
200         val bluePrintBasePath: String = bluePrintContext.rootPath
201         val profileSourceFileFolderPath: String = bluePrintBasePath.plus(File.separator)
202             .plus(ks8ProfileLocation)
203         val profileFilePathTarGz: String = profileSourceFileFolderPath.plus(".tar.gz")
204         val profileFilePathTgz: String = profileSourceFileFolderPath.plus(".tgz")
205
206         if (Paths.get(profileFilePathTarGz).toFile().exists())
207             return Paths.get(profileFilePathTarGz)
208         else if (Paths.get(profileFilePathTgz).toFile().exists())
209             return Paths.get(profileFilePathTgz)
210         else if (Paths.get(profileSourceFileFolderPath).toFile().exists()) {
211             log.info("Profile building started from source $ks8ProfileSource")
212             val properties: MutableMap<String, Any> = mutableMapOf()
213             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] = false
214             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = ""
215             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = ""
216             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = ""
217             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] = 1
218             properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_SUMMARY] = false
219             val resolutionResult: Pair<String, JsonNode> = resourceResolutionService.resolveResources(
220                 bluePrintRuntimeService,
221                 nodeTemplateName,
222                 ks8ProfileSource,
223                 properties
224             )
225             val tempMainPath: File = createTempDir("k8s-profile-", "")
226             val tempProfilePath: File = createTempDir("content-", "", tempMainPath)
227
228             try {
229                 val manifestFiles: ArrayList<File>? = readManifestFiles(
230                     Paths.get(profileSourceFileFolderPath).toFile(),
231                     tempProfilePath
232                 )
233                 if (manifestFiles != null) {
234                     templateLocation(
235                         Paths.get(profileSourceFileFolderPath).toFile(), resolutionResult.second,
236                         tempProfilePath, manifestFiles
237                     )
238                 } else
239                     throw BluePrintProcessorException("Manifest file is missing")
240                 // Preparation of the final profile content
241                 val finalProfileFilePath = Paths.get(
242                     tempMainPath.toString().plus(File.separator).plus(
243                         "$k8sRbProfileName.tar.gz"
244                     )
245                 )
246                 if (!BluePrintArchiveUtils.compress(
247                         tempProfilePath, finalProfileFilePath.toFile(),
248                         ArchiveType.TarGz
249                     )
250                 ) {
251                     throw BluePrintProcessorException("Profile compression has failed")
252                 }
253                 FileUtils.deleteDirectory(tempProfilePath)
254
255                 return finalProfileFilePath
256             } catch (t: Throwable) {
257                 FileUtils.deleteDirectory(tempMainPath)
258                 throw t
259             }
260         } else
261             throw BluePrintProcessorException("Profile source $ks8ProfileSource is missing in CBA folder")
262     }
263
264     private fun readManifestFiles(profileSource: File, destinationFolder: File): ArrayList<File>? {
265         val directoryListing: Array<File>? = profileSource.listFiles()
266         var result: ArrayList<File>? = null
267         if (directoryListing != null) {
268             for (child in directoryListing) {
269                 if (!child.isDirectory && child.name.toLowerCase() == "manifest.yaml") {
270                     child.bufferedReader().use { inr ->
271                         val manifestYaml = Yaml()
272                         val manifestObject: Map<String, Any> = manifestYaml.load(inr)
273                         val typeObject: MutableMap<String, Any>? = manifestObject["type"] as MutableMap<String, Any>?
274                         if (typeObject != null) {
275                             result = ArrayList<File>()
276                             val valuesObject = typeObject["values"]
277                             if (valuesObject != null) {
278                                 result!!.add(File(destinationFolder.toString().plus(File.separator).plus(valuesObject)))
279                                 result!!.add(File(destinationFolder.toString().plus(File.separator).plus(child.name)))
280                             }
281                             (typeObject["configresource"] as ArrayList<*>?)?.forEach { item ->
282                                 val fileInfo: Map<String, Any> = item as Map<String, Any>
283                                 val filePath = fileInfo["filepath"]
284                                 val chartPath = fileInfo["chartpath"]
285                                 if (filePath == null || chartPath == null)
286                                     log.error("One configresource in manifest was skipped because of the wrong format")
287                                 else {
288                                     result!!.add(File(destinationFolder.toString().plus(File.separator).plus(filePath)))
289                                 }
290                             }
291                         }
292                     }
293                     break
294                 }
295             }
296         }
297         return result
298     }
299
300     private fun templateLocation(
301         location: File,
302         params: JsonNode,
303         destinationFolder: File,
304         manifestFiles: ArrayList<File>
305     ) {
306         val directoryListing: Array<File>? = location.listFiles()
307         if (directoryListing != null) {
308             for (child in directoryListing) {
309                 var newDestinationFolder = destinationFolder.toPath()
310                 if (child.isDirectory)
311                     newDestinationFolder = Paths.get(destinationFolder.toString().plus(File.separator).plus(child.name))
312
313                 templateLocation(child, params, newDestinationFolder.toFile(), manifestFiles)
314             }
315         } else if (!location.isDirectory) {
316             if (location.extension.toLowerCase() == "vtl") {
317                 templateFile(location, params, destinationFolder, manifestFiles)
318             } else {
319                 val finalFilePath = Paths.get(
320                     destinationFolder.path.plus(File.separator)
321                         .plus(location.name)
322                 ).toFile()
323                 if (isFileInTheManifestFiles(finalFilePath, manifestFiles)) {
324                     if (!destinationFolder.exists())
325                         Files.createDirectories(destinationFolder.toPath())
326                     FileUtils.copyFile(location, finalFilePath)
327                 }
328             }
329         }
330     }
331
332     private fun isFileInTheManifestFiles(file: File, manifestFiles: ArrayList<File>): Boolean {
333         manifestFiles.forEach { fileFromManifest ->
334             if (fileFromManifest.toString().toLowerCase() == file.toString().toLowerCase())
335                 return true
336         }
337         return false
338     }
339
340     private fun templateFile(
341         templatedFile: File,
342         params: JsonNode,
343         destinationFolder: File,
344         manifestFiles: ArrayList<File>
345     ) {
346         val finalFile = File(
347             destinationFolder.path.plus(File.separator)
348                 .plus(templatedFile.nameWithoutExtension)
349         )
350         if (!isFileInTheManifestFiles(finalFile, manifestFiles))
351             return
352         val fileContent = templatedFile.bufferedReader().readText()
353         val finalFileContent = BluePrintVelocityTemplateService.generateContent(
354             fileContent,
355             params, true
356         )
357         if (!destinationFolder.exists())
358             Files.createDirectories(destinationFolder.toPath())
359         finalFile.bufferedWriter().use { out -> out.write(finalFileContent) }
360     }
361 }