Refactor inputs to support docker/metadata-action (#50)

This commit is contained in:
なつき 2021-10-12 10:22:18 -07:00 committed by GitHub
parent 1d3fd04cee
commit bdfa69ab89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 253 additions and 221 deletions

View file

@ -10,55 +10,20 @@ env:
IMAGE_NAMESPACE: redhat-github-actions IMAGE_NAMESPACE: redhat-github-actions
IMAGE_NAME: ptr-test IMAGE_NAME: ptr-test
IMAGE_TAG: v1 IMAGE_TAG: v1
SHORT_IMAGE_NAME_TAG: ptr-test:v1
FULLY_QUALIFIED_IMAGE_NAME_TAG: quay.io/redhat-github-actions/ptr-test:v1
jobs: jobs:
build-only-podman: build:
name: Build and push image built only on Podman name: |-
runs-on: ubuntu-20.04 Build with ${{ matrix.build_with }} and push${{ matrix.fully_qualified_image_name_tag && ' FQIN' || '' }} (latest: ${{ matrix.install_latest }})
strategy:
fail-fast: false
matrix:
install_latest: [ true, false ]
steps:
# Checkout push-to-registry action github repository
- name: Checkout Push to Registry action
uses: actions/checkout@v2
- name: Install latest podman
if: matrix.install_latest
run: |
bash .github/install_latest_podman.sh
- name: Build image using Podman
run: |
podman build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF
FROM busybox
RUN echo "hello world"
EOF
- name: Push image to ${{ env.IMAGE_REGISTRY }}
id: push
uses: ./
with:
image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }}
registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Echo outputs
run: |
echo "${{ toJSON(steps.push.outputs) }}"
build-only-docker:
name: Build and push image built only on Docker
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
install_latest: [ true, false ] install_latest: [ true, false ]
build_with: [ "docker after podman", "podman after docker", "podman only", "docker only" ]
fully_qualified_image_name_tag: [ true, false ]
steps: steps:
@ -72,102 +37,25 @@ jobs:
bash .github/install_latest_podman.sh bash .github/install_latest_podman.sh
- name: Build image using Docker - name: Build image using Docker
if: endsWith(matrix.build_with, 'docker')
run: | run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF docker build -t ${{ matrix.fully_qualified_image_name_tag && env.FULLY_QUALIFIED_IMAGE_NAME_TAG || env.SHORT_IMAGE_NAME_TAG }} -<<EOF
FROM busybox
RUN echo "hello world"
EOF
- name: Push image to ${{ env.IMAGE_REGISTRY }}
id: push
uses: ./
with:
image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }}
registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Echo outputs
run: |
echo "${{ toJSON(steps.push.outputs) }}"
build-podman-latest:
name: Build and push image built latest on Podman
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
install_latest: [ true, false ]
steps:
# Checkout push-to-registry action github repository
- name: Checkout Push to Registry action
uses: actions/checkout@v2
- name: Install latest podman
if: matrix.install_latest
run: |
bash .github/install_latest_podman.sh
- name: Build image using Docker
run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF
FROM busybox FROM busybox
RUN echo "hello world" RUN echo "hello world"
EOF EOF
- name: Build image using Podman - name: Build image using Podman
if: contains(matrix.build_with, 'podman')
run: | run: |
podman build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF podman build -t ${{ matrix.fully_qualified_image_name_tag && env.FULLY_QUALIFIED_IMAGE_NAME_TAG || env.SHORT_IMAGE_NAME_TAG }} -<<EOF
FROM busybox
RUN echo "hello world"
EOF
- name: Push image to ${{ env.IMAGE_REGISTRY }}
id: push
uses: ./
with:
image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }}
registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Echo outputs
run: |
echo "${{ toJSON(steps.push.outputs) }}"
build-docker-latest:
name: Build and push image built latest on Docker
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
install_latest: [ true, false ]
steps:
# Checkout push-to-registry action github repository
- name: Checkout Push to Registry action
uses: actions/checkout@v2
- name: Install latest podman
if: matrix.install_latest
run: |
bash .github/install_latest_podman.sh
- name: Build image using Podman
run: |
podman build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF
FROM busybox FROM busybox
RUN echo "hello world" RUN echo "hello world"
EOF EOF
- name: Build image using Docker - name: Build image using Docker
if: startsWith(matrix.build_with, 'docker')
run: | run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -<<EOF docker build -t ${{ matrix.fully_qualified_image_name_tag && env.FULLY_QUALIFIED_IMAGE_NAME_TAG || env.SHORT_IMAGE_NAME_TAG }} -<<EOF
FROM busybox FROM busybox
RUN echo "hello world" RUN echo "hello world"
EOF EOF
@ -177,7 +65,7 @@ jobs:
uses: ./ uses: ./
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }} tags: ${{ matrix.fully_qualified_image_name_tag && env.FULLY_QUALIFIED_IMAGE_NAME_TAG || env.IMAGE_TAG }}
registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }} registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }}
username: ${{ secrets.REGISTRY_USER }} username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}

View file

@ -24,16 +24,16 @@ Refer to the [`podman push`](http://docs.podman.io/en/latest/markdown/podman-man
| Input Name | Description | Default | | Input Name | Description | Default |
| ---------- | ----------- | ------- | | ---------- | ----------- | ------- |
| image | Name of the image you want to push. Eg. `username/imagename` or `imagename`. See the note below about naming image and registry. | **Required** | image | Name of the image you want to push. Eg. `username/imagename` or `imagename`. See the note below about naming image and registry. | **Required unless tags are provided in `<repository>:<tag>` form**
| tags | The tag or tags of the image to push. For multiple tags, separate by a space. For example, `latest ${{ github.sha }}`. | `latest` | tags | The tag or tags of the image to push. For multiple tags, separate by whitespace. For example, `latest ${{ github.sha }}`. For multiple image names, specify tags in `<repository>:<tag>` form. For example, `quay.io/podman/stable:latest quay.io/containers/podman:latest` | `latest`
| registry | Hostname and optional namespace to push the image to. Eg. `quay.io` or `quay.io/username`. See the note below about naming image and registry. | **Required** | registry | Hostname and optional namespace to push the image to. Eg. `quay.io` or `quay.io/username`. See the note below about naming image and registry. | **Required unless tags are provided in `<repository>:<tag>` form**
| username | Username with which to authenticate to the registry. Required unless already logged in to the registry. | None | username | Username with which to authenticate to the registry. Required unless already logged in to the registry. | None
| password | Password, encrypted password, or access token to use to log in to the registry. Required unless already logged in to the registry. | None | password | Password, encrypted password, or access token to use to log in to the registry. Required unless already logged in to the registry. | None
| tls-verify | Verify TLS certificates when contacting the registry. Set to `false` to skip certificate verification. | `true` | tls-verify | Verify TLS certificates when contacting the registry. Set to `false` to skip certificate verification. | `true`
| digestfile | After copying the image, write the digest of the resulting image to the file. The contents of this file are the digest output. | Auto-generated from image and tag | digestfile | After copying the image, write the digest of the resulting image to the file. The contents of this file are the digest output. | Auto-generated from image and tag
| extra-args | Extra args to be passed to podman push. Separate arguments by newline. Do not use quotes. | None | extra-args | Extra args to be passed to podman push. Separate arguments by newline. Do not use quotes. | None
**NOTE**: You can provide the registry namespace (usually your username, or organization) either as a suffix to input `registry` (eg. `quay.io/username`) or as a prefix to input `image` (eg. `username/myimage`), but not in both. The full image path will be resolved from `<registry>/<image>`. **NOTE**: You can provide the registry namespace (usually your username, or organization) either as a suffix to input `registry` (eg. `quay.io/username`) or as a prefix to input `image` (eg. `username/myimage`), but not in both. The full image path will be resolved from `<registry>/<image>`. Alternatively, you can provide input `tags` as `<repository>:<tag>` with repository in fully qualified image name (FQIN) form (e.g. `quay.io/username/myimage:latest`). When FQIN tags are provided, input `image` and `registry` will be ignored.
## Action Outputs ## Action Outputs

View file

@ -7,14 +7,14 @@ branding:
inputs: inputs:
image: image:
description: 'Name of the image to push (e.g. username/imagename or imagename)' description: 'Name of the image to push (e.g. username/imagename or imagename)'
required: true required: false
tags: tags:
description: 'The tag or tags of the image to push. For multiple tags, seperate by a space. For example, "latest v1"' description: 'The tag or tags of the image to push. For multiple tags, seperate by whitespace. For example, "latest v1"'
required: false required: false
default: 'latest' default: 'latest'
registry: registry:
description: 'Hostname and optional namespace to push the image to (eg. quay.io/username or quay.io)' description: 'Hostname and optional namespace to push the image to (eg. quay.io/username or quay.io)'
required: true required: false
username: username:
description: 'Username to use as credential to authenticate to the registry' description: 'Username to use as credential to authenticate to the registry'
required: false required: false

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

17
package-lock.json generated
View file

@ -10,9 +10,9 @@
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
}, },
"@actions/exec": { "@actions/exec": {
"version": "1.0.4", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.0.tgz",
"integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==", "integrity": "sha512-LImpN9AY0J1R1mEYJjVJfSZWU4zYOlEcwSTgPve1rFQqK5AwrEs6uWW5Rv70gbDIQIAUwI86z6B+9mPK4w9Sbg==",
"requires": { "requires": {
"@actions/io": "^1.0.1" "@actions/io": "^1.0.1"
} }
@ -139,6 +139,12 @@
"integrity": "sha512-t+i85G2LLauDOlH3MQqxVblCKMt5yyRHZsO7NoVKE8T1W1aIosH1bs5xH2RqwXaWw2Si+r66W/tuHRQzKbR51w==", "integrity": "sha512-t+i85G2LLauDOlH3MQqxVblCKMt5yyRHZsO7NoVKE8T1W1aIosH1bs5xH2RqwXaWw2Si+r66W/tuHRQzKbR51w==",
"dev": true "dev": true
}, },
"@types/ini": {
"version": "1.3.31",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz",
"integrity": "sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w==",
"dev": true
},
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.7", "version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
@ -1156,6 +1162,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"ini": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
},
"is-arrayish": { "is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",

View file

@ -14,13 +14,15 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.4", "@actions/exec": "^1.1.0",
"@actions/io": "^1.0.2" "@actions/io": "^1.0.2",
"ini": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@redhat-actions/action-io-generator": "^1.5.0", "@redhat-actions/action-io-generator": "^1.5.0",
"@redhat-actions/eslint-config": "^1.3.2", "@redhat-actions/eslint-config": "^1.3.2",
"@redhat-actions/tsconfig": "^1.1.1", "@redhat-actions/tsconfig": "^1.1.1",
"@types/ini": "^1.3.30",
"@types/node": "^12.12.7", "@types/node": "^12.12.7",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",

View file

@ -17,7 +17,7 @@ export enum Inputs {
EXTRA_ARGS = "extra-args", EXTRA_ARGS = "extra-args",
/** /**
* Name of the image to push (e.g. username/imagename or imagename) * Name of the image to push (e.g. username/imagename or imagename)
* Required: true * Required: false
* Default: None. * Default: None.
*/ */
IMAGE = "image", IMAGE = "image",
@ -29,12 +29,12 @@ export enum Inputs {
PASSWORD = "password", PASSWORD = "password",
/** /**
* Hostname and optional namespace to push the image to (eg. quay.io/username or quay.io) * Hostname and optional namespace to push the image to (eg. quay.io/username or quay.io)
* Required: true * Required: false
* Default: None. * Default: None.
*/ */
REGISTRY = "registry", REGISTRY = "registry",
/** /**
* The tag or tags of the image to push. For multiple tags, seperate by a space. For example, "latest v1" * The tag or tags of the image to push. For multiple tags, seperate by whitespace. For example, "latest v1"
* Required: false * Required: false
* Default: "latest" * Default: "latest"
*/ */

View file

@ -2,8 +2,14 @@ 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 os from "os";
import * as path from "path"; import * as path from "path";
import { splitByNewline } from "./util"; import {
isStorageDriverOverlay, findFuseOverlayfsPath,
splitByNewline,
isFullImageName, getFullImageName,
getFullDockerImageName,
} from "./util";
import { Inputs, Outputs } from "./generated/inputs-outputs"; import { Inputs, Outputs } from "./generated/inputs-outputs";
interface ExecResult { interface ExecResult {
@ -21,9 +27,10 @@ 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 sourceImages: string[];
let tagsList: string[]; let destinationImages: string[];
let dockerBaseUrl: string; let dockerPodmanRoot: string;
let dockerPodmanOpts: string[];
async function getPodmanPath(): Promise<string> { async function getPodmanPath(): Promise<string> {
if (podmanPath == null) { if (podmanPath == null) {
@ -34,31 +41,62 @@ async function getPodmanPath(): Promise<string> {
return podmanPath; return podmanPath;
} }
// base URL that gets appended if image is pulled from the Docker imaege storage
const DOCKER_IO = `docker.io`;
const DOCKER_IO_NAMESPACED = DOCKER_IO + `/library`;
async function run(): Promise<void> { async function run(): Promise<void> {
const DEFAULT_TAG = "latest"; const DEFAULT_TAG = "latest";
const imageInput = core.getInput(Inputs.IMAGE, { required: true }); const imageInput = core.getInput(Inputs.IMAGE);
const tags = core.getInput(Inputs.TAGS); const tags = core.getInput(Inputs.TAGS);
// split tags // split tags
tagsList = tags.split(" "); const tagsList = tags.trim().split(/\s+/);
// handle the case when image name is 'namespace/imagename' and image is present in docker storage
dockerBaseUrl = imageInput.indexOf("/") > -1 ? DOCKER_IO : DOCKER_IO_NAMESPACED;
// info message if user doesn't provides any tag // info message if user doesn't provides any tag
if (tagsList.length === 0) { if (tagsList.length === 0) {
core.info(`Input "${Inputs.TAGS}" is not provided, using default tag "${DEFAULT_TAG}"`); core.info(`Input "${Inputs.TAGS}" is not provided, using default tag "${DEFAULT_TAG}"`);
tagsList.push(DEFAULT_TAG); tagsList.push(DEFAULT_TAG);
} }
const registry = core.getInput(Inputs.REGISTRY, { required: true }); const registry = core.getInput(Inputs.REGISTRY);
const username = core.getInput(Inputs.USERNAME); const username = core.getInput(Inputs.USERNAME);
const password = core.getInput(Inputs.PASSWORD); const password = core.getInput(Inputs.PASSWORD);
const tlsVerify = core.getInput(Inputs.TLS_VERIFY); const tlsVerify = core.getInput(Inputs.TLS_VERIFY);
const digestFileInput = core.getInput(Inputs.DIGESTFILE); const digestFileInput = core.getInput(Inputs.DIGESTFILE);
// check if all tags provided are in `image:tag` format
const isFullImageNameTag = isFullImageName(tagsList[0]);
if (tagsList.some((tag) => isFullImageName(tag) !== isFullImageNameTag)) {
throw new Error(`Input "${Inputs.TAGS}" cannot have a mix of full name and non full name tags`);
}
if (!isFullImageNameTag) {
if (!imageInput) {
throw new Error(`Input "${Inputs.IMAGE}" must be provided when using non full name tags`);
}
if (!registry) {
throw new Error(`Input "${Inputs.REGISTRY}" must be provided when using non full name tags`);
}
const registryWithoutTrailingSlash = registry.replace(/\/$/, "");
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}`;
core.info(`Combining image name "${imageInput}" and registry "${registry}" `
+ `to form registry path "${registryPath}"`);
if (imageInput.indexOf("/") > -1 && registry.indexOf("/") > -1) {
core.warning(`"${registryPath}" does not seem to be a valid registry path. `
+ `The registry path should not contain more than 2 slashes. `
+ `Refer to the Inputs section of the readme for naming image and registry.`);
}
sourceImages = tagsList.map((tag) => getFullImageName(imageInput, tag));
destinationImages = tagsList.map((tag) => getFullImageName(registryPath, tag));
}
else {
if (imageInput) {
core.warning(`Input "${Inputs.IMAGE}" is ignored when using full name tags`);
}
if (registry) {
core.warning(`Input "${Inputs.REGISTRY}" is ignored when using full name tags`);
}
sourceImages = tagsList;
destinationImages = tagsList;
}
const inputExtraArgsStr = core.getInput(Inputs.EXTRA_ARGS); const inputExtraArgsStr = core.getInput(Inputs.EXTRA_ARGS);
let podmanExtraArgs: string[] = []; let podmanExtraArgs: string[] = [];
if (inputExtraArgsStr) { if (inputExtraArgsStr) {
@ -68,7 +106,6 @@ async function run(): Promise<void> {
podmanExtraArgs = lines.flatMap((line) => line.split(" ")).map((arg) => arg.trim()); podmanExtraArgs = lines.flatMap((line) => line.split(" ")).map((arg) => arg.trim());
} }
imageToPush = `${imageInput}`;
const registryPathList: string[] = []; const registryPathList: string[] = [];
// check if image with all the required tags exist in Podman image storage // check if image with all the required tags exist in Podman image storage
@ -79,13 +116,13 @@ async function run(): Promise<void> {
if (podmanFoundTags.length > 0) { if (podmanFoundTags.length > 0) {
core.info(`Tag${podmanFoundTags.length !== 1 ? "s" : ""} "${podmanFoundTags.join(", ")}" ` core.info(`Tag${podmanFoundTags.length !== 1 ? "s" : ""} "${podmanFoundTags.join(", ")}" `
+ `of "${imageToPush}" found in Podman image storage`); + `found in Podman image storage`);
} }
// Log warning if few tags are not found // Log warning if few tags are not found
if (podmanMissingTags.length > 0 && podmanFoundTags.length > 0) { if (podmanMissingTags.length > 0 && podmanFoundTags.length > 0) {
core.warning(`Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" ` core.warning(`Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
+ `of "${imageToPush}" not found in Podman image storage`); + `not found in Podman image storage`);
} }
// check if image with all the required tags exist in Docker image storage // check if image with all the required tags exist in Docker image storage
@ -97,19 +134,19 @@ async function run(): Promise<void> {
if (dockerFoundTags.length > 0) { if (dockerFoundTags.length > 0) {
core.info(`Tag${dockerFoundTags.length !== 1 ? "s" : ""} "${dockerFoundTags.join(", ")}" ` core.info(`Tag${dockerFoundTags.length !== 1 ? "s" : ""} "${dockerFoundTags.join(", ")}" `
+ `of "${imageToPush}" found in Docker image storage`); + `found in Docker image storage`);
} }
// Log warning if few tags are not found // Log warning if few tags are not found
if (dockerMissingTags.length > 0 && dockerFoundTags.length > 0) { if (dockerMissingTags.length > 0 && dockerFoundTags.length > 0) {
core.warning(`Tag${dockerMissingTags.length !== 1 ? "s" : ""} "${dockerMissingTags.join(", ")}" ` core.warning(`Tag${dockerMissingTags.length !== 1 ? "s" : ""} "${dockerMissingTags.join(", ")}" `
+ `of "${imageToPush}" not found in Docker image storage`); + `not found in Docker image storage`);
} }
// failing if image with any of the tag 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 (podmanMissingTags.length > 0 && dockerMissingTags.length > 0) { if (podmanMissingTags.length > 0 && dockerMissingTags.length > 0) {
throw new Error( throw new Error(
`❌ All tags for "${imageToPush}" were not found in either Podman image storage, or Docker image storage. ` `❌ All tags were not found in either Podman image storage, or Docker image storage. `
+ `Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" ` + `Tag${podmanMissingTags.length !== 1 ? "s" : ""} "${podmanMissingTags.join(", ")}" `
+ `not found in Podman image storage, and tag${dockerMissingTags.length !== 1 ? "s" : ""} ` + `not found in Podman image storage, and tag${dockerMissingTags.length !== 1 ? "s" : ""} `
+ `"${dockerMissingTags.join(", ")}" not found in Docker image storage.` + `"${dockerMissingTags.join(", ")}" not found in Docker image storage.`
@ -123,25 +160,23 @@ async function run(): Promise<void> {
const isPodmanImageLatest = await isPodmanLocalImageLatest(); const isPodmanImageLatest = await isPodmanLocalImageLatest();
if (!isPodmanImageLatest) { if (!isPodmanImageLatest) {
core.warning( core.warning(
`The version of "${imageToPush}" in the Docker image storage is more recent ` `The version of "${sourceImages[0]}" in the Docker image storage is more recent `
+ `than the version in the Podman image storage. The image(s) from the Docker image storage ` + `than the version in the Podman image storage. The image(s) from the Docker image storage `
+ `will be pushed.` + `will be pushed.`
); );
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
isImageFromDocker = true; isImageFromDocker = true;
} }
else { else {
core.warning( core.warning(
`The version of "${imageToPush}" in the Podman image storage is more recent ` `The version of "${sourceImages[0]}" in the Podman image storage is more recent `
+ `than the version in the Docker image storage. The image(s) from the Podman image ` + `than the version in the Docker image storage. The image(s) from the Podman image `
+ `storage will be pushed.` + `storage will be pushed.`
); );
} }
} }
else if (allTagsinDocker) { else if (allTagsinDocker) {
imageToPush = `${dockerBaseUrl}/${imageToPush}`;
core.info( core.info(
`"${imageToPush}" was found in the Docker image storage, but not in the Podman ` `Tag "${sourceImages[0]}" 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 ` + `image storage. The image(s) will be pulled into Podman image storage, pushed, and then `
+ `removed from the Podman image storage.` + `removed from the Podman image storage.`
); );
@ -149,13 +184,12 @@ async function run(): Promise<void> {
} }
else { else {
core.info( core.info(
`"${imageToPush}" was found in the Podman image storage, but not in the Docker ` `Tag "${sourceImages[0]}" was found in the Podman image storage, but not in the Docker `
+ `image storage. The image(s) will be pushed from Podman image storage.` + `image storage. The image(s) will be pushed from Podman image storage.`
); );
} }
let pushMsg = `⏳ Pushing "${imageToPush}" with tag${tagsList.length !== 1 ? "s" : ""} ` let pushMsg = `⏳ Pushing "${sourceImages[0]}" to ${destinationImages.join(", ")}`;
+ `"${tagsList.join(", ")}" to "${registry}"`;
if (username) { if (username) {
pushMsg += ` as "${username}"`; pushMsg += ` as "${username}"`;
} }
@ -173,36 +207,23 @@ async function run(): Promise<void> {
} }
let digestFile = digestFileInput; let digestFile = digestFileInput;
const imageNameWithTag = `${imageToPush}:${tagsList[0]}`;
if (!digestFile) { if (!digestFile) {
digestFile = `${imageNameWithTag.replace( digestFile = `${sourceImages[0].replace(
/[/\\/?%*:|"<>]/g, /[/\\/?%*:|"<>]/g,
"-", "-",
)}_digest.txt`; )}_digest.txt`;
} }
const registryWithoutTrailingSlash = registry.replace(/\/$/, "");
const registryPath = `${registryWithoutTrailingSlash}/${imageInput}`;
core.info(`Combining image name "${imageInput}" and registry "${registry}" `
+ `to form registry path "${registryPath}"`);
if (imageInput.indexOf("/") > -1 && registry.indexOf("/") > -1) {
core.warning(`"${registryPath}" does not seem to be a valid registry path. `
+ `The registry path should not contain more than 2 slashes. `
+ `Refer to the Inputs section of the readme for naming image and registry.`);
}
// push the image // push the image
for (const tag of tagsList) { for (const destinationImage of destinationImages) {
const imageWithTag = `${imageToPush}:${tag}`;
const registryPathWithTag = `${registryPath}:${tag}`;
const args = [ const args = [
...(isImageFromDocker ? dockerPodmanOpts : []),
"push", "push",
"--quiet", "--quiet",
"--digestfile", "--digestfile",
digestFile, digestFile,
imageWithTag, isImageFromDocker ? getFullDockerImageName(sourceImages[0]) : sourceImages[0],
registryPathWithTag, destinationImage,
]; ];
if (podmanExtraArgs.length > 0) { if (podmanExtraArgs.length > 0) {
@ -220,9 +241,9 @@ async function run(): Promise<void> {
} }
await execute(await getPodmanPath(), args); await execute(await getPodmanPath(), args);
core.info(`✅ Successfully pushed "${imageWithTag}" to "${registryPathWithTag}"`); core.info(`✅ Successfully pushed "${sourceImages[0]}" to "${destinationImage}"`);
registryPathList.push(registryPathWithTag); registryPathList.push(destinationImage);
try { try {
const digest = (await fs.promises.readFile(digestFile)).toString(); const digest = (await fs.promises.readFile(digestFile)).toString();
@ -241,24 +262,21 @@ async function run(): Promise<void> {
} }
async function pullImageFromDocker(): Promise<ImageStorageCheckResult> { async function pullImageFromDocker(): Promise<ImageStorageCheckResult> {
core.info(`🔍 Checking if "${imageToPush}" with tag${tagsList.length !== 1 ? "s" : ""} ` core.info(`🔍 Checking if "${sourceImages.join(", ")}" present in the local Docker image storage`);
+ `"${tagsList.join(", ")}" is present in the local Docker image storage`);
let imageWithTag;
const foundTags: string[] = []; const foundTags: string[] = [];
const missingTags: string[] = []; const missingTags: string[] = [];
try { try {
for (const tag of tagsList) { for (const imageWithTag of sourceImages) {
imageWithTag = `${imageToPush}:${tag}`;
const commandResult: ExecResult = await execute( const commandResult: ExecResult = await execute(
await getPodmanPath(), await getPodmanPath(),
[ "pull", `docker-daemon:${imageWithTag}` ], [ ...dockerPodmanOpts, "pull", `docker-daemon:${imageWithTag}` ],
{ ignoreReturnCode: true, failOnStdErr: false, group: true } { ignoreReturnCode: true, failOnStdErr: false, group: true }
); );
if (commandResult.exitCode === 0) { if (commandResult.exitCode === 0) {
foundTags.push(tag); foundTags.push(imageWithTag);
} }
else { else {
missingTags.push(tag); missingTags.push(imageWithTag);
} }
} }
} }
@ -274,24 +292,21 @@ async function pullImageFromDocker(): Promise<ImageStorageCheckResult> {
async function checkImageInPodman(): Promise<ImageStorageCheckResult> { async function checkImageInPodman(): Promise<ImageStorageCheckResult> {
// check if images exist in Podman's storage // check if images exist in Podman's storage
core.info(`🔍 Checking if "${imageToPush}" with tag${tagsList.length !== 1 ? "s" : ""} ` core.info(`🔍 Checking if "${sourceImages.join(", ")}" present in the local Podman image storage`);
+ `"${tagsList.join(", ")}" is present in the local Podman image storage`);
let imageWithTag;
const foundTags: string[] = []; const foundTags: string[] = [];
const missingTags: string[] = []; const missingTags: string[] = [];
try { try {
for (const tag of tagsList) { for (const imageWithTag of sourceImages) {
imageWithTag = `${imageToPush}:${tag}`;
const commandResult: ExecResult = await execute( const commandResult: ExecResult = await execute(
await getPodmanPath(), await getPodmanPath(),
[ "image", "exists", imageWithTag ], [ "image", "exists", imageWithTag ],
{ ignoreReturnCode: true } { ignoreReturnCode: true }
); );
if (commandResult.exitCode === 0) { if (commandResult.exitCode === 0) {
foundTags.push(tag); foundTags.push(imageWithTag);
} }
else { else {
missingTags.push(tag); missingTags.push(imageWithTag);
} }
} }
} }
@ -308,7 +323,7 @@ async function checkImageInPodman(): Promise<ImageStorageCheckResult> {
async function isPodmanLocalImageLatest(): Promise<boolean> { async function isPodmanLocalImageLatest(): Promise<boolean> {
// checking for only one tag as creation time will be // checking for only one tag as creation time will be
// same for all the tags present // same for all the tags present
const imageWithTag = `${imageToPush}:${tagsList[0]}`; const imageWithTag = sourceImages[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(), [
@ -323,9 +338,10 @@ async function isPodmanLocalImageLatest(): Promise<boolean> {
// appending 'docker.io/library' infront of image name as pulled image name // appending 'docker.io/library' infront of image name as pulled image name
// from Docker image storage starts with the 'docker.io/library' // from Docker image storage starts with the 'docker.io/library'
const pulledImageCreationTimeStamp = await execute(await getPodmanPath(), [ const pulledImageCreationTimeStamp = await execute(await getPodmanPath(), [
...dockerPodmanOpts,
"image", "image",
"inspect", "inspect",
`${dockerBaseUrl}/${imageWithTag}`, getFullDockerImageName(imageWithTag),
"--format", "--format",
"{{.Created}}", "{{.Created}}",
]); ]);
@ -337,13 +353,41 @@ async function isPodmanLocalImageLatest(): Promise<boolean> {
return podmanImageTime > dockerImageTime; return podmanImageTime > dockerImageTime;
} }
// remove the pulled image from the Podman image storage async function createDockerPodmanImageStroage(): Promise<void> {
async function removeDockerImage(): Promise<void> { core.info(`Creating temporary Podman image storage for pulling from Docker daemon`);
if (imageToPush) { dockerPodmanRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "podman-from-docker-"));
core.info(`Removing "${imageToPush}" from the Podman image storage`);
for (const tag of tagsList) { dockerPodmanOpts = [ "--root", dockerPodmanRoot ];
const imageWithTag = `${imageToPush}:${tag}`;
await execute(await getPodmanPath(), [ "rmi", imageWithTag ]); if (await isStorageDriverOverlay()) {
const fuseOverlayfsPath = await findFuseOverlayfsPath();
if (fuseOverlayfsPath) {
core.info(`Overriding storage mount_program with "fuse-overlayfs" in environment`);
dockerPodmanOpts.push("--storage-opt");
dockerPodmanOpts.push(`overlay.mount_program=${fuseOverlayfsPath}`);
}
else {
core.warning(`"fuse-overlayfs" is not found. Install it before running this action. `
+ `For more detail see https://github.com/redhat-actions/buildah-build/issues/45`);
}
}
else {
core.info("Storage driver is not 'overlay', so not overriding storage configuration");
}
}
async function removeDockerPodmanImageStroage(): Promise<void> {
if (dockerPodmanRoot) {
try {
core.info(`Removing temporary Podman image storage for pulling from Docker daemon`);
await execute(
await getPodmanPath(),
[ ...dockerPodmanOpts, "rmi", "-a", "-f" ]
);
await fs.promises.rmdir(dockerPodmanRoot, { recursive: true });
}
catch (err) {
core.warning(`Failed to remove podman image stroage ${dockerPodmanRoot}: ${err}`);
} }
} }
} }
@ -400,12 +444,17 @@ async function execute(
} }
} }
run() async function main(): Promise<void> {
.then(async () => { try {
if (isImageFromDocker) { await createDockerPodmanImageStroage();
await removeDockerImage(); await run();
} }
}) finally {
await removeDockerPodmanImageStroage();
}
}
main()
.catch((err) => { .catch((err) => {
core.setFailed(err.message); core.setFailed(err.message);
}); });

View file

@ -3,6 +3,88 @@
* Licensed under the MIT License. See LICENSE file in the project root for license information. * Licensed under the MIT License. See LICENSE file in the project root for license information.
**************************************************************************************************/ **************************************************************************************************/
import * as ini from "ini";
import { promises as fs } from "fs";
import * as core from "@actions/core";
import * as path from "path";
import * as io from "@actions/io";
import * as os from "os";
async function findStorageDriver(filePaths: string[]): Promise<string> {
let storageDriver = "";
for (const filePath of filePaths) {
core.debug(`Checking if the storage file exists at ${filePath}`);
if (await fileExists(filePath)) {
core.debug(`Storage file exists at ${filePath}`);
const fileContent = ini.parse(await fs.readFile(filePath, "utf-8"));
if (fileContent.storage.driver) {
storageDriver = fileContent.storage.driver;
}
}
}
return storageDriver;
}
export async function isStorageDriverOverlay(): Promise<boolean> {
let xdgConfigHome = path.join(os.homedir(), ".config");
if (process.env.XDG_CONFIG_HOME) {
xdgConfigHome = process.env.XDG_CONFIG_HOME;
}
const filePaths: string[] = [
"/etc/containers/storage.conf",
path.join(xdgConfigHome, "containers/storage.conf"),
];
const storageDriver = await findStorageDriver(filePaths);
return (storageDriver === "overlay");
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
}
catch (err) {
return false;
}
}
export async function findFuseOverlayfsPath(): Promise<string | undefined> {
let fuseOverlayfsPath;
try {
fuseOverlayfsPath = await io.which("fuse-overlayfs");
}
catch (err) {
core.debug(err);
}
return fuseOverlayfsPath;
}
export function splitByNewline(s: string): string[] { export function splitByNewline(s: string): string[] {
return s.split(/\r?\n/); return s.split(/\r?\n/);
} }
export function isFullImageName(image: string): boolean {
return image.indexOf(":") > 0;
}
export function getFullImageName(image: string, tag: string): string {
if (isFullImageName(tag)) {
return tag;
}
return `${image}:${tag}`;
}
const DOCKER_IO = `docker.io`;
const DOCKER_IO_NAMESPACED = DOCKER_IO + `/library`;
export function getFullDockerImageName(image: string): string {
switch (image.split("/").length) {
case 1:
return `${DOCKER_IO_NAMESPACED}/${image}`;
case 2:
return `${DOCKER_IO}/${image}`;
default:
return image;
}
}