Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 1 | /** |
| 2 | * |
| 3 | * Mirror Docker images |
| 4 | * |
| 5 | * Expected parameters: |
| 6 | * TARGET_REGISTRY_CREDENTIALS_ID Credentials for target Docker Registry |
| 7 | * TARGET_REGISTRY Target Docker Registry name |
| 8 | * REGISTRY_URL Target Docker Registry URL |
| 9 | * IMAGE_TAG Tag to use when pushing images |
azvyagintsev | d162b91 | 2018-07-26 10:52:14 +0200 | [diff] [blame] | 10 | * SOURCE_IMAGE_TAG Tag to use when pulling images(optional,if SOURCE_IMAGE_TAG has been found) |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 11 | * SET_DEFAULT_ARTIFACTORY_PROPERTIES Add extra props. directly to artifactory, |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 12 | * IMAGE_LIST List of images to mirror |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 13 | * Example: docker.elastic.co/elasticsearch/elasticsearch:5.4.1 docker-prod-local.docker.mirantis.net/mirantis/external/docker.elastic.co/elasticsearch |
| 14 | * docker.elastic.co/elasticsearch/elasticsearch:SUBS_SOURCE_IMAGE_TAG docker-prod-local.docker.mirantis.net/mirantis/external/elasticsearch:${IMAGE_TAG}* Will be proceed like: |
| 15 | * docker tag docker.elastic.co/elasticsearch/elasticsearch:5.4.1 docker-prod-local.docker.mirantis.net/mirantis/external/docker.elastic.co/elasticsearch/elasticsearch:5.4.1 |
| 16 | * |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 17 | * |
| 18 | */ |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 19 | import java.util.regex.Pattern |
Alexander Evseev | 89b0da4 | 2018-12-10 13:14:04 +0000 | [diff] [blame] | 20 | import groovy.json.JsonSlurper |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 21 | |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 22 | common = new com.mirantis.mk.Common() |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 23 | jenkinsUtils = new com.mirantis.mk.JenkinsUtils() |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 24 | external = false |
| 25 | externalMarker = '/mirantis/external/' |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 26 | |
Aleksey Zvyagintsev | 4a80421 | 2019-02-07 13:02:31 +0000 | [diff] [blame] | 27 | slaveNode = env.SLAVE_NODE ?: 'virtual' |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 28 | setDefaultArtifactoryProperties = env.SET_DEFAULT_ARTIFACTORY_PROPERTIES ?: true |
| 29 | |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 30 | |
| 31 | |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 32 | def getImageName(String image) { |
| 33 | def regex = Pattern.compile('(?:.+/)?([^:]+)(?::.+)?') |
| 34 | def matcher = regex.matcher(image) |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 35 | if (matcher.find()) { |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 36 | def imageName = matcher.group(1) |
| 37 | return imageName |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 38 | } else { |
| 39 | error("Wrong format of image name.") |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 40 | } |
| 41 | } |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 42 | |
Denis Egorenko | 343a571 | 2018-12-10 19:08:45 +0400 | [diff] [blame] | 43 | def getImageInfo(String imageName) { |
| 44 | String unique_image_id = sh( |
Kyrylo Mashchenko | a86912d | 2018-12-21 08:31:47 +0000 | [diff] [blame] | 45 | script: "docker inspect --format='{{index .RepoDigests 0}}' '${imageName}'", |
| 46 | returnStdout: true, |
Denis Egorenko | 343a571 | 2018-12-10 19:08:45 +0400 | [diff] [blame] | 47 | ).trim() |
| 48 | String imageSha256 = unique_image_id.tokenize(':')[1] |
| 49 | common.infoMsg("Docker ${imageName} image sha256 is ${imageSha256}") |
Kyrylo Mashchenko | a86912d | 2018-12-21 08:31:47 +0000 | [diff] [blame] | 50 | return [ 'id': unique_image_id, 'sha256': imageSha256 ] |
Denis Egorenko | 343a571 | 2018-12-10 19:08:45 +0400 | [diff] [blame] | 51 | } |
| 52 | |
| 53 | def imageURL(String registry, String imageName, String sha256) { |
| 54 | def ret = new URL("https://${registry}/artifactory/api/search/checksum?sha256=${sha256}").getText() |
| 55 | // Most probably, we would get many images, especially for external images. We need to guess |
| 56 | // exactly one, which we pushing now |
| 57 | def tgtGuessImage = imageName.replace(':', '/').replace(registry, '') |
| 58 | ArrayList img_data = new JsonSlurper().parseText(ret)['results'] |
| 59 | def tgtImgUrl = img_data*.uri.find { it.contains(tgtGuessImage) } |
| 60 | if (tgtImgUrl) { |
| 61 | return tgtImgUrl |
| 62 | } else { |
| 63 | error("Can't find image ${imageName} in registry ${registry} with sha256: ${sha256}!") |
| 64 | } |
| 65 | } |
| 66 | |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 67 | timeout(time: 4, unit: 'HOURS') { |
| 68 | node(slaveNode) { |
Kirill Mashchenko | 2c67fe6 | 2018-12-26 02:18:19 +0400 | [diff] [blame] | 69 | def user = jenkinsUtils.currentUsername() |
Denis Egorenko | 899ee32 | 2018-11-19 17:35:50 +0400 | [diff] [blame] | 70 | currentBuild.description = "${user}: [${env.SOURCE_IMAGE_TAG} => ${env.IMAGE_TAG}]\n${env.IMAGE_LIST}" |
Jakub Josef | a63f986 | 2018-01-11 17:58:38 +0100 | [diff] [blame] | 71 | try { |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 72 | allowedGroups = ['release-engineering'] |
Roman Vialov | d6b9e83 | 2019-02-04 12:46:54 +0300 | [diff] [blame] | 73 | releaseTags = ['proposed', 'release', '2018', '2019', '2020'] |
Maxim Rasskazov | 8505de2 | 2019-06-21 14:56:12 +0400 | [diff] [blame] | 74 | tags = [env.IMAGE_TAG] |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 75 | tagInRelease = tags.any { tag -> releaseTags.any { tag.contains(it) } } |
| 76 | if (tagInRelease) { |
| 77 | if (!jenkinsUtils.currentUserInGroups(allowedGroups)) { |
Kirill Mashchenko | dfa2e59 | 2019-01-11 11:41:12 +0400 | [diff] [blame] | 78 | throw new Exception("You - ${user} - don't have permissions to run this job with tags ${tags}!") |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 79 | } else { |
Kirill Mashchenko | 2c67fe6 | 2018-12-26 02:18:19 +0400 | [diff] [blame] | 80 | echo "User `${user}` belongs to one of groups `${allowedGroups}`. Proceeding..." |
Kirill Mashchenko | 343973c | 2018-12-25 15:52:24 +0400 | [diff] [blame] | 81 | } |
| 82 | } |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 83 | stage("Mirror Docker Images") { |
| 84 | |
Jakub Josef | a63f986 | 2018-01-11 17:58:38 +0100 | [diff] [blame] | 85 | def images = IMAGE_LIST.tokenize('\n') |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 86 | def imageName, sourceImage, targetRegistryPath, imageArray |
| 87 | for (image in images) { |
| 88 | if (image.trim().indexOf(' ') == -1) { |
| 89 | error("Wrong format of image and target repository input") |
Jakub Josef | a63f986 | 2018-01-11 17:58:38 +0100 | [diff] [blame] | 90 | } |
| 91 | imageArray = image.trim().tokenize(' ') |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 92 | sourceImage = imageArray[0] |
| 93 | if (sourceImage.contains('SUBS_SOURCE_IMAGE_TAG')) { |
| 94 | common.warningMsg("Replacing SUBS_SOURCE_IMAGE_TAG => ${env.SOURCE_IMAGE_TAG}") |
| 95 | sourceImage = sourceImage.replace('SUBS_SOURCE_IMAGE_TAG', env.SOURCE_IMAGE_TAG) |
azvyagintsev | d162b91 | 2018-07-26 10:52:14 +0200 | [diff] [blame] | 96 | } |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 97 | targetRegistryPath = imageArray[1] |
| 98 | targetRegistry = imageArray[1].split('/')[0] |
| 99 | imageName = getImageName(sourceImage) |
| 100 | targetImageFull = "${targetRegistryPath}/${imageName}:${env.IMAGE_TAG}" |
Denis Egorenko | 2ee651c | 2018-11-23 17:41:38 +0400 | [diff] [blame] | 101 | |
| 102 | def mcp_artifactory = new com.mirantis.mcp.MCPArtifactory() |
| 103 | if (targetImageFull.contains(externalMarker)) { |
| 104 | external = true |
| 105 | // check if images exists - raise error, as we don't want to rewrite existing one |
| 106 | def imageRepo = targetRegistryPath - targetRegistry |
Denis Egorenko | 1b207d1 | 2018-11-26 15:35:21 +0400 | [diff] [blame] | 107 | if (mcp_artifactory.imageExists(env.REGISTRY_URL, "${imageRepo}/${imageName}", env.IMAGE_TAG)) { |
Denis Egorenko | 2ee651c | 2018-11-23 17:41:38 +0400 | [diff] [blame] | 108 | error("Image ${targetImageFull} already exists!") |
| 109 | } |
| 110 | } |
| 111 | |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 112 | srcImage = docker.image(sourceImage) |
azvyagintsev | 86ba4a0 | 2018-10-03 19:21:28 +0300 | [diff] [blame] | 113 | common.retry(3, 5) { |
| 114 | srcImage.pull() |
| 115 | } |
Denis Egorenko | 343a571 | 2018-12-10 19:08:45 +0400 | [diff] [blame] | 116 | source_image_sha256 = getImageInfo(sourceImage)['sha256'] |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 117 | // Use sh-docker call for tag, due magic code in plugin: |
| 118 | // https://github.com/jenkinsci/docker-workflow-plugin/blob/docker-workflow-1.17/src/main/resources/org/jenkinsci/plugins/docker/workflow/Docker.groovy#L168-L170 |
| 119 | sh("docker tag ${srcImage.id} ${targetImageFull}") |
| 120 | common.infoMsg("Attempt to push docker image into remote registry: ${env.REGISTRY_URL}") |
azvyagintsev | 86ba4a0 | 2018-10-03 19:21:28 +0300 | [diff] [blame] | 121 | common.retry(3, 5) { |
| 122 | docker.withRegistry(env.REGISTRY_URL, env.TARGET_REGISTRY_CREDENTIALS_ID) { |
Alexander Evseev | 3a37d08 | 2018-12-10 12:46:39 +0000 | [diff] [blame] | 123 | sh("docker push ${targetImageFull}") |
azvyagintsev | 86ba4a0 | 2018-10-03 19:21:28 +0300 | [diff] [blame] | 124 | } |
Alexander Evseev | d70ef00 | 2018-10-02 13:01:27 +0200 | [diff] [blame] | 125 | } |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 126 | def buildTime = new Date().format("yyyyMMdd-HH:mm:ss.SSS", TimeZone.getTimeZone('UTC')) |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 127 | |
| 128 | if (setDefaultArtifactoryProperties) { |
| 129 | common.infoMsg("Processing artifactory props for : ${targetImageFull}") |
| 130 | LinkedHashMap artifactoryProperties = [:] |
Denis Egorenko | 343a571 | 2018-12-10 19:08:45 +0400 | [diff] [blame] | 131 | def tgtImageInfo = getImageInfo(targetImageFull) |
| 132 | def tgt_image_sha256 = tgtImageInfo['sha256'] |
| 133 | def unique_image_id = tgtImageInfo['id'] |
| 134 | def tgtImgUrl = imageURL(targetRegistry, targetImageFull, tgt_image_sha256) - '/manifest.json' |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 135 | artifactoryProperties = [ |
Kyrylo Mashchenko | a86912d | 2018-12-21 08:31:47 +0000 | [diff] [blame] | 136 | 'com.mirantis.targetTag' : env.IMAGE_TAG, |
| 137 | 'com.mirantis.uniqueImageId': unique_image_id, |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 138 | ] |
| 139 | if (external) { |
| 140 | artifactoryProperties << ['com.mirantis.externalImage': external] |
| 141 | } |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 142 | def historyProperties = [] |
Denis Egorenko | 4c9b3c5 | 2018-12-24 16:11:54 +0400 | [diff] [blame] | 143 | try { |
| 144 | def sourceRegistry = sourceImage.split('/')[0] |
| 145 | def sourceImgUrl = imageURL(sourceRegistry, sourceImage, source_image_sha256) - '/manifest.json' |
| 146 | def existingProps = mcp_artifactory.getPropertiesForArtifact(sourceImgUrl) |
| 147 | // check does the source image have already history props |
| 148 | if (existingProps) { |
| 149 | historyProperties = existingProps.get('com.mirantis.versionHistory', []) |
| 150 | } |
| 151 | } catch (Exception e) { |
| 152 | common.warningMsg("Can't find history for ${sourceImage}.") |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 153 | } |
| 154 | // %5C - backslash symbol is needed |
| 155 | historyProperties.add("${buildTime}%5C=${sourceImage}") |
Kyrylo Mashchenko | a86912d | 2018-12-21 08:31:47 +0000 | [diff] [blame] | 156 | artifactoryProperties << [ 'com.mirantis.versionHistory': historyProperties.join(',') ] |
Denis Egorenko | a0f1b2f | 2018-11-23 17:24:35 +0000 | [diff] [blame] | 157 | common.infoMsg("artifactoryProperties=> ${artifactoryProperties}") |
| 158 | common.retry(3, 5) { |
Denis Egorenko | 0191839 | 2018-12-10 12:28:46 +0400 | [diff] [blame] | 159 | mcp_artifactory.setProperties(tgtImgUrl, artifactoryProperties) |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 160 | } |
| 161 | } |
Richard Felkl | 7c920d0 | 2017-12-11 15:28:18 +0100 | [diff] [blame] | 162 | } |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 163 | } |
Jakub Josef | a63f986 | 2018-01-11 17:58:38 +0100 | [diff] [blame] | 164 | } catch (Throwable e) { |
azvyagintsev | 388a1bd | 2018-09-06 12:39:49 +0300 | [diff] [blame] | 165 | // Stub for future processing |
Jakub Josef | a63f986 | 2018-01-11 17:58:38 +0100 | [diff] [blame] | 166 | currentBuild.result = "FAILURE" |
| 167 | throw e |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 168 | } |
Richard Felkl | e90ef1c | 2017-12-08 00:13:56 +0100 | [diff] [blame] | 169 | } |
azvyagintsev | d162b91 | 2018-07-26 10:52:14 +0200 | [diff] [blame] | 170 | } |