2021-01-19 20:54:49 +05:30
|
|
|
import * as core from "@actions/core";
|
|
|
|
import * as exec from "@actions/exec";
|
|
|
|
import * as io from "@actions/io";
|
2021-01-07 18:49:38 -05:00
|
|
|
import * as fs from "fs";
|
2020-11-26 14:01:32 -05:00
|
|
|
import * as path from "path";
|
2021-02-08 20:07:42 +05:30
|
|
|
import { splitByNewline } from "./util";
|
2020-11-07 13:04:04 +01:00
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
interface ExecResult {
|
|
|
|
exitCode: number;
|
|
|
|
stdout: string;
|
|
|
|
stderr: string;
|
|
|
|
}
|
2020-11-07 13:04:04 +01:00
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
interface ImageStorageCheckResult {
|
|
|
|
readonly foundTags: string[];
|
|
|
|
readonly missingTags: string[];
|
|
|
|
}
|
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
let podmanPath: string | undefined;
|
2020-11-13 15:12:47 +01:00
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
// boolean value to check if pushed image is from Docker image storage
|
|
|
|
let isImageFromDocker = false;
|
|
|
|
let imageToPush: string;
|
2021-02-03 08:05:48 +05:30
|
|
|
let tagsList: string[];
|
2021-01-19 20:54:49 +05:30
|
|
|
|
|
|
|
async function getPodmanPath(): Promise<string> {
|
|
|
|
if (podmanPath == null) {
|
|
|
|
podmanPath = await io.which("podman", true);
|
2020-11-23 18:11:35 -05:00
|
|
|
}
|
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
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 });
|
2021-02-03 08:05:48 +05:30
|
|
|
const tags = core.getInput("tags") || "latest";
|
|
|
|
// split tags
|
|
|
|
tagsList = tags.split(" ");
|
2021-01-19 20:54:49 +05:30
|
|
|
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");
|
2020-11-26 14:01:32 -05:00
|
|
|
|
2021-02-08 20:07:42 +05:30
|
|
|
const inputExtraArgsStr = core.getInput("extra-args");
|
|
|
|
let podmanExtraArgs: string[] = [];
|
|
|
|
if (inputExtraArgsStr) {
|
|
|
|
// transform the array of lines into an array of arguments
|
|
|
|
// by splitting over lines, then over spaces, then trimming.
|
|
|
|
const lines = splitByNewline(inputExtraArgsStr);
|
|
|
|
podmanExtraArgs = lines.flatMap((line) => line.split(" ")).map((arg) => arg.trim());
|
|
|
|
}
|
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
imageToPush = `${imageInput}`;
|
|
|
|
const registryPathList: string[] = [];
|
2020-11-26 14:01:32 -05:00
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
// check if image with all the required tags exist in Podman image storage
|
|
|
|
const podmanImageStorageCheckResult: ImageStorageCheckResult = await checkImageInPodman();
|
2020-11-26 14:01:32 -05:00
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
const podmanFoundTags: string[] = podmanImageStorageCheckResult.foundTags;
|
|
|
|
const podmanMissingTags: string[] = podmanImageStorageCheckResult.missingTags;
|
|
|
|
|
|
|
|
if (podmanFoundTags.length > 0) {
|
|
|
|
core.info(`Tag(s) ${podmanFoundTags.join(", ")} of ${imageToPush} found in Podman image storage`);
|
|
|
|
}
|
2021-01-19 20:54:49 +05:30
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
// Log warning if few tags are found
|
|
|
|
if (podmanMissingTags.length > 0 && podmanFoundTags.length > 0) {
|
|
|
|
core.warning(`Tag(s) ${podmanMissingTags.join(", ")} of ${imageToPush} not found in Podman image storage`);
|
2020-11-13 15:12:47 +01:00
|
|
|
}
|
2020-11-07 13:04:04 +01:00
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
// 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 dockerImageStorageCheckResult: ImageStorageCheckResult = await pullImageFromDocker();
|
|
|
|
|
|
|
|
const dockerFoundTags: string[] = dockerImageStorageCheckResult.foundTags;
|
|
|
|
const dockerMissingTags: string[] = dockerImageStorageCheckResult.missingTags;
|
|
|
|
|
|
|
|
if (dockerFoundTags.length > 0) {
|
|
|
|
core.info(`Tag(s) ${dockerFoundTags.join(", ")} of ${imageToPush} found in Docker image storage`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log warning if few tags are found
|
|
|
|
if (dockerMissingTags.length > 0 && dockerFoundTags.length > 0) {
|
|
|
|
core.warning(`Tag(s) ${dockerMissingTags.join(", ")} of ${imageToPush} not found in Docker image storage`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// failing if image with any of the tag is not found in Docker as well as Podman
|
|
|
|
if (podmanMissingTags.length > 0 && dockerMissingTags.length > 0) {
|
|
|
|
throw new Error(
|
|
|
|
`Tag(s) ${podmanMissingTags.join(", ")} of ${imageToPush} not found in Podman image storage `
|
|
|
|
+ `and Tag(s) ${dockerMissingTags.join(", ")} of ${imageToPush} not found in Docker image storage.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const allTagsinPodman: boolean = podmanFoundTags.length === tagsList.length;
|
|
|
|
const allTagsinDocker: boolean = dockerFoundTags.length === tagsList.length;
|
|
|
|
|
|
|
|
if (allTagsinPodman && allTagsinDocker) {
|
2021-01-19 20:54:49 +05:30
|
|
|
const isPodmanImageLatest = await isPodmanLocalImageLatest();
|
|
|
|
if (!isPodmanImageLatest) {
|
2021-02-03 08:05:48 +05:30
|
|
|
core.warning(
|
|
|
|
`The version of ${imageToPush} in the Docker image storage is more recent `
|
|
|
|
+ `than the version in the Podman image storage. The image(s) from the Docker image storage `
|
|
|
|
+ `will be pushed.`
|
|
|
|
);
|
2021-01-19 20:54:49 +05:30
|
|
|
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
|
|
|
|
isImageFromDocker = true;
|
|
|
|
}
|
|
|
|
else {
|
2021-02-03 08:05:48 +05:30
|
|
|
core.warning(
|
|
|
|
`The version of ${imageToPush} in the Podman image storage is more recent `
|
|
|
|
+ `than the version in the Docker image storage. The image(s) from the Podman image `
|
|
|
|
+ `storage will be pushed.`
|
|
|
|
);
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
}
|
2021-02-03 08:05:48 +05:30
|
|
|
else if (allTagsinDocker) {
|
2021-01-19 20:54:49 +05:30
|
|
|
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
|
2021-02-03 08:05:48 +05:30
|
|
|
core.info(
|
|
|
|
`${imageToPush} was found in the Docker image storage, but not in the Podman `
|
|
|
|
+ `image storage. The image(s) will be pulled into Podman image storage, pushed, and then `
|
|
|
|
+ `removed from the Podman image storage.`
|
|
|
|
);
|
2021-01-19 20:54:49 +05:30
|
|
|
isImageFromDocker = true;
|
|
|
|
}
|
2021-02-03 08:05:48 +05:30
|
|
|
else {
|
|
|
|
core.info(
|
|
|
|
`${imageToPush} was found in the Podman image storage, but not in the Docker `
|
|
|
|
+ `image storage. The image(s) will be pushed from Podman image storage.`
|
|
|
|
);
|
|
|
|
}
|
2021-01-19 20:54:49 +05:30
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
let pushMsg = `Pushing ${imageToPush} with tags ${tagsList.join(", ")} to ${registry}`;
|
2021-01-19 20:54:49 +05:30
|
|
|
if (username) {
|
|
|
|
pushMsg += ` as ${username}`;
|
|
|
|
}
|
|
|
|
core.info(pushMsg);
|
|
|
|
|
|
|
|
const registryWithoutTrailingSlash = registry.replace(/\/$/, "");
|
2020-11-23 18:11:35 -05:00
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
const creds = `${username}:${password}`;
|
2020-11-26 14:01:32 -05:00
|
|
|
|
2021-01-08 09:57:51 -05:00
|
|
|
let digestFile = digestFileInput;
|
2021-02-03 08:05:48 +05:30
|
|
|
const imageNameWithTag = `${imageToPush}:${tagsList[0]}`;
|
2021-01-08 09:57:51 -05:00
|
|
|
if (!digestFile) {
|
2021-02-03 08:05:48 +05:30
|
|
|
digestFile = `${imageNameWithTag.replace(
|
2021-01-19 20:54:49 +05:30
|
|
|
/[/\\/?%*:|"<>]/g,
|
|
|
|
"-",
|
|
|
|
)}_digest.txt`;
|
2021-01-08 09:57:51 -05:00
|
|
|
}
|
2021-01-07 18:49:38 -05:00
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
// push the image
|
2021-02-03 08:05:48 +05:30
|
|
|
for (const tag of tagsList) {
|
|
|
|
const imageWithTag = `${imageToPush}:${tag}`;
|
|
|
|
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}:${tag}`;
|
2020-11-27 11:44:15 +05:30
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
const args = [
|
|
|
|
"push",
|
|
|
|
"--quiet",
|
|
|
|
"--digestfile",
|
|
|
|
digestFile,
|
|
|
|
"--creds",
|
|
|
|
creds,
|
|
|
|
imageWithTag,
|
|
|
|
registryPath,
|
|
|
|
];
|
2020-11-27 11:44:15 +05:30
|
|
|
|
2021-02-08 20:07:42 +05:30
|
|
|
if (podmanExtraArgs.length > 0) {
|
|
|
|
args.push(...podmanExtraArgs);
|
|
|
|
}
|
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
// check if tls-verify is not set to null
|
|
|
|
if (tlsVerify) {
|
|
|
|
args.push(`--tls-verify=${tlsVerify}`);
|
|
|
|
}
|
2020-11-26 14:01:32 -05:00
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
await execute(await getPodmanPath(), args);
|
|
|
|
core.info(`Successfully pushed ${imageWithTag} to ${registryPath}`);
|
|
|
|
|
|
|
|
registryPathList.push(registryPath);
|
|
|
|
}
|
2021-01-07 18:49:38 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
const digest = (await fs.promises.readFile(digestFile)).toString();
|
|
|
|
core.info(digest);
|
2021-01-19 20:54:49 +05:30
|
|
|
core.setOutput("digest", digest);
|
2021-01-07 18:49:38 -05:00
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
core.warning(`Failed to read digest file "${digestFile}": ${err}`);
|
|
|
|
}
|
2021-02-03 08:05:48 +05:30
|
|
|
|
|
|
|
core.setOutput("registry-paths", JSON.stringify(registryPathList));
|
2020-11-07 13:04:04 +01:00
|
|
|
}
|
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
async function pullImageFromDocker(): Promise<ImageStorageCheckResult> {
|
|
|
|
core.info(`Checking if ${imageToPush} with tag(s) ${tagsList.join(", ")} is present in Docker image storage`);
|
|
|
|
let imageWithTag;
|
|
|
|
const foundTags: string[] = [];
|
|
|
|
const missingTags: string[] = [];
|
2021-01-19 20:54:49 +05:30
|
|
|
try {
|
2021-02-03 08:05:48 +05:30
|
|
|
for (const tag of tagsList) {
|
|
|
|
imageWithTag = `${imageToPush}:${tag}`;
|
|
|
|
const commandResult: ExecResult = await execute(
|
|
|
|
await getPodmanPath(),
|
|
|
|
[ "pull", `docker-daemon:${imageWithTag}` ],
|
|
|
|
{ ignoreReturnCode: true, failOnStdErr: false }
|
|
|
|
);
|
|
|
|
if (!commandResult.exitCode) {
|
|
|
|
foundTags.push(tag);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
missingTags.push(tag);
|
|
|
|
}
|
|
|
|
}
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
catch (err) {
|
2021-02-03 08:05:48 +05:30
|
|
|
core.debug(err);
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
2021-02-03 08:05:48 +05:30
|
|
|
|
|
|
|
return {
|
|
|
|
foundTags,
|
|
|
|
missingTags,
|
|
|
|
};
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
|
2021-02-03 08:05:48 +05:30
|
|
|
async function checkImageInPodman(): Promise<ImageStorageCheckResult> {
|
2021-01-19 20:54:49 +05:30
|
|
|
// check if images exist in Podman's storage
|
2021-02-03 08:05:48 +05:30
|
|
|
core.info(`Checking if ${imageToPush} with tag(s) ${tagsList.join(", ")} is present in Podman image storage`);
|
|
|
|
let imageWithTag;
|
|
|
|
const foundTags: string[] = [];
|
|
|
|
const missingTags: string[] = [];
|
2021-01-19 20:54:49 +05:30
|
|
|
try {
|
2021-02-03 08:05:48 +05:30
|
|
|
for (const tag of tagsList) {
|
|
|
|
imageWithTag = `${imageToPush}:${tag}`;
|
|
|
|
const commandResult: ExecResult = await execute(
|
|
|
|
await getPodmanPath(),
|
|
|
|
[ "image", "exists", imageWithTag ],
|
|
|
|
{ ignoreReturnCode: true }
|
|
|
|
);
|
|
|
|
if (!commandResult.exitCode) {
|
|
|
|
foundTags.push(tag);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
missingTags.push(tag);
|
|
|
|
}
|
|
|
|
}
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
core.debug(err);
|
|
|
|
}
|
2021-02-03 08:05:48 +05:30
|
|
|
|
|
|
|
return {
|
|
|
|
foundTags,
|
|
|
|
missingTags,
|
|
|
|
};
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
async function isPodmanLocalImageLatest(): Promise<boolean> {
|
2021-02-03 08:05:48 +05:30
|
|
|
// checking for only one tag as creation time will be
|
|
|
|
// same for all the tags present
|
|
|
|
const imageWithTag = `${imageToPush}:${tagsList[0]}`;
|
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
// get creation time of the image present in the Podman image storage
|
|
|
|
const podmanLocalImageTimeStamp = await execute(await getPodmanPath(), [
|
|
|
|
"image",
|
|
|
|
"inspect",
|
2021-02-03 08:05:48 +05:30
|
|
|
imageWithTag,
|
2021-01-19 20:54:49 +05:30
|
|
|
"--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",
|
2021-02-03 08:05:48 +05:30
|
|
|
`${dockerBaseUrl}/${imageWithTag}`,
|
2021-01-19 20:54:49 +05:30
|
|
|
"--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`);
|
2021-02-03 08:05:48 +05:30
|
|
|
for (const tag of tagsList) {
|
|
|
|
const imageWithTag = `${imageToPush}:${tag}`;
|
|
|
|
await execute(await getPodmanPath(), [ "rmi", imageWithTag ]);
|
|
|
|
}
|
2021-01-19 20:54:49 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function execute(
|
|
|
|
executable: string,
|
|
|
|
args: string[],
|
|
|
|
execOptions: exec.ExecOptions = {},
|
|
|
|
): Promise<ExecResult> {
|
2020-11-26 14:01:32 -05:00
|
|
|
let stdout = "";
|
|
|
|
let stderr = "";
|
2020-11-17 14:19:05 -05:00
|
|
|
|
2020-11-26 14:01:32 -05:00
|
|
|
const finalExecOptions = { ...execOptions };
|
2021-01-19 20:54:49 +05:30
|
|
|
finalExecOptions.ignoreReturnCode = true; // the return code is processed below
|
2020-11-26 14:01:32 -05:00
|
|
|
|
|
|
|
finalExecOptions.listeners = {
|
2021-01-19 20:54:49 +05:30
|
|
|
stdline: (line): void => {
|
|
|
|
stdout += `${line}\n`;
|
2020-11-26 14:01:32 -05:00
|
|
|
},
|
2021-01-19 20:54:49 +05:30
|
|
|
errline: (line): void => {
|
|
|
|
stderr += `${line}\n`;
|
2020-11-13 15:12:47 +01:00
|
|
|
},
|
2021-01-19 20:54:49 +05:30
|
|
|
};
|
2020-11-26 14:01:32 -05:00
|
|
|
|
|
|
|
const exitCode = await exec.exec(executable, args, finalExecOptions);
|
|
|
|
|
|
|
|
if (execOptions.ignoreReturnCode !== true && exitCode !== 0) {
|
2021-01-19 20:54:49 +05:30
|
|
|
// Throwing the stderr as part of the Error makes the stderr show up in the action outline,
|
|
|
|
// which saves some clicking when debugging.
|
2020-11-26 14:01:32 -05:00
|
|
|
let error = `${path.basename(executable)} exited with code ${exitCode}`;
|
|
|
|
if (stderr) {
|
|
|
|
error += `\n${stderr}`;
|
2020-11-07 13:04:04 +01:00
|
|
|
}
|
2020-11-26 14:01:32 -05:00
|
|
|
throw new Error(error);
|
2020-11-17 14:19:05 -05:00
|
|
|
}
|
2020-11-26 14:01:32 -05:00
|
|
|
|
|
|
|
return {
|
2021-01-19 20:54:49 +05:30
|
|
|
exitCode,
|
|
|
|
stdout,
|
|
|
|
stderr,
|
2020-11-26 14:01:32 -05:00
|
|
|
};
|
2020-11-07 13:04:04 +01:00
|
|
|
}
|
|
|
|
|
2021-01-19 20:54:49 +05:30
|
|
|
run()
|
|
|
|
.catch(core.setFailed)
|
|
|
|
.finally(() => {
|
|
|
|
if (isImageFromDocker) {
|
|
|
|
removeDockerImage();
|
|
|
|
}
|
|
|
|
});
|