mirror of
https://github.com/redhat-actions/push-to-registry.git
synced 2025-04-20 23:31:22 +00:00
Solve issue when image is present in Podman and Docker both
If updated docker image is present in docker env then docker image won't get used if image same name and tag is already present in podman env. To fix this, selected latest built image and removed the image at the end from the podman env if image is pulled from docker env. Signed-off-by: divyansh42 <diagrawa@redhat.com>
This commit is contained in:
parent
b038efb70a
commit
4deaffac48
8 changed files with 2028 additions and 56 deletions
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
out/
|
||||
dist/
|
6
.eslintrc.js
Normal file
6
.eslintrc.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
extends: [
|
||||
"@tetchel/eslint-config-actions",
|
||||
],
|
||||
};
|
12
.github/workflows/verify-push.yaml
vendored
12
.github/workflows/verify-push.yaml
vendored
|
@ -4,7 +4,7 @@
|
|||
name: Test Push
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
env:
|
||||
IMAGE_NAME: hello-world
|
||||
IMAGE_NAME: myimage
|
||||
IMAGE_REGISTRY: quay.io
|
||||
IMAGE_TAG: latest
|
||||
|
||||
|
@ -17,9 +17,12 @@ jobs:
|
|||
- name: Checkout Push to Registry action
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Pull hello-world image to push in next step
|
||||
- name: Pull Hello world image
|
||||
run: docker pull ${{ env.IMAGE_NAME }}
|
||||
- name: Build Image using Docker
|
||||
run: |
|
||||
docker build -t ${{ env.IMAGE_NAME }}:latest -<<EOF
|
||||
FROM busybox
|
||||
RUN echo "hello world"
|
||||
EOF
|
||||
|
||||
# Push the image to image registry
|
||||
- name: Push To Quay
|
||||
|
@ -32,7 +35,6 @@ jobs:
|
|||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
|
||||
- name: Echo outputs
|
||||
run: |
|
||||
echo "registry-path ${{ steps.push.outputs.registry-path }}"
|
||||
|
|
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
1839
package-lock.json
generated
1839
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@
|
|||
"compile": "tsc -p .",
|
||||
"bundle": "ncc build src/index.ts --source-map --minify",
|
||||
"clean": "rm -rf out/ dist/",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Red Hat",
|
||||
|
@ -17,8 +18,12 @@
|
|||
"@actions/io": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tetchel/eslint-config-actions": "0.0.8",
|
||||
"@types/node": "^12.12.7",
|
||||
"@typescript-eslint/eslint-plugin": "^4.13.0",
|
||||
"@typescript-eslint/parser": "^4.13.0",
|
||||
"@vercel/ncc": "^0.25.1",
|
||||
"eslint": "^7.17.0",
|
||||
"typescript": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
|
211
src/index.ts
211
src/index.ts
|
@ -1,60 +1,100 @@
|
|||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import * as io from '@actions/io';
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
const imageInput = core.getInput('image', { required: true });
|
||||
const tag = core.getInput('tag') || 'latest';
|
||||
const registry = core.getInput('registry', { required: true });
|
||||
const username = core.getInput('username', { required: true });
|
||||
const password = core.getInput('password', { required: true });
|
||||
const tlsVerify = core.getInput('tls-verify');
|
||||
const digestFileInput = core.getInput('digestfile');
|
||||
interface Response {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
// get podman cli
|
||||
const podman = await io.which('podman', true);
|
||||
async function run(): Promise<void> {
|
||||
const imageInput = core.getInput("image", { required: true });
|
||||
const tag = core.getInput("tag") || "latest";
|
||||
const registry = core.getInput("registry", { required: true });
|
||||
const username = core.getInput("username", { required: true });
|
||||
const password = core.getInput("password", { required: true });
|
||||
const tlsVerify = core.getInput("tls-verify");
|
||||
const digestFileInput = core.getInput("digestfile");
|
||||
|
||||
// get Podman cli
|
||||
const podman = await io.which("podman", true);
|
||||
|
||||
let imageToPush = `${imageInput}:${tag}`;
|
||||
|
||||
// check if image exist in Podman local registry
|
||||
const isPresentInPodman: boolean = await checkImageInPodman(
|
||||
imageToPush,
|
||||
podman,
|
||||
);
|
||||
|
||||
// check if image exist in Docker local registry and if exist pull the image to Podman
|
||||
const isPresentInDocker: boolean = await pullImageFromDocker(
|
||||
imageToPush,
|
||||
podman,
|
||||
);
|
||||
|
||||
// boolean value to check if pushed image is from Docker local registry
|
||||
let isPushingDockerImage = false;
|
||||
|
||||
if (isPresentInPodman && isPresentInDocker) {
|
||||
const warningMsg = "Image found in Podman as well as in Docker local registry.";
|
||||
|
||||
let isPodmanImageLatest = false;
|
||||
try {
|
||||
isPodmanImageLatest = await isPodmanLocalImageLatest(
|
||||
imageToPush,
|
||||
podman,
|
||||
);
|
||||
}
|
||||
catch (err) {
|
||||
core.setFailed(err);
|
||||
}
|
||||
|
||||
if (!isPodmanImageLatest) {
|
||||
core.warning(`${warningMsg} Using Docker local registry's image as that is built latest`);
|
||||
imageToPush = `docker.io/library/${imageToPush}`;
|
||||
isPushingDockerImage = true;
|
||||
}
|
||||
else {
|
||||
core.warning(`${warningMsg} Using Podman local registry's image as that is built latest`);
|
||||
}
|
||||
}
|
||||
else if (isPresentInDocker) {
|
||||
imageToPush = `docker.io/library/${imageToPush}`;
|
||||
isPushingDockerImage = true;
|
||||
}
|
||||
|
||||
const imageToPush = `${imageInput}:${tag}`;
|
||||
let pushMsg = `Pushing ${imageToPush} to ${registry}`;
|
||||
if (username) {
|
||||
pushMsg += ` as ${username}`;
|
||||
}
|
||||
core.info(pushMsg);
|
||||
|
||||
//check if images exist in podman's local storage
|
||||
const checkImages = await execute(podman, ['images', '--format', 'json']);
|
||||
const registryPath = `${registry.replace(/\/$/, "")}/${imageInput}:${tag}`;
|
||||
|
||||
const parsedCheckImages = JSON.parse(checkImages.stdout);
|
||||
|
||||
// this is to temporarily solve an issue with the case-sensitive of the property field name. i.e it is Names or names??
|
||||
const nameKeyMixedCase = parsedCheckImages[0] && Object.keys(parsedCheckImages[0]).find(key => 'names' === key.toLowerCase());
|
||||
const imagesFound = parsedCheckImages.
|
||||
filter((image: string) => image[nameKeyMixedCase] && image[nameKeyMixedCase].find((name: string) => name.includes(`${imageToPush}`))).
|
||||
map((image: string ) => image[nameKeyMixedCase]);
|
||||
|
||||
if (imagesFound.length === 0) {
|
||||
//check inside the docker daemon local storage
|
||||
await execute(podman, [ 'pull', `docker-daemon:${imageToPush}` ]);
|
||||
}
|
||||
|
||||
// push image
|
||||
const registryPath = `${registry.replace(/\/$/, '')}/${imageToPush}`;
|
||||
|
||||
const creds: string = `${username}:${password}`;
|
||||
const creds = `${username}:${password}`;
|
||||
|
||||
let digestFile = digestFileInput;
|
||||
if (!digestFile) {
|
||||
digestFile = `${imageToPush.replace(/[/\\/?%*:|"<>]/g, "-")}_digest.txt`;
|
||||
digestFile = `${imageToPush.replace(
|
||||
/[/\\/?%*:|"<>]/g,
|
||||
"-",
|
||||
)}_digest.txt`;
|
||||
}
|
||||
|
||||
const args = [ 'push',
|
||||
'--quiet',
|
||||
'--digestfile', digestFile,
|
||||
'--creds', creds,
|
||||
// push image
|
||||
const args = [
|
||||
"push",
|
||||
"--quiet",
|
||||
"--digestfile",
|
||||
digestFile,
|
||||
"--creds",
|
||||
creds,
|
||||
imageToPush,
|
||||
registryPath
|
||||
registryPath,
|
||||
];
|
||||
|
||||
// check if tls-verify is not set to null
|
||||
|
@ -65,19 +105,93 @@ export async function run(): Promise<void> {
|
|||
await execute(podman, args);
|
||||
|
||||
core.info(`Successfully pushed ${imageToPush} to ${registryPath}.`);
|
||||
core.setOutput('registry-path', registryPath);
|
||||
core.setOutput("registry-path", registryPath);
|
||||
|
||||
// remove the pulled image from the Podman local registry
|
||||
if (isPushingDockerImage) {
|
||||
core.info(`Removing ${imageToPush} from the Podman local registry`);
|
||||
await execute(podman, [ "rmi", imageToPush ]);
|
||||
}
|
||||
|
||||
try {
|
||||
const digest = (await fs.promises.readFile(digestFile)).toString();
|
||||
core.info(digest);
|
||||
core.setOutput('digest', digest);
|
||||
core.setOutput("digest", digest);
|
||||
}
|
||||
catch (err) {
|
||||
core.warning(`Failed to read digest file "${digestFile}": ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(executable: string, args: string[], execOptions: exec.ExecOptions = {}): Promise<{ exitCode: number, stdout: string, stderr: string }> {
|
||||
async function pullImageFromDocker(
|
||||
imageName: string,
|
||||
podman: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await execute(podman, [ "pull", `docker-daemon:${imageName}` ]);
|
||||
core.info("Image found and sucessfully pulled from Docker local registry");
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
core.info("Image not found in Docker local registry");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkImageInPodman(
|
||||
imageName: string,
|
||||
podman: string,
|
||||
): Promise<boolean> {
|
||||
// check if images exist in Podman's local registry
|
||||
core.info("Checking image in Podman local registry");
|
||||
try {
|
||||
await execute(podman, [ "image", "exists", imageName ]);
|
||||
core.info("Image found in Podman local registry");
|
||||
}
|
||||
catch (err) {
|
||||
core.info("Image not found in Podman local registry");
|
||||
core.debug(err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function isPodmanLocalImageLatest(
|
||||
imageName: string,
|
||||
podman: string,
|
||||
): Promise<boolean> {
|
||||
// get creation time of the image present in the Podman local registry
|
||||
const podmanLocalImageTimeStamp = await execute(podman, [
|
||||
"image",
|
||||
"inspect",
|
||||
imageName,
|
||||
"--format",
|
||||
"{{.Created}}",
|
||||
]);
|
||||
|
||||
// get creation time of the image pulled from the Docker local registry
|
||||
// appending 'docker.io/library' infront of image name as pulled image name
|
||||
// from Docker local registry starts with the 'docker.io/library'
|
||||
const pulledImageCreationTimeStamp = await execute(podman, [
|
||||
"image",
|
||||
"inspect",
|
||||
`docker.io/library/${imageName}`,
|
||||
"--format",
|
||||
"{{.Created}}",
|
||||
]);
|
||||
|
||||
const podmanImageTime = new Date(podmanLocalImageTimeStamp.stdout).getTime();
|
||||
|
||||
const dockerImageTime = new Date(pulledImageCreationTimeStamp.stdout).getTime();
|
||||
|
||||
return podmanImageTime > dockerImageTime;
|
||||
}
|
||||
|
||||
async function execute(
|
||||
executable: string,
|
||||
args: string[],
|
||||
execOptions: exec.ExecOptions = {},
|
||||
): Promise<Response> {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
|
@ -86,17 +200,18 @@ async function execute(executable: string, args: string[], execOptions: exec.Exe
|
|||
|
||||
finalExecOptions.listeners = {
|
||||
stdline: (line) => {
|
||||
stdout += line + "\n";
|
||||
stdout += `${line}\n`;
|
||||
},
|
||||
errline: (line) => {
|
||||
stderr += line + "\n"
|
||||
stderr += `${line}\n`;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const exitCode = await exec.exec(executable, args, finalExecOptions);
|
||||
|
||||
if (execOptions.ignoreReturnCode !== true && exitCode !== 0) {
|
||||
// Throwing the stderr as part of the Error makes the stderr show up in the action outline, which saves some clicking when debugging.
|
||||
// Throwing the stderr as part of the Error makes the stderr show up in the action outline,
|
||||
// which saves some clicking when debugging.
|
||||
let error = `${path.basename(executable)} exited with code ${exitCode}`;
|
||||
if (stderr) {
|
||||
error += `\n${stderr}`;
|
||||
|
@ -105,7 +220,9 @@ async function execute(executable: string, args: string[], execOptions: exec.Exe
|
|||
}
|
||||
|
||||
return {
|
||||
exitCode, stdout, stderr
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue