Skip to content

Commit 7c033fc

Browse files
authored
feat(ecr): support retagging in push task (#570)
* feat(ecr): support retagging in push task See https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-retag.html Adds an additional option to the ECR push task to specify an existing image in ECR by repository and tag, and then add a new tag to this image without re-pushing the complete image. It will fail if there is no corresponding tag + repo in ECR. It will NOT fail if the target image already has the tag being added. Doc changes pending. * formatting
1 parent 7e0e79d commit 7c033fc

File tree

6 files changed

+113
-33
lines changed

6 files changed

+113
-33
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "ECR: Push task now supports image retagging."
4+
}

src/tasks/ECRPushImage/TaskOperations.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import tl = require('azure-pipelines-task-lib/task')
88
import { DockerHandler } from 'lib/dockerUtils'
99
import { constructTaggedImageName, getEcrAuthorizationData, loginToRegistry } from 'lib/ecrUtils'
1010
import { parse } from 'url'
11-
import { imageNameSource, TaskParameters } from './TaskParameters'
11+
import { imageNameSource, retagSource, TaskParameters } from './TaskParameters'
1212

1313
export class TaskOperations {
1414
private dockerPath = ''
@@ -63,30 +63,76 @@ export class TaskOperations {
6363
proxyEndpoint = authData.proxyEndpoint
6464
}
6565

66-
if (this.taskParameters.autoCreateRepository) {
67-
await this.createRepositoryIfNeeded(this.taskParameters.repositoryName)
68-
}
69-
66+
const targetRepositoryName = this.taskParameters.repositoryName
7067
const targetImageName = constructTaggedImageName(
7168
this.taskParameters.repositoryName,
72-
this.taskParameters.pushTag
69+
this.taskParameters.targetTag
7370
)
7471
const targetImageRef = `${endpoint}/${targetImageName}`
75-
await this.tagImage(sourceImageRef, targetImageRef)
7672

77-
await loginToRegistry(this.dockerHandler, this.dockerPath, authToken, proxyEndpoint)
73+
// Retag an image without pushing the complete image from docker.
74+
if (this.taskParameters.imageSource === retagSource) {
75+
const images = (
76+
await this.ecrClient
77+
.batchGetImage({
78+
repositoryName: targetRepositoryName,
79+
imageIds: [{ imageTag: this.taskParameters.targetTag }]
80+
})
81+
.promise()
82+
).images
83+
84+
if (!images || !images[0]) {
85+
throw new Error(
86+
tl.loc(
87+
'FailureToFindExistingImage',
88+
this.taskParameters.targetTag,
89+
this.taskParameters.repositoryName
90+
)
91+
)
92+
}
93+
94+
const manifest = images[0].imageManifest
95+
if (manifest) {
96+
try {
97+
await this.ecrClient
98+
.putImage({
99+
imageTag: this.taskParameters.newTag,
100+
repositoryName: targetRepositoryName,
101+
imageManifest: manifest
102+
})
103+
.promise()
104+
} catch (err) {
105+
if (err.code !== 'ImageAlreadyExistsException') {
106+
// Thrown when manifest and tag already exist in ECR.
107+
// Do not block if the tag already exists on the target image.
108+
throw err
109+
}
110+
console.log(err.message)
111+
}
112+
} else {
113+
throw new Error('batchGetImage did not return an image manifest.')
114+
}
115+
} else {
116+
if (this.taskParameters.autoCreateRepository) {
117+
await this.createRepositoryIfNeeded(this.taskParameters.repositoryName)
118+
}
119+
120+
await this.tagImage(sourceImageRef, targetImageRef)
78121

79-
await this.pushImageToECR(targetImageRef)
122+
await loginToRegistry(this.dockerHandler, this.dockerPath, authToken, proxyEndpoint)
123+
124+
await this.pushImageToECR(targetImageRef)
125+
126+
if (this.taskParameters.removeDockerImage) {
127+
await this.removeDockerImage(sourceImageRef)
128+
}
129+
}
80130

81131
if (this.taskParameters.outputVariable) {
82132
console.log(tl.loc('SettingOutputVariable', this.taskParameters.outputVariable, targetImageRef))
83133
tl.setVariable(this.taskParameters.outputVariable, targetImageRef)
84134
}
85135

86-
if (this.taskParameters.removeDockerImage) {
87-
await this.removeDockerImage(sourceImageRef)
88-
}
89-
90136
console.log(tl.loc('TaskCompleted'))
91137
}
92138

src/tasks/ECRPushImage/TaskParameters.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getInputOrEmpty, getInputRequired } from 'lib/vstsUtils'
99

1010
export const imageNameSource = 'imagename'
1111
export const imageIdSource = 'imageid'
12+
export const retagSource = 'retag'
1213

1314
export interface TaskParameters {
1415
awsConnectionParameters: AWSConnectionParameters
@@ -17,7 +18,8 @@ export interface TaskParameters {
1718
sourceImageTag: string
1819
sourceImageId: string
1920
repositoryName: string
20-
pushTag: string
21+
targetTag: string
22+
newTag: string
2123
autoCreateRepository: boolean
2224
forceDockerNamingConventions: boolean
2325
removeDockerImage: boolean
@@ -29,24 +31,37 @@ export function buildTaskParameters(): TaskParameters {
2931
awsConnectionParameters: buildConnectionParameters(),
3032
imageSource: getInputRequired('imageSource'),
3133
repositoryName: getInputRequired('repositoryName'),
32-
pushTag: getInputOrEmpty('pushTag'),
34+
targetTag: getInputOrEmpty('targetTag'),
3335
autoCreateRepository: tl.getBoolInput('autoCreateRepository', false),
3436
forceDockerNamingConventions: tl.getBoolInput('forceDockerNamingConventions', false),
3537
removeDockerImage: tl.getBoolInput('removeDockerImage', false),
3638
outputVariable: getInputOrEmpty('outputVariable'),
3739
sourceImageName: '',
3840
sourceImageId: '',
39-
sourceImageTag: ''
41+
sourceImageTag: '',
42+
newTag: ''
4043
}
4144

42-
if (parameters.imageSource === imageNameSource) {
43-
parameters.sourceImageName = getInputRequired('sourceImageName')
44-
parameters.sourceImageTag = getInputOrEmpty('sourceImageTag')
45-
if (!parameters.sourceImageTag) {
46-
parameters.sourceImageTag = 'latest'
47-
}
48-
} else {
49-
parameters.sourceImageId = getInputRequired('sourceImageId')
45+
switch (parameters.imageSource) {
46+
case imageNameSource:
47+
parameters.sourceImageName = getInputRequired('sourceImageName')
48+
parameters.sourceImageTag = getInputOrEmpty('sourceImageTag')
49+
if (!parameters.sourceImageTag) {
50+
parameters.sourceImageTag = 'latest'
51+
}
52+
break
53+
case imageIdSource:
54+
parameters.sourceImageId = getInputRequired('sourceImageId')
55+
break
56+
case retagSource:
57+
parameters.newTag = getInputRequired('newTag')
58+
break
59+
default:
60+
throw new Error(`Unknown imageSource specified: ${parameters.imageSource}`)
61+
}
62+
63+
if (!parameters.targetTag) {
64+
parameters.targetTag = 'latest'
5065
}
5166

5267
return parameters

src/tasks/ECRPushImage/task.json

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@
4949
"label": "Image Identity",
5050
"required": true,
5151
"defaultValue": "imagename",
52-
"helpMarkDown": "How the image to be pushed is identified. You can select from either the image ID or the image name. If image name is selected a tag can also be specified",
52+
"helpMarkDown": "How the image to be pushed is identified. You can select from either the image ID or the image name. If image name is selected a tag can also be specified. Alternatively, you can opt to retag an image in ECR by specifying an existing tag in the target repository. [Retagging](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-retag.html) saves bandwidth by not re-pushing the image to ECR.",
5353
"options": {
5454
"imagename": "Image name with optional tag",
55-
"imageid": "Image ID"
55+
"imageid": "Image ID",
56+
"retag": "Retag pushed image"
5657
},
5758
"properties": {
5859
"EditableOptions": "False"
@@ -94,36 +95,48 @@
9495
"helpMarkDown": "The name of the repository to which the image will be pushed."
9596
},
9697
{
97-
"name": "pushTag",
98+
"name": "targetTag",
9899
"label": "Target Repository Tag",
99100
"type": "string",
100101
"required": false,
101102
"defaultValue": "latest",
102-
"helpMarkDown": "Optional tag for the new image in the repository. If not specified, ECR will assume 'latest'."
103+
"helpMarkDown": "Optional tag for the image in the remote repository. If not specified, ECR will assume 'latest'."
104+
},
105+
{
106+
"name": "newTag",
107+
"label": "New Tag",
108+
"type": "string",
109+
"required": true,
110+
"defaultValue": "",
111+
"helpMarkDown": "Tag to add to the target image.",
112+
"visibleRule": "imageSource = retag"
103113
},
104114
{
105115
"name": "autoCreateRepository",
106116
"label": "Create repository if it does not exist",
107117
"type": "boolean",
108118
"defaultValue": false,
109119
"required": false,
110-
"helpMarkDown": "If selected the task will attempt to create the repository if it does not exist."
120+
"helpMarkDown": "If selected the task will attempt to create the repository if it does not exist.",
121+
"visibleRule": "imageSource != retag"
111122
},
112123
{
113124
"name": "forceDockerNamingConventions",
114125
"label": "Force repository name to follow Docker naming conventions",
115126
"type": "boolean",
116127
"defaultValue": false,
117128
"required": false,
118-
"helpMarkDown": "If enabled, the Docker repository name will be modified to follow Docker naming conventions. Converts upper case characters to lower case. Removes all characters except 0-9, -, . and _ ."
129+
"helpMarkDown": "If enabled, the Docker repository name will be modified to follow Docker naming conventions. Converts upper case characters to lower case. Removes all characters except 0-9, -, . and _ .",
130+
"visibleRule": "imageSource != retag"
119131
},
120132
{
121133
"name": "removeDockerImage",
122134
"label": "Remove Docker image after ECR push",
123135
"type": "boolean",
124136
"defaultValue": false,
125137
"required": false,
126-
"helpMarkDown": "If enabled, this command removes the image and untags any references to it"
138+
"helpMarkDown": "If enabled, this command removes the image and untags any references to it",
139+
"visibleRule": "imageSource != retag"
127140
},
128141
{
129142
"name": "outputVariable",
@@ -172,6 +185,7 @@
172185
"SettingOutputVariable": "Setting output variable %s with the pushed image tag %s",
173186
"TaskCompleted": "Successfully completed sending the message",
174187
"FailureToObtainAuthToken": "Failed to obtain auth token!",
188+
"FailureToFindExistingImage": "Failed to find image with tag '%s' in target repository '%s'",
175189
"NoValidEndpoint": "Failed to get endpoint of repository %s. Check your credentials and if the endpoint exists!"
176190
}
177191
}

tests/endToEndTests/ecr-push-image-should-fail-no-docker.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"sourceImageTag": "latest",
4242
"sourceImageId": "",
4343
"repositoryName": "test-repo",
44-
"pushTag": "latest",
44+
"targetTag": "latest",
4545
"autoCreateRepository": "false",
4646
"outputVariable": "",
4747
"logRequest": "false",

tests/taskTests/ecrPushImage/ecrPushImage-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const defaultTaskParameters: TaskParameters = {
1919
sourceImageTag: '',
2020
sourceImageId: '',
2121
repositoryName: '',
22-
pushTag: '',
22+
targetTag: '',
23+
newTag: '',
2324
autoCreateRepository: false,
2425
forceDockerNamingConventions: false,
2526
removeDockerImage: false,

0 commit comments

Comments
 (0)