Implement artifactory upload function

    Related: RE-3241
    Related: PRODX-54274

Change-Id: I02e5c0283df63a834340b6d78060dd327c1ba39b
diff --git a/resources/artifactory/scripts/upload.sh b/resources/artifactory/scripts/upload.sh
new file mode 100755
index 0000000..f4e780b
--- /dev/null
+++ b/resources/artifactory/scripts/upload.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+#
+#   :mod: `upload.sh` -- Artifactory upload helper
+#   =========================================================================
+#
+#   .. module:: upload.sh
+#       :platform: Unix
+#       :synopsys: this script uploads files to JFrog Artifactory
+#                  instance
+#
+#   .. envvar::
+#       :var ARTIFACTORY_USERNAME:  Access username
+#       :var ARTIFACTORY_PASSWORD:  Access password
+#       :var ARTIFACTORY_URL:       Artifactory api url
+#       :var ARTIFACTORY_TARGET:    Path at the repository for uploading
+#                                   including repository name
+#       :var ARTIFACTORY_PROPS:     Optional artifact properties
+#       :var FILE_TO_UPLOAD:        Path to local file to upload
+#
+#   .. requirements::
+#       * ``awk``
+#       * ``cat``
+#       * ``curl``
+#       * ``echo``
+#       * ``grep``
+#       * ``mktemp``
+#       * ``sed``
+#       * ``sha1sum``
+#       * ``touch``
+#
+
+cleanup_tmpfiles() {
+    trap EXIT
+    [ -d "${TMP_DIR}" ] && rm -rf "${TMP_DIR}"
+    exit 0
+}
+
+TMP_DIR=$(mktemp -d)
+trap cleanup_tmpfiles EXIT
+
+STDOUT_FILE=${TMP_DIR}/stdout
+STDERR_FILE=${TMP_DIR}/stderr
+HEADERS_FILE=${TMP_DIR}/headers
+touch "${STDOUT_FILE}" "${STDERR_FILE}" "${HEADERS_FILE}"
+
+exec 2>${STDERR_FILE}
+
+UPLOAD_PREFIX=${FILE_TO_UPLOAD%/*}
+FILE_NAME=${FILE_TO_UPLOAD##*/}
+
+EFFECTIVE_URL="${ARTIFACTORY_URL}/${ARTIFACTORY_TARGET}/${UPLOAD_PREFIX}/${FILE_NAME};${ARTIFACTORY_PROPS}"
+
+FILE_SHA1_CHECKSUM=$(sha1sum "${FILE_TO_UPLOAD}" | awk '{print $1}')
+
+curl \
+    --silent \
+    --show-error \
+    --location \
+    --dump-header "${HEADERS_FILE}" \
+    --output "${STDOUT_FILE}" \
+    --user "${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD}" \
+    --request PUT \
+    --header "X-Checksum-Sha1:${FILE_SHA1_CHECKSUM}" \
+    --upload-file "${FILE_TO_UPLOAD}" \
+    --url "${EFFECTIVE_URL}"
+
+EXIT_CODE=$?
+HTTP_RESPONSE_CODE=$(cat "${HEADERS_FILE}" | grep '^HTTP' | awk '{print $2}')
+
+if [ "${HTTP_RESPONSE_CODE:0:1}" != "2" ]; then
+    >&2 echo "Failed at ${EFFECTIVE_URL}"
+fi
+
+for outfile in "${STDOUT_FILE}" "${STDERR_FILE}"; do
+    sed -z -i \
+        -e 's|\n|\\n|g' \
+        -e 's|"|\\"|g' \
+        -e 's|\r||g' \
+        "${outfile}"
+done
+
+cat << EOF
+{
+    "stdout": "$(cat ${STDOUT_FILE})",
+    "stderr": "$(cat ${STDERR_FILE})",
+    "exit_code": ${EXIT_CODE},
+    "response_code": ${HTTP_RESPONSE_CODE:-null},
+}
+EOF
diff --git a/resources/artifactory/servers/artifactory-logs.json b/resources/artifactory/servers/artifactory-logs.json
new file mode 100644
index 0000000..6079b33
--- /dev/null
+++ b/resources/artifactory/servers/artifactory-logs.json
@@ -0,0 +1,6 @@
+{
+    "platformUrl": "https://artifactory-logs.infra.mirantis.net",
+    "artifactoryUrl": "https://artifactory-logs.infra.mirantis.net/artifactory",
+    "credentialsId": "artifactory",
+    "connectionRetry": 3
+}
\ No newline at end of file
diff --git a/resources/artifactory/servers/mcp-ci.json b/resources/artifactory/servers/mcp-ci.json
new file mode 100644
index 0000000..801bd25
--- /dev/null
+++ b/resources/artifactory/servers/mcp-ci.json
@@ -0,0 +1,6 @@
+{
+    "platformUrl": "https://artifactory.mcp.mirantis.net",
+    "artifactoryUrl": "https://artifactory.mcp.mirantis.net/artifactory",
+    "credentialsId": "artifactory",
+    "connectionRetry": 3
+}
\ No newline at end of file
diff --git a/vars/artifactory.groovy b/vars/artifactory.groovy
new file mode 100644
index 0000000..4c5447c
--- /dev/null
+++ b/vars/artifactory.groovy
@@ -0,0 +1,207 @@
+/**
+ * This method creates config Map object and calls upload(Map) method
+ *
+ * @param  server  a server ID - file name at resources/../servers
+ * @param  spec    a JSON representation of upload spec
+ * @return         a List object that contains upload results
+ */
+List upload(String server, String spec) {
+    return upload(server: server, spec: spec)
+}
+
+
+/**
+ * This method converts JSON representation of spec into Map object and calls
+ * upload(String, Map)
+ *
+ * @param  config  a Map object with params `[ server: String, spec: String or Map ]`
+ * @return         a List object that contains upload results
+ */
+List upload(Map config) {
+    if (config.spec?.getClass() in [java.util.LinkedHashMap, net.sf.json.JSONObject]) {
+        return upload(config.server, config.spec)
+    }
+    return upload(config.server, parseJSON(config.spec))
+}
+
+
+/**
+ * This method uploads files into artifactory instance
+ *
+ * Input spec example:
+ *     [ files: [[
+ *          pattern: "**",
+ *          target: "my-repository/path/to/upload",
+ *          props: "myPropKey1=myPropValue1;myPropKey2=myPropValue2", // optional
+ *     ],]
+ *
+ * Result example:
+ *     [[
+ *        localPath: "local/path/to/file.name",
+ *        remotePath: "/path/to/upload/local/path/to/file.name",
+ *        repo: "my-repository",
+ *        size: 12345,
+ *        uri: ... ,
+ *        checksums: [
+ *            md5: ... ,
+ *            sha1: ... ,
+ *            sha256: ... "
+ *        ],
+ *     ],]
+ *
+ * @param  server  a server ID - server name at resources/../servers
+ * @param  spec    a Map object with upload spec
+ * @return         a List object that contains upload results
+ */
+List upload(String server, Map spec) {
+    List result = []
+
+    Map artConfig = parseJSON(loadResource("artifactory/servers/${server}.json"))
+    String uploadScript = loadResource("artifactory/scripts/upload.sh")
+
+    List artCredentials = [usernamePassword(
+        credentialsId: artConfig.credentialsId,
+        usernameVariable: 'ARTIFACTORY_USERNAME',
+        passwordVariable: 'ARTIFACTORY_PASSWORD')]
+
+    List files = createFileListBySpec(spec)
+
+    retry(artConfig.get('connectionRetry', 1)) {
+        withCredentials(artCredentials) {
+            files.each{ file ->
+                String scriptRawResult
+                List envList = [
+                    "ARTIFACTORY_URL=${artConfig.artifactoryUrl}",
+                    "ARTIFACTORY_TARGET=${file.target}",
+                    "ARTIFACTORY_PROPS=${file.props}",
+                    "FILE_TO_UPLOAD=${file.name}"
+                ]
+                withEnv(envList) {
+                    scriptRawResult = sh \
+                        script: uploadScript,
+                        returnStdout: true
+                }
+
+                Map scriptResult = parseScriptResult(scriptRawResult)
+                Map uploadResult = parseJSON(scriptResult.stdout)
+                ['created', 'createdBy', 'downloadUri', 'mimeType', 'originalChecksums'].each {
+                    uploadResult.remove(it)
+                }
+                uploadResult.localPath = file.name
+                uploadResult.put('remotePath', uploadResult.remove('path'));
+                result << uploadResult
+            }
+        }
+    }
+    return result
+}
+
+
+/**
+ * This method looks up for local files to upload according to the spec
+ *
+ * @param  spec   a Map object with upload spec
+ * @return        a List object that contains found local files to upload
+ */
+List createFileListBySpec(Map spec) {
+    List result = []
+
+    Map jenkinsProps = [
+        "build.name": env.JOB_NAME,
+        "build.number": env.BUILD_NUMBER,
+        "build.timestamp": currentBuild.startTimeInMillis
+    ]
+
+    spec.files?.each{ specItem ->
+        Map targetProps = specItem.props?.split(';')?.findAll{ it.contains('=') }?.collectEntries{
+                List parts = it.split('=', 2)
+                return [(parts[0]): parts[1]]
+            } ?: [:]
+
+        String props = (jenkinsProps + targetProps)
+            .collect{ "${it.key}=${it.value}" }
+            .join(';')
+
+        if (!(specItem.pattern && specItem.target)) {
+            error "ArtifactoryUploader: Malformed upload spec:\n${spec}"
+        }
+
+        List targetFiles = []
+        try {
+            targetFiles = findFiles(glob: specItem.pattern)
+                .findAll{ !it.directory }
+                .collect{ it.path }
+        } catch (Exception e) {
+            error "ArtifactoryUploader: Unable to find files by pattern: ${specItem.pattern}"
+        }
+
+        targetFiles.each{ file ->
+            result << [ name: file, target: specItem.target, props: props ]
+        }
+    }
+    return result
+}
+
+
+/**
+ * This method parses uploading results
+ *
+ * @param  scriptRawResult   a JSON representation of uploading results
+ * @return                   a Map object that contains uploading results
+ */
+Map parseScriptResult(String scriptRawResult) {
+    Map result = parseJSON scriptRawResult
+
+    if (result.exit_code == 0 &&
+        result.response_code &&
+        result.response_code in 200..299 &&
+        result.stdout) { return result }
+
+    String errorMessage = "ArtifactoryUploader: Upload failed"
+    if (result.stdout) {
+        errorMessage += "\nStdout: ${result.stdout}"
+    }
+    if (result.stderr) {
+        errorMessage += "\nStderr: ${result.stderr}"
+    }
+    if (result.response_code) {
+        errorMessage += "\nResponse code: ${result.response_code}"
+    }
+    error errorMessage
+}
+
+
+/**
+ * This method loads resourcse file from the library
+ *
+ * @param  path   path to the resource file
+ * @return        content of the resource file
+ */
+String loadResource(String path) {
+    try {
+        return libraryResource(path)
+    } catch (Exception e) {
+        error "ArtifactoryUploader: Unable to load resource: ${path}"
+    }
+}
+
+
+/**
+ * This method converts a JSON representation into an object
+ *
+ * @param  text   a JSON content
+ * @return        a Map or List object
+ */
+Map parseJSON(String text) {
+    def json = new groovy.json.JsonSlurper()
+    Map result
+    try {
+        // result = readJSON text: text
+        result = json.parseText(text)
+    } catch (Exception e) {
+        json = null
+        error "ArtifactoryUploader: Unable to parse JSON:\n${text}"
+    }
+    json = null
+    return result
+}