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
+}