Add feature to push multiple tags of the image

Signed-off-by: divyansh42 <diagrawa@redhat.com>
This commit is contained in:
divyansh42 2021-01-25 15:52:13 +05:30
parent 23eb62f550
commit 73da53f4a0
6 changed files with 96 additions and 55 deletions

View file

@ -1,7 +1,7 @@
# This workflow will perform a test whenever there # This workflow will perform a test whenever there
# is some change in code done to ensure that the changes # is some change in code done to ensure that the changes
# are not buggy and we are getting the desired output. # are not buggy and we are getting the desired output.
name: Test Push without image name: Test Build and Push
on: [ push, workflow_dispatch ] on: [ push, workflow_dispatch ]
env: env:
IMAGE_NAME: myimage IMAGE_NAME: myimage
@ -19,10 +19,13 @@ jobs:
- name: Build Image using Docker - name: Build Image using Docker
run: | run: |
docker build -t ${{ env.IMAGE_NAME }}:latest -<<EOF docker build -t ${{ env.IMAGE_NAME }}:v1 -<<EOF
FROM busybox FROM busybox
RUN echo "hello world" RUN echo "hello world"
EOF EOF
docker tag ${{ env.IMAGE_NAME }}:v1 ${{ env.IMAGE_NAME }}:v2
docker tag ${{ env.IMAGE_NAME }}:v1 ${{ env.IMAGE_NAME }}:v3
# Push the image to image registry # Push the image to image registry
- name: Push To Quay - name: Push To Quay
@ -30,12 +33,12 @@ jobs:
id: push id: push
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tag: ${{ env.IMAGE_TAG }} tags: v1 v2 v3
registry: ${{ env.IMAGE_REGISTRY }}/${{ secrets.REGISTRY_USER }} registry: ${{ env.IMAGE_REGISTRY }}/${{ secrets.REGISTRY_USER }}
username: ${{ secrets.REGISTRY_USER }} username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Echo outputs - name: Echo outputs
run: | run: |
echo "registry-path ${{ steps.push.outputs.registry-path }}" echo "Digest: ${{ steps.push.outputs.digest }}"
echo "digest ${{ steps.push.outputs.digest }}" echo "Registry Paths: ${{ steps.push.outputs.registry-paths }}"

View file

@ -73,11 +73,12 @@ Refer to the [`podman push`](http://docs.podman.io/en/latest/markdown/podman-man
## Action Outputs ## Action Outputs
`registry-path`: The registry path to which the image was pushed.<br> `registry-paths`: The List of registry paths to which the tag(s) were pushed.<br>
For example, `quay.io/username/spring-image:v1`. For example, `quay.io/username/spring-image:v1,quay.io/username/spring-image:v2`.
`digest`: The pushed image digest, as written to the `digestfile`.<br> `digest`: The pushed image digest, as written to the `digestfile`.<br>
For example, `sha256:66ce924069ec4181725d15aa27f34afbaf082f434f448dc07a42daa3305cdab3`. For example, `sha256:66ce924069ec4181725d15aa27f34afbaf082f434f448dc07a42daa3305cdab3`.
For multiple tags, digest remains same.
## Examples ## Examples

View file

@ -8,8 +8,8 @@ inputs:
image: image:
description: 'Name of the image to push' description: 'Name of the image to push'
required: true required: true
tag: tags:
description: 'Tag of the image to push' description: 'The tags of the image to push. For multiple tags, seperate by a space. For example, "v1 v0.1".'
required: false required: false
default: 'latest' default: 'latest'
registry: registry:
@ -25,10 +25,18 @@ inputs:
description: 'Verify TLS certificates when contacting the registry' description: 'Verify TLS certificates when contacting the registry'
required: false required: false
default: 'true' default: 'true'
digestfile:
description: |
After copying the image, write the digest of the resulting image to the file.
By default, the filename will be determined from the image and tag.
The contents of this file are the digest output.
required: false
outputs: outputs:
registry-path: digest:
description: 'The registry path to which the image was pushed' description: 'The pushed image digest, as written to the `digestfile`'
registry-paths:
description: 'List of registry paths to which the tag(s) were pushed'
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,7 @@ let podmanPath: string | undefined;
// boolean value to check if pushed image is from Docker image storage // boolean value to check if pushed image is from Docker image storage
let isImageFromDocker = false; let isImageFromDocker = false;
let imageToPush: string; let imageToPush: string;
let tagsList: string[];
async function getPodmanPath(): Promise<string> { async function getPodmanPath(): Promise<string> {
if (podmanPath == null) { if (podmanPath == null) {
@ -29,24 +30,28 @@ const dockerBaseUrl = "docker.io/library";
async function run(): Promise<void> { async function run(): Promise<void> {
const imageInput = core.getInput("image", { required: true }); const imageInput = core.getInput("image", { required: true });
const tag = core.getInput("tag") || "latest"; const tags = core.getInput("tags");
// split tags
tagsList = tags.split(" ");
const registry = core.getInput("registry", { required: true }); const registry = core.getInput("registry", { required: true });
const username = core.getInput("username", { required: true }); const username = core.getInput("username", { required: true });
const password = core.getInput("password", { required: true }); const password = core.getInput("password", { required: true });
const tlsVerify = core.getInput("tls-verify"); const tlsVerify = core.getInput("tls-verify");
const digestFileInput = core.getInput("digestfile"); const digestFileInput = core.getInput("digestfile");
imageToPush = `${imageInput}:${tag}`; imageToPush = `${imageInput}`;
const registryPathList: string[] = [];
// check if image exist in Podman image storage // check if image with all the required tags exist in Podman image storage
const isPresentInPodman: boolean = await checkImageInPodman(); const isPresentInPodman: boolean = await checkImageInPodman();
// check if image exist in Docker image storage and if exist pull the image to Podman // check if image with all the required tags exist in Docker image storage
// and if exist pull the image with all the tags to Podman
const isPresentInDocker: boolean = await pullImageFromDocker(); const isPresentInDocker: boolean = await pullImageFromDocker();
// failing if image is not found in Docker as well as Podman // failing if image with any of the tag is not found in Docker as well as Podman
if (!isPresentInDocker && !isPresentInPodman) { if (!isPresentInDocker && !isPresentInPodman) {
throw new Error(`${imageToPush} not found in Podman local storage, or Docker local storage.`); throw new Error(`All the tags of ${imageToPush} not found in Podman local storage, or Docker local storage.`);
} }
if (isPresentInPodman && isPresentInDocker) { if (isPresentInPodman && isPresentInDocker) {
@ -60,38 +65,42 @@ async function run(): Promise<void> {
} }
else { else {
core.warning(`The version of ${imageToPush} in the Podman image storage is more recent ` core.warning(`The version of ${imageToPush} in the Podman image storage is more recent `
+ `than the version in the Docker image storage. The image from the Podman image ` + `than the version in the Docker image storage. Tag(s) of the image from the Podman image `
+ `storage will be pushed.`); + `storage will be pushed.`);
} }
} }
else if (isPresentInDocker) { else if (isPresentInDocker) {
imageToPush = `${dockerBaseUrl}/${imageToPush}`; imageToPush = `${dockerBaseUrl}/${imageToPush}`;
core.info(`${imageToPush} was found in the Docker image storage, but not in the Podman ` core.info(`${imageToPush} was found in the Docker image storage, but not in the Podman `
+ `image storage. The image will be pulled into Podman image storage, pushed, and then ` + `image storage. Tag(s) of the image will be pulled into Podman image storage, pushed, and then `
+ `removed from the Podman image storage.`); + `removed from the Podman image storage.`);
isImageFromDocker = true; isImageFromDocker = true;
} }
let pushMsg = `Pushing ${imageToPush} to ${registry}`; let pushMsg = `Pushing ${imageToPush} with tags ${tagsList.toString()} to ${registry}`;
if (username) { if (username) {
pushMsg += ` as ${username}`; pushMsg += ` as ${username}`;
} }
core.info(pushMsg); core.info(pushMsg);
const registryWithoutTrailingSlash = registry.replace(/\/$/, ""); const registryWithoutTrailingSlash = registry.replace(/\/$/, "");
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}:${tag}`;
const creds = `${username}:${password}`; const creds = `${username}:${password}`;
let digestFile = digestFileInput; let digestFile = digestFileInput;
const imageNameWithTag = `${imageToPush}:${tagsList[0]}`;
if (!digestFile) { if (!digestFile) {
digestFile = `${imageToPush.replace( digestFile = `${imageNameWithTag.replace(
/[/\\/?%*:|"<>]/g, /[/\\/?%*:|"<>]/g,
"-", "-",
)}_digest.txt`; )}_digest.txt`;
} }
// push the image // push the image
for (const tag of tagsList) {
const imageWithTag = `${imageToPush}:${tag}`;
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}:${tag}`;
const args = [ const args = [
"push", "push",
"--quiet", "--quiet",
@ -99,7 +108,7 @@ async function run(): Promise<void> {
digestFile, digestFile,
"--creds", "--creds",
creds, creds,
imageToPush, imageWithTag,
registryPath, registryPath,
]; ];
@ -109,9 +118,10 @@ async function run(): Promise<void> {
} }
await execute(await getPodmanPath(), args); await execute(await getPodmanPath(), args);
core.info(`Successfully pushed ${imageWithTag} to ${registryPath}.`);
core.info(`Successfully pushed ${imageToPush} to ${registryPath}.`); registryPathList.push(registryPath);
core.setOutput("registry-path", registryPath); }
try { try {
const digest = (await fs.promises.readFile(digestFile)).toString(); const digest = (await fs.promises.readFile(digestFile)).toString();
@ -121,41 +131,57 @@ async function run(): Promise<void> {
catch (err) { catch (err) {
core.warning(`Failed to read digest file "${digestFile}": ${err}`); core.warning(`Failed to read digest file "${digestFile}": ${err}`);
} }
core.setOutput("registry-paths", registryPathList.toString());
} }
async function pullImageFromDocker(): Promise<boolean> { async function pullImageFromDocker(): Promise<boolean> {
let imageWithTag;
try { try {
await execute(await getPodmanPath(), [ "pull", `docker-daemon:${imageToPush}` ]); for (const tag of tagsList) {
core.info(`${imageToPush} found in Docker image storage`); imageWithTag = `${imageToPush}:${tag}`;
return true; await execute(await getPodmanPath(), [ "pull", `docker-daemon:${imageWithTag}` ]);
core.info(`${imageWithTag} found in Docker image storage`);
}
} }
catch (err) { catch (err) {
core.info(`${imageToPush} not found in Docker image storage`); core.info(`${imageWithTag} not found in Docker image storage`);
return false; return false;
} }
return true;
} }
async function checkImageInPodman(): Promise<boolean> { async function checkImageInPodman(): Promise<boolean> {
// check if images exist in Podman's storage // check if images exist in Podman's storage
core.info(`Checking if ${imageToPush} is in Podman image storage`); core.info(`Checking if ${imageToPush} with tag(s) ${tagsList.toString()} is present in Podman image storage`);
let imageWithTag;
try { try {
await execute(await getPodmanPath(), [ "image", "exists", imageToPush ]); for (const tag of tagsList) {
core.info(`${imageToPush} found in Podman image storage`); imageWithTag = `${imageToPush}:${tag}`;
return true; await execute(await getPodmanPath(), [ "image", "exists", imageWithTag ]);
core.info(`${imageWithTag} found in Podman image storage`);
}
} }
catch (err) { catch (err) {
core.info(`${imageToPush} not found in Podman image storage`); core.info(`${imageWithTag} not found in Podman image storage`);
core.debug(err); core.debug(err);
return false; return false;
} }
return true;
} }
async function isPodmanLocalImageLatest(): Promise<boolean> { async function isPodmanLocalImageLatest(): Promise<boolean> {
// checking for only one tag as creation time will be
// same for all the tags present
const imageWithTag = `${imageToPush}:${tagsList[0]}`;
// get creation time of the image present in the Podman image storage // get creation time of the image present in the Podman image storage
const podmanLocalImageTimeStamp = await execute(await getPodmanPath(), [ const podmanLocalImageTimeStamp = await execute(await getPodmanPath(), [
"image", "image",
"inspect", "inspect",
imageToPush, imageWithTag,
"--format", "--format",
"{{.Created}}", "{{.Created}}",
]); ]);
@ -166,7 +192,7 @@ async function isPodmanLocalImageLatest(): Promise<boolean> {
const pulledImageCreationTimeStamp = await execute(await getPodmanPath(), [ const pulledImageCreationTimeStamp = await execute(await getPodmanPath(), [
"image", "image",
"inspect", "inspect",
`${dockerBaseUrl}/${imageToPush}`, `${dockerBaseUrl}/${imageWithTag}`,
"--format", "--format",
"{{.Created}}", "{{.Created}}",
]); ]);
@ -181,8 +207,11 @@ async function isPodmanLocalImageLatest(): Promise<boolean> {
// remove the pulled image from the Podman image storage // remove the pulled image from the Podman image storage
async function removeDockerImage(): Promise<void> { async function removeDockerImage(): Promise<void> {
if (imageToPush) { if (imageToPush) {
core.info(`Removing ${imageToPush} from the Podman image storage`); for (const tag of tagsList) {
await execute(await getPodmanPath(), [ "rmi", imageToPush ]); const imageWithTag = `${imageToPush}:${tag}`;
await execute(await getPodmanPath(), [ "rmi", imageWithTag ]);
core.info(`Removing ${imageWithTag} from the Podman image storage`);
}
} }
} }