mirror of
https://github.com/redhat-actions/push-to-registry.git
synced 2025-02-22 18:21:20 +01:00
Solve issue when image is present in Podman and Docker both (#16)
* 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
23eb62f550
9 changed files with 2062 additions and 72 deletions
6
.eslintrc.js
Normal file
6
.eslintrc.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
"@redhat-actions/eslint-config",
|
||||||
|
],
|
||||||
|
};
|
16
.github/workflows/verify-push.yaml
vendored
16
.github/workflows/verify-push.yaml
vendored
|
@ -1,10 +1,10 @@
|
||||||
# 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
|
name: Test Push without image
|
||||||
on: [ push, pull_request, workflow_dispatch ]
|
on: [ push, workflow_dispatch ]
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: hello-world
|
IMAGE_NAME: myimage
|
||||||
IMAGE_REGISTRY: quay.io
|
IMAGE_REGISTRY: quay.io
|
||||||
IMAGE_TAG: latest
|
IMAGE_TAG: latest
|
||||||
|
|
||||||
|
@ -17,9 +17,12 @@ jobs:
|
||||||
- name: Checkout Push to Registry action
|
- name: Checkout Push to Registry action
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Pull hello-world image to push in next step
|
- name: Build Image using Docker
|
||||||
- name: Pull Hello world image
|
run: |
|
||||||
run: docker pull ${{ env.IMAGE_NAME }}
|
docker build -t ${{ env.IMAGE_NAME }}:latest -<<EOF
|
||||||
|
FROM busybox
|
||||||
|
RUN echo "hello world"
|
||||||
|
EOF
|
||||||
|
|
||||||
# Push the image to image registry
|
# Push the image to image registry
|
||||||
- name: Push To Quay
|
- name: Push To Quay
|
||||||
|
@ -32,7 +35,6 @@ jobs:
|
||||||
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 "registry-path ${{ steps.push.outputs.registry-path }}"
|
||||||
|
|
10
README.md
10
README.md
|
@ -120,6 +120,16 @@ jobs:
|
||||||
run: echo "New image has been pushed to ${{ steps.push-to-quay.outputs.registry-path }}"
|
run: echo "New image has been pushed to ${{ steps.push-to-quay.outputs.registry-path }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Note about images built with Docker
|
||||||
|
|
||||||
|
This action uses `Podman` to push, but can also push images built with `Docker`. However, Docker and Podman store their images in different locations, and Podman can only push images in its own storage.
|
||||||
|
|
||||||
|
If the image to push is present in the Docker image storage but not in the Podman image storage, it will be pulled into Podman's storage.
|
||||||
|
|
||||||
|
If the image to push is present in both the Docker and Podman image storage, the action will push the image which was more recently built, and log a warning.
|
||||||
|
|
||||||
|
If the action pulled an image from the Docker image storage into the Podman storage, it will be cleaned up from the Podman storage before the action exits.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
Note that quay.io repositories are private by default.<br>
|
Note that quay.io repositories are private by default.<br>
|
||||||
|
|
||||||
|
|
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
1847
package-lock.json
generated
1847
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@
|
||||||
"compile": "tsc -p .",
|
"compile": "tsc -p .",
|
||||||
"bundle": "ncc build src/index.ts --source-map --minify",
|
"bundle": "ncc build src/index.ts --source-map --minify",
|
||||||
"clean": "rm -rf out/ dist/",
|
"clean": "rm -rf out/ dist/",
|
||||||
|
"lint": "eslint . --max-warnings=0",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "Red Hat",
|
"author": "Red Hat",
|
||||||
|
@ -17,8 +18,13 @@
|
||||||
"@actions/io": "^1.0.2"
|
"@actions/io": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@redhat-actions/eslint-config": "^1.2.0",
|
||||||
|
"@redhat-actions/tsconfig": "^1.1.0",
|
||||||
"@types/node": "^12.12.7",
|
"@types/node": "^12.12.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||||
|
"@typescript-eslint/parser": "^4.14.0",
|
||||||
"@vercel/ncc": "^0.25.1",
|
"@vercel/ncc": "^0.25.1",
|
||||||
|
"eslint": "^7.18.0",
|
||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
226
src/index.ts
226
src/index.ts
|
@ -1,60 +1,106 @@
|
||||||
import * as core from '@actions/core';
|
import * as core from "@actions/core";
|
||||||
import * as exec from '@actions/exec';
|
import * as exec from "@actions/exec";
|
||||||
import * as io from '@actions/io';
|
import * as io from "@actions/io";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
interface ExecResult {
|
||||||
const imageInput = core.getInput('image', { required: true });
|
exitCode: number;
|
||||||
const tag = core.getInput('tag') || 'latest';
|
stdout: string;
|
||||||
const registry = core.getInput('registry', { required: true });
|
stderr: string;
|
||||||
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
|
let podmanPath: string | undefined;
|
||||||
const podman = await io.which('podman', true);
|
|
||||||
|
// boolean value to check if pushed image is from Docker image storage
|
||||||
|
let isImageFromDocker = false;
|
||||||
|
let imageToPush: string;
|
||||||
|
|
||||||
|
async function getPodmanPath(): Promise<string> {
|
||||||
|
if (podmanPath == null) {
|
||||||
|
podmanPath = await io.which("podman", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return podmanPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base URL that gets appended if image is pulled from the Docker imaege storage
|
||||||
|
const dockerBaseUrl = "docker.io/library";
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
imageToPush = `${imageInput}:${tag}`;
|
||||||
|
|
||||||
|
// check if image exist in Podman image storage
|
||||||
|
const isPresentInPodman: boolean = await checkImageInPodman();
|
||||||
|
|
||||||
|
// check if image exist in Docker image storage and if exist pull the image to Podman
|
||||||
|
const isPresentInDocker: boolean = await pullImageFromDocker();
|
||||||
|
|
||||||
|
// failing if image is not found in Docker as well as Podman
|
||||||
|
if (!isPresentInDocker && !isPresentInPodman) {
|
||||||
|
throw new Error(`${imageToPush} not found in Podman local storage, or Docker local storage.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPresentInPodman && isPresentInDocker) {
|
||||||
|
const isPodmanImageLatest = await isPodmanLocalImageLatest();
|
||||||
|
if (!isPodmanImageLatest) {
|
||||||
|
core.warning(`The version of ${imageToPush} in the Docker image storage is more recent `
|
||||||
|
+ `than the version in the Podman image storage. The image from the Docker image storage `
|
||||||
|
+ `will be pushed.`);
|
||||||
|
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
|
||||||
|
isImageFromDocker = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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 `
|
||||||
|
+ `storage will be pushed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isPresentInDocker) {
|
||||||
|
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
|
||||||
|
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 `
|
||||||
|
+ `removed from the Podman image storage.`);
|
||||||
|
isImageFromDocker = true;
|
||||||
|
}
|
||||||
|
|
||||||
const imageToPush = `${imageInput}:${tag}`;
|
|
||||||
let pushMsg = `Pushing ${imageToPush} to ${registry}`;
|
let pushMsg = `Pushing ${imageToPush} to ${registry}`;
|
||||||
if (username) {
|
if (username) {
|
||||||
pushMsg += ` as ${username}`;
|
pushMsg += ` as ${username}`;
|
||||||
}
|
}
|
||||||
core.info(pushMsg);
|
core.info(pushMsg);
|
||||||
|
|
||||||
//check if images exist in podman's local storage
|
const registryWithoutTrailingSlash = registry.replace(/\/$/, "");
|
||||||
const checkImages = await execute(podman, ['images', '--format', 'json']);
|
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}:${tag}`;
|
||||||
|
|
||||||
const parsedCheckImages = JSON.parse(checkImages.stdout);
|
const creds = `${username}:${password}`;
|
||||||
|
|
||||||
// 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}`;
|
|
||||||
|
|
||||||
let digestFile = digestFileInput;
|
let digestFile = digestFileInput;
|
||||||
if (!digestFile) {
|
if (!digestFile) {
|
||||||
digestFile = `${imageToPush.replace(/[/\\/?%*:|"<>]/g, "-")}_digest.txt`;
|
digestFile = `${imageToPush.replace(
|
||||||
|
/[/\\/?%*:|"<>]/g,
|
||||||
|
"-",
|
||||||
|
)}_digest.txt`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = [ 'push',
|
// push the image
|
||||||
'--quiet',
|
const args = [
|
||||||
'--digestfile', digestFile,
|
"push",
|
||||||
'--creds', creds,
|
"--quiet",
|
||||||
|
"--digestfile",
|
||||||
|
digestFile,
|
||||||
|
"--creds",
|
||||||
|
creds,
|
||||||
imageToPush,
|
imageToPush,
|
||||||
registryPath
|
registryPath,
|
||||||
];
|
];
|
||||||
|
|
||||||
// check if tls-verify is not set to null
|
// check if tls-verify is not set to null
|
||||||
|
@ -62,41 +108,109 @@ export async function run(): Promise<void> {
|
||||||
args.push(`--tls-verify=${tlsVerify}`);
|
args.push(`--tls-verify=${tlsVerify}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await execute(podman, args);
|
await execute(await getPodmanPath(), args);
|
||||||
|
|
||||||
core.info(`Successfully pushed ${imageToPush} to ${registryPath}.`);
|
core.info(`Successfully pushed ${imageToPush} to ${registryPath}.`);
|
||||||
core.setOutput('registry-path', registryPath);
|
core.setOutput("registry-path", registryPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const digest = (await fs.promises.readFile(digestFile)).toString();
|
const digest = (await fs.promises.readFile(digestFile)).toString();
|
||||||
core.info(digest);
|
core.info(digest);
|
||||||
core.setOutput('digest', digest);
|
core.setOutput("digest", digest);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
core.warning(`Failed to read digest file "${digestFile}": ${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(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execute(await getPodmanPath(), [ "pull", `docker-daemon:${imageToPush}` ]);
|
||||||
|
core.info(`${imageToPush} found in Docker image storage`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
core.info(`${imageToPush} not found in Docker image storage`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkImageInPodman(): Promise<boolean> {
|
||||||
|
// check if images exist in Podman's storage
|
||||||
|
core.info(`Checking if ${imageToPush} is in Podman image storage`);
|
||||||
|
try {
|
||||||
|
await execute(await getPodmanPath(), [ "image", "exists", imageToPush ]);
|
||||||
|
core.info(`${imageToPush} found in Podman image storage`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
core.info(`${imageToPush} not found in Podman image storage`);
|
||||||
|
core.debug(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isPodmanLocalImageLatest(): Promise<boolean> {
|
||||||
|
// get creation time of the image present in the Podman image storage
|
||||||
|
const podmanLocalImageTimeStamp = await execute(await getPodmanPath(), [
|
||||||
|
"image",
|
||||||
|
"inspect",
|
||||||
|
imageToPush,
|
||||||
|
"--format",
|
||||||
|
"{{.Created}}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// get creation time of the image pulled from the Docker image storage
|
||||||
|
// appending 'docker.io/library' infront of image name as pulled image name
|
||||||
|
// from Docker image storage starts with the 'docker.io/library'
|
||||||
|
const pulledImageCreationTimeStamp = await execute(await getPodmanPath(), [
|
||||||
|
"image",
|
||||||
|
"inspect",
|
||||||
|
`${dockerBaseUrl}/${imageToPush}`,
|
||||||
|
"--format",
|
||||||
|
"{{.Created}}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const podmanImageTime = new Date(podmanLocalImageTimeStamp.stdout).getTime();
|
||||||
|
|
||||||
|
const dockerImageTime = new Date(pulledImageCreationTimeStamp.stdout).getTime();
|
||||||
|
|
||||||
|
return podmanImageTime > dockerImageTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the pulled image from the Podman image storage
|
||||||
|
async function removeDockerImage(): Promise<void> {
|
||||||
|
if (imageToPush) {
|
||||||
|
core.info(`Removing ${imageToPush} from the Podman image storage`);
|
||||||
|
await execute(await getPodmanPath(), [ "rmi", imageToPush ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(
|
||||||
|
executable: string,
|
||||||
|
args: string[],
|
||||||
|
execOptions: exec.ExecOptions = {},
|
||||||
|
): Promise<ExecResult> {
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
const finalExecOptions = { ...execOptions };
|
const finalExecOptions = { ...execOptions };
|
||||||
finalExecOptions.ignoreReturnCode = true; // the return code is processed below
|
finalExecOptions.ignoreReturnCode = true; // the return code is processed below
|
||||||
|
|
||||||
finalExecOptions.listeners = {
|
finalExecOptions.listeners = {
|
||||||
stdline: (line) => {
|
stdline: (line): void => {
|
||||||
stdout += line + "\n";
|
stdout += `${line}\n`;
|
||||||
},
|
},
|
||||||
errline: (line) => {
|
errline: (line): void => {
|
||||||
stderr += line + "\n"
|
stderr += `${line}\n`;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const exitCode = await exec.exec(executable, args, finalExecOptions);
|
const exitCode = await exec.exec(executable, args, finalExecOptions);
|
||||||
|
|
||||||
if (execOptions.ignoreReturnCode !== true && exitCode !== 0) {
|
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}`;
|
let error = `${path.basename(executable)} exited with code ${exitCode}`;
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
error += `\n${stderr}`;
|
error += `\n${stderr}`;
|
||||||
|
@ -105,8 +219,16 @@ async function execute(executable: string, args: string[], execOptions: exec.Exe
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exitCode, stdout, stderr
|
exitCode,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch(core.setFailed);
|
run()
|
||||||
|
.catch(core.setFailed)
|
||||||
|
.finally(() => {
|
||||||
|
if (isImageFromDocker) {
|
||||||
|
removeDockerImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
{
|
{
|
||||||
|
"extends": "@redhat-actions/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"rootDir": "src/",
|
||||||
"module": "commonjs",
|
"outDir": "out/"
|
||||||
"lib": [
|
|
||||||
"ES2017"
|
|
||||||
],
|
|
||||||
"outDir": "out",
|
|
||||||
"rootDir": ".",
|
|
||||||
},
|
},
|
||||||
"exclude": [
|
"include": [
|
||||||
"node_modules"
|
"src/"
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue