Add manifest feature

Signed-off-by: divyansh42 <diagrawa@redhat.com>
This commit is contained in:
divyansh42 2021-11-08 16:52:39 +05:30
parent c7ca484deb
commit 85208924b2
9 changed files with 198 additions and 59 deletions

View file

@ -20,7 +20,6 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: [ amd64, i386, arm64v8 ]
install_latest: [ true, false ] install_latest: [ true, false ]
steps: steps:
@ -45,8 +44,7 @@ jobs:
run: | run: |
cat > Containerfile<<EOF cat > Containerfile<<EOF
ARG ARCH FROM docker.io/alpine:3.14
FROM docker.io/\${ARCH}/alpine:3.14
RUN echo "hello world" RUN echo "hello world"
@ -59,8 +57,7 @@ jobs:
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }} tags: ${{ env.IMAGE_TAG }}
arch: ${{ matrix.arch }} archs: amd64, ppc64le, arm64
build-args: ARCH=${{ matrix.arch }}
containerfiles: | containerfiles: |
./Containerfile ./Containerfile
@ -83,6 +80,10 @@ jobs:
run: | run: |
podman run --rm ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }} podman run --rm ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }}
- name: Check manifest
run: |
buildah manifest inspect ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }}
build-multiplatform-containerfile: build-multiplatform-containerfile:
name: Build multi-platform image using Containerfile name: Build multi-platform image using Containerfile
env: env:
@ -91,7 +92,6 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [ "linux/amd64", "linux/arm64/v8" ]
install_latest: [ true, false ] install_latest: [ true, false ]
steps: steps:
@ -129,7 +129,7 @@ jobs:
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }} tags: ${{ env.IMAGE_TAG }}
platform: ${{ matrix.platform }} platforms: linux/amd64, linux/ppc64le
containerfiles: | containerfiles: |
./Containerfile ./Containerfile
@ -142,11 +142,10 @@ jobs:
- name: Check images created - name: Check images created
run: buildah images | grep '${{ env.IMAGE_NAME }}' run: buildah images | grep '${{ env.IMAGE_NAME }}'
- name: Check image metadata - name: Check manifest
run: | run: |
set -x set -x
buildah inspect ${{ steps.build_image_multiplatform.outputs.image }}:${{ env.IMAGE_TAG }} | jq ".OCIv1.architecture" buildah manifest inspect ${{ steps.build_image_multiplatform.outputs.image }}:${{ env.IMAGE_TAG }}
buildah inspect ${{ steps.build_image_multiplatform.outputs.image }}:${{ env.IMAGE_TAG }} | jq ".Docker.architecture"
- name: Run image - name: Run image
run: | run: |
@ -160,7 +159,6 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: [ amd64, i386, arm64v8 ]
install_latest: [ true, false ] install_latest: [ true, false ]
steps: steps:
@ -230,8 +228,7 @@ jobs:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: ${{ env.IMAGE_TAG }} tags: ${{ env.IMAGE_TAG }}
base-image: 'registry.access.redhat.com/openjdk/openjdk-11-rhel7' base-image: 'registry.access.redhat.com/openjdk/openjdk-11-rhel7'
arch: ${{ matrix.arch }} archs: amd64, i386, ppc64le
build-args: ARCH=${{ matrix.arch }}
# To avoid hardcoding a particular version of the binary. # To avoid hardcoding a particular version of the binary.
content: | content: |
./spring-petclinic/target/spring-petclinic-*.jar ./spring-petclinic/target/spring-petclinic-*.jar
@ -251,8 +248,7 @@ jobs:
- name: Check images created - name: Check images created
run: buildah images | grep '${{ env.IMAGE_NAME }}' run: buildah images | grep '${{ env.IMAGE_NAME }}'
- name: Check image metadata - name: Check manifest
run: | run: |
set -x set -x
buildah inspect ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }} | jq ".OCIv1.architecture" buildah manifest inspect ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }}
buildah inspect ${{ steps.build_image_multiarch.outputs.image }}:${{ env.IMAGE_TAG }} | jq ".Docker.architecture"

View file

@ -54,16 +54,24 @@ inputs:
default: 'false' default: 'false'
required: false required: false
arch: arch:
description: 'Label the image with this ARCH, instead of defaulting to the host architecture.' description:
'Label the image with this ARCH, instead of defaulting to the host architecture'
required: false required: false
archs: archs:
description: 'Alias for "arch". "arch" takes precedence if both are set.' description: |
'Same as input 'arch', use this for multiple architectures.
Seperate them by a comma'
required: false required: false
platform: platform:
description: | description: |
Label the image with this PLATFORM, instead of defaulting to the host platform. Label the image with this PLATFORM, instead of defaulting to the host platform.
Only supported for containerfile builds. Only supported for containerfile builds.
required: false required: false
platforms:
description: |
'Same as input 'platform', use this for multiple platforms.
Seperate them by a comma'
required: false
extra-args: extra-args:
description: | description: |
Extra args to be passed to buildah bud. Extra args to be passed to buildah bud.

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

@ -14,7 +14,8 @@
"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" "lint": "eslint . --max-warnings=0",
"generate-ios": "npx action-io-generator -w -o ./src/generated/inputs-outputs.ts"
}, },
"keywords": [], "keywords": [],
"author": "Red Hat", "author": "Red Hat",

View file

@ -21,12 +21,15 @@ export interface BuildahConfigSettings {
interface Buildah { interface Buildah {
buildUsingDocker( buildUsingDocker(
image: string, context: string, containerFiles: string[], buildArgs: string[], image: string, context: string, containerFiles: string[], buildArgs: string[],
useOCI: boolean, arch: string, platform: string, labels: string[], layers: string, extraArgs: string[] useOCI: boolean, labels: string[], layers: string,
extraArgs: string[], arch?: string, platform?: string,
): Promise<CommandResult>; ): Promise<CommandResult>;
from(baseImage: string): Promise<CommandResult>; from(baseImage: string): Promise<CommandResult>;
config(container: string, setting: BuildahConfigSettings): Promise<CommandResult>; config(container: string, setting: BuildahConfigSettings): Promise<CommandResult>;
copy(container: string, contentToCopy: string[]): Promise<CommandResult | undefined>; copy(container: string, contentToCopy: string[]): Promise<CommandResult | undefined>;
commit(container: string, newImageName: string, useOCI: boolean): Promise<CommandResult>; commit(container: string, newImageName: string, useOCI: boolean): Promise<CommandResult>;
manifestCreate(manifest: string): Promise<void>;
manifestAdd(manifest: string, imageName: string, tags: string[]): Promise<void>;
} }
export class BuildahCli implements Buildah { export class BuildahCli implements Buildah {
@ -64,7 +67,8 @@ export class BuildahCli implements Buildah {
async buildUsingDocker( async buildUsingDocker(
image: string, context: string, containerFiles: string[], buildArgs: string[], image: string, context: string, containerFiles: string[], buildArgs: string[],
useOCI: boolean, arch: string, platform: string, labels: string[], layers: string, extraArgs: string[] useOCI: boolean, labels: string[], layers: string,
extraArgs: string[], arch?: string, platform?: string
): Promise<CommandResult> { ): Promise<CommandResult> {
const args: string[] = [ "bud" ]; const args: string[] = [ "bud" ];
if (arch) { if (arch) {
@ -178,6 +182,21 @@ export class BuildahCli implements Buildah {
return this.execute(args); return this.execute(args);
} }
async manifestCreate(manifest: string): Promise<void> {
const args: string[] = [ "manifest", "create" ];
args.push(manifest);
core.info(`Creating manifest ${manifest}`);
await this.execute(args);
}
async manifestAdd(manifest: string, image: string): Promise<void> {
const args: string[] = [ "manifest", "add" ];
args.push(manifest);
args.push(image);
core.info(`Adding image "${image}" to the manifest.`);
await this.execute(args);
}
private static convertArrayToStringArg(args: string[]): string { private static convertArrayToStringArg(args: string[]): string {
let arrayAsString = "["; let arrayAsString = "[";
args.forEach((arg) => { args.forEach((arg) => {

View file

@ -1,13 +1,14 @@
// This file was auto-generated by action-io-generator. Do not edit by hand! // This file was auto-generated by action-io-generator. Do not edit by hand!
export enum Inputs { export enum Inputs {
/** /**
* Label the image with this ARCH, instead of defaulting to the host architecture. * Label the image with this ARCH, instead of defaulting to the host architecture
* Required: false * Required: false
* Default: None. * Default: None.
*/ */
ARCH = "arch", ARCH = "arch",
/** /**
* Alias for "arch". "arch" takes precedence if both are set. * 'Same as input 'arch', use this for multiple architectures.
* Seperate them by a comma'
* Required: false * Required: false
* Default: None. * Default: None.
*/ */
@ -98,6 +99,13 @@ export enum Inputs {
* Default: None. * Default: None.
*/ */
PLATFORM = "platform", PLATFORM = "platform",
/**
* 'Same as input 'platform', use this for multiple platforms.
* Seperate them by a comma'
* Required: false
* Default: None.
*/
PLATFORMS = "platforms",
/** /**
* The port to expose when running containers based on image * The port to expose when running containers based on image
* Required: false * Required: false

View file

@ -10,7 +10,7 @@ import { Inputs, Outputs } from "./generated/inputs-outputs";
import { BuildahCli, BuildahConfigSettings } from "./buildah"; import { BuildahCli, BuildahConfigSettings } from "./buildah";
import { import {
getArch, getPlatform, getContainerfiles, getInputList, splitByNewline, getArch, getPlatform, getContainerfiles, getInputList, splitByNewline,
isFullImageName, getFullImageName, isFullImageName, getFullImageName, getTagSuffix,
} from "./utils"; } from "./utils";
export async function run(): Promise<void> { export async function run(): Promise<void> {
@ -55,34 +55,56 @@ export async function run(): Promise<void> {
const newImage = getFullImageName(image, tagsList[0]); const newImage = getFullImageName(image, tagsList[0]);
const useOCI = core.getInput(Inputs.OCI) === "true"; const useOCI = core.getInput(Inputs.OCI) === "true";
const arch = getArch(); const archs = getArch();
const platform = getPlatform(); const platforms = getPlatform();
if (arch && platform) { // core.debug(`Archs ---> ${archs.toString()}`);
// core.debug(`Platforms ---> ${platforms.toString()}`);
if ((archs.length > 0) && (platforms.length > 0)) {
throw new Error("The --platform option may not be used in combination with the --arch option."); throw new Error("The --platform option may not be used in combination with the --arch option.");
} }
if (containerFiles.length !== 0) { if (containerFiles.length !== 0) {
await doBuildUsingContainerFiles(cli, newImage, workspace, containerFiles, useOCI, arch, platform, labelsList); await doBuildUsingContainerFiles(cli, newImage, workspace, containerFiles, useOCI,
archs, platforms, labelsList);
} }
else { else {
if (platform) { if (platforms.length > 0) {
throw new Error("The --platform option is not supported for builds without containerfiles."); throw new Error("The --platform option is not supported for builds without containerfiles.");
} }
await doBuildFromScratch(cli, newImage, useOCI, arch, labelsList); await doBuildFromScratch(cli, newImage, useOCI, archs, labelsList);
} }
if (tagsList.length > 1) { if ((archs.length > 0) || (platforms.length > 0)) {
core.info(`Creating manifest with tag${tagsList.length !== 1 ? "s" : ""} "${tagsList.join(", ")}"`);
for (const tag of tagsList) {
const manifestName = getFullImageName(image, tag);
await cli.manifestCreate(manifestName);
for (const arch of archs) {
const tagSuffix = getTagSuffix(arch);
await cli.manifestAdd(manifestName, `${newImage}-${tagSuffix}`);
}
for (const platform of platforms) {
const tagSuffix = getTagSuffix(platform);
await cli.manifestAdd(manifestName, `${newImage}-${tagSuffix}`);
}
}
}
else if (tagsList.length > 1) {
await cli.tag(image, tagsList); await cli.tag(image, tagsList);
} }
core.setOutput(Outputs.IMAGE, image); core.setOutput(Outputs.IMAGE, image);
core.setOutput(Outputs.TAGS, tags); core.setOutput(Outputs.TAGS, tags);
core.setOutput(Outputs.IMAGE_WITH_TAG, newImage); core.setOutput(Outputs.IMAGE_WITH_TAG, newImage);
} }
async function doBuildUsingContainerFiles( async function doBuildUsingContainerFiles(
cli: BuildahCli, newImage: string, workspace: string, containerFiles: string[], useOCI: boolean, arch: string, cli: BuildahCli, newImage: string, workspace: string, containerFiles: string[], useOCI: boolean, archs: string[],
platform: string, labels: string[], platforms: string[], labels: string[],
): Promise<void> { ): Promise<void> {
if (containerFiles.length === 1) { if (containerFiles.length === 1) {
core.info(`Performing build from Containerfile`); core.info(`Performing build from Containerfile`);
@ -104,13 +126,36 @@ async function doBuildUsingContainerFiles(
const lines = splitByNewline(inputExtraArgsStr); const lines = splitByNewline(inputExtraArgsStr);
buildahBudExtraArgs = lines.flatMap((line) => line.split(" ")).map((arg) => arg.trim()); buildahBudExtraArgs = lines.flatMap((line) => line.split(" ")).map((arg) => arg.trim());
} }
await cli.buildUsingDocker(
newImage, context, containerFileAbsPaths, buildArgs, useOCI, arch, platform, labels, layers, buildahBudExtraArgs // since multi arch image can not have same tag
); // therefore, appending arch/platform in the tag
if (archs.length > 0 || platforms.length > 0) {
for (const arch of archs) {
const tagSuffix = getTagSuffix(arch);
await cli.buildUsingDocker(
`${newImage}-${tagSuffix}`, context, containerFileAbsPaths, buildArgs,
useOCI, labels, layers, buildahBudExtraArgs, arch, undefined
);
}
for (const platform of platforms) {
const tagSuffix = getTagSuffix(platform);
await cli.buildUsingDocker(
`${newImage}-${tagSuffix}`, context, containerFileAbsPaths, buildArgs,
useOCI, labels, layers, buildahBudExtraArgs, undefined, platform
);
}
}
else {
await cli.buildUsingDocker(
newImage, context, containerFileAbsPaths, buildArgs,
useOCI, labels, layers, buildahBudExtraArgs
);
}
} }
async function doBuildFromScratch( async function doBuildFromScratch(
cli: BuildahCli, newImage: string, useOCI: boolean, arch: string, labels: string[], cli: BuildahCli, newImage: string, useOCI: boolean, archs: string[], labels: string[],
): Promise<void> { ): Promise<void> {
core.info(`Performing build from scratch`); core.info(`Performing build from scratch`);
@ -124,17 +169,35 @@ async function doBuildFromScratch(
const container = await cli.from(baseImage); const container = await cli.from(baseImage);
const containerId = container.output.replace("\n", ""); const containerId = container.output.replace("\n", "");
const newImageConfig: BuildahConfigSettings = { if (archs.length > 0) {
entrypoint, for (const arch of archs) {
port, const tagSuffix = getTagSuffix(arch);
workingdir: workingDir, const newImageConfig: BuildahConfigSettings = {
envs, entrypoint,
arch, port,
labels, workingdir: workingDir,
}; envs,
await cli.config(containerId, newImageConfig); arch,
await cli.copy(containerId, content); labels,
await cli.commit(containerId, newImage, useOCI); };
await cli.config(containerId, newImageConfig);
await cli.copy(containerId, content);
await cli.commit(containerId, `${newImage}-${tagSuffix}`, useOCI);
}
}
else {
const newImageConfig: BuildahConfigSettings = {
entrypoint,
port,
workingdir: workingDir,
envs,
labels,
};
await cli.config(containerId, newImageConfig);
await cli.copy(containerId, content);
await cli.commit(containerId, newImage, useOCI);
}
} }
run().catch(core.setFailed); run().catch(core.setFailed);

View file

@ -65,24 +65,50 @@ export function splitByNewline(s: string): string[] {
return s.split(/\r?\n/); return s.split(/\r?\n/);
} }
export function getArch(): string { export function getArch(): string[] {
// 'arch' should be used over 'archs', see https://github.com/redhat-actions/buildah-build/issues/60 const archs = getCommaSeperatedInput(Inputs.ARCHS);
const archs = core.getInput(Inputs.ARCHS);
const arch = core.getInput(Inputs.ARCH); const arch = core.getInput(Inputs.ARCH);
if (arch && archs) { if (arch && archs.length > 0) {
core.warning( core.warning(
`Both "${Inputs.ARCH}" and "${Inputs.ARCHS}" inputs are set. ` `Both "${Inputs.ARCH}" and "${Inputs.ARCHS}" inputs are set. `
+ `Please use only one of these two inputs, as they are aliases of one another. ` + `Please use "${Inputs.ARCH}" if you want to provide multiple `
+ `"${Inputs.ARCH}" takes precedence.` + `ARCH else use ${Inputs.ARCH}". "${Inputs.ARCHS}" takes preference.`
); );
} }
return arch || archs; if (archs.length > 0) {
return archs;
}
else if (arch) {
return [ arch ];
}
return [];
} }
export function getPlatform(): string { export function getPlatform(): string[] {
return core.getInput(Inputs.PLATFORM); const platform = core.getInput(Inputs.PLATFORM);
const platforms = getCommaSeperatedInput(Inputs.PLATFORMS);
if (platform && platforms.length > 0) {
core.warning(
`Both "${Inputs.PLATFORM}" and "${Inputs.PLATFORMS}" inputs are set. `
+ `Please use "${Inputs.PLATFORMS}" if you want to provide multiple `
+ `PLATFORM else use ${Inputs.PLATFORM}". "${Inputs.PLATFORMS}" takes preference.`
);
}
if (platforms.length > 0) {
core.debug("return platforms");
return platforms;
}
else if (platform) {
core.debug("return platform");
return [ platform ];
}
core.debug("return empty");
return [];
} }
export function getContainerfiles(): string[] { export function getContainerfiles(): string[] {
@ -115,6 +141,20 @@ export function getInputList(name: string): string[] {
); );
} }
export function getCommaSeperatedInput(name: string): string[] {
const items = core.getInput(name);
if (items.length === 0) {
core.debug("empty");
return [];
}
const splitItems = items.split(",");
return splitItems
.reduce<string[]>(
(acc, line) => acc.concat(line).map((item) => item.trim()),
[],
);
}
export function isFullImageName(image: string): boolean { export function isFullImageName(image: string): boolean {
return image.indexOf(":") > 0; return image.indexOf(":") > 0;
} }
@ -125,3 +165,7 @@ export function getFullImageName(image: string, tag: string): string {
} }
return `${image}:${tag}`; return `${image}:${tag}`;
} }
export function getTagSuffix(item: string): string {
return item.replace(/[^a-zA-Z0-9 ]/g, "");
}