Add manifest feature (#85)

* Add manifest feature

Signed-off-by: divyansh42 <diagrawa@redhat.com>
This commit is contained in:
Divyanshu Agrawal 2021-11-17 15:09:25 +05:30 committed by GitHub
parent c7ca484deb
commit 3ffbc5da4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 227 additions and 69 deletions

View file

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

View file

@ -25,8 +25,8 @@ After building your image, use [push-to-registry](https://github.com/redhat-acti
| Input Name | Description | Default |
| ---------- | ----------- | ------- |
| arch | Label the image with this architecture, instead of defaulting to the host architecture. Refer to [Multi arch builds](#multi-arch-builds) for more information. | None (host architecture)
| platform | Label the image with this platform, instead of defaulting to the host platform. Refer to [Multi arch builds](#multi-arch-builds) for more information. | None (host platform)
| archs | Label the image with this architecture, instead of defaulting to the host architecture. Refer to [Multi arch builds](#multi-arch-builds) for more information. For multiple architectures, seperate them by a comma | None (host architecture)
| platforms | Label the image with this platform, instead of defaulting to the host platform. Refer to [Multi arch builds](#multi-arch-builds) for more information. For multiple platforms, seperate them by a comma | None (host platform)
| build-args | Build arguments to pass to the Docker build using `--build-arg`, if using a Containerfile that requires ARGs. Use the form `arg_name=arg_value`, and separate arguments with newlines. | None
| context | Path to directory to use as the build context. | `.`
| containerfiles\* | The list of Containerfile paths to perform a build using docker instructions. Separate filenames by newline. | **Required**
@ -45,7 +45,7 @@ After building your image, use [push-to-registry](https://github.com/redhat-acti
| Input Name | Description | Default |
| ---------- | ----------- | ------- |
| arch | Label the image with this architecture, instead of defaulting to the host architecture. Refer to [Multi arch builds](#multi-arch-builds) for more information. | None (host architecture)
| archs | Label the image with this architecture, instead of defaulting to the host architecture. Refer to [Multi arch builds](#multi-arch-builds) for more information. For multiple architectures, seperate them by a comma | None (host architecture)
| base-image | The base image to use for the container. | **Required**
| content | Paths to files or directories to copy inside the container to create the file image. This is a multiline input to allow you to copy multiple files/directories.| None
| entrypoint | The entry point to set for the container. Separate arguments by newline. | None
@ -197,17 +197,22 @@ sudo podman run --rm --privileged docker.io/tonistiigi/binfmt --install all
```
This registration remains active until the host reboots.
### The `arch` and `platform` inputs
The `arch` and `platform` arguments override the Architecture and Platform labels in the output image, respectively. They do not actually affect the architectures and platforms the output image will run on. The image must still be built for the required architecture or platform.
### The `archs` and `platforms` inputs
The `archs` and `platforms` arguments override the Architecture and Platform labels in the output image, respectively. They do not actually affect the architectures and platforms the output image will run on. The image must still be built for the required architecture or platform.
There is a simple example [in this issue](https://github.com/redhat-actions/buildah-build/issues/60#issuecomment-876552452).
### Creating a Multi-Arch Image List
Use the [buildah manifest](https://github.com/containers/buildah/blob/main/docs/buildah-manifest.1.md) command to bundle images into an image list, so multiple image can be referenced by the same repository tag.
There are examples and explanations of the `manifest` command [in this issue](https://github.com/containers/buildah/issues/1590).
Input `archs` and `platforms` is provided to build the multi architecture images. If one of these input is provided then a [manifest](https://github.com/containers/buildah/blob/main/docs/buildah-manifest.1.md) is built with the multiple architecture images. Name of the manifest is taken from the inputs `image` and `tags`.
Incase multiple tags are provided then multiple manifest is created based on the provided tags.
This action does not support the `manifest` command at this time, but there is [an issue open](https://github.com/redhat-actions/buildah-build/issues/61).
Use the `archs` and `platforms` inputs to build multi-architecture images. The name of the manifest is determined by the image and tags inputs.
If multiple tags are provided, multiple equivalent manifests will be created with the given tags.
[`push-to-registry`](https://github.com/redhat-actions/push-to-registry) action can be used to push the generated image manifest.
## Build with docker/metadata-action

View file

@ -54,16 +54,24 @@ inputs:
default: 'false'
required: false
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
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
platform:
description: |
Label the image with this PLATFORM, instead of defaulting to the host platform.
Only supported for containerfile builds.
required: false
platforms:
description: |
'Same as input 'platform', use this for multiple platforms.
Seperate them by a comma'
required: false
extra-args:
description: |
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 .",
"bundle": "ncc build src/index.ts --source-map --minify",
"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": [],
"author": "Red Hat",

View file

@ -21,12 +21,15 @@ export interface BuildahConfigSettings {
interface Buildah {
buildUsingDocker(
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>;
from(baseImage: string): Promise<CommandResult>;
config(container: string, setting: BuildahConfigSettings): Promise<CommandResult>;
copy(container: string, contentToCopy: string[]): Promise<CommandResult | undefined>;
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 {
@ -64,7 +67,8 @@ export class BuildahCli implements Buildah {
async buildUsingDocker(
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> {
const args: string[] = [ "bud" ];
if (arch) {
@ -169,13 +173,31 @@ export class BuildahCli implements Buildah {
return this.execute(args);
}
async tag(imageName: string, tags: string[]): Promise<CommandResult> {
async tag(imageName: string, tags: string[]): Promise<void> {
const args: string[] = [ "tag" ];
const builtImage = [];
for (const tag of tags) {
args.push(getFullImageName(imageName, tag));
builtImage.push(getFullImageName(imageName, tag));
}
core.info(`Tagging the built image with tags ${tags.toString()}`);
return this.execute(args);
await this.execute(args);
core.info(`✅ Successfully built image${builtImage.length !== 1 ? "s" : ""} "${builtImage.join(", ")}"`);
}
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 {

View file

@ -1,13 +1,14 @@
// This file was auto-generated by action-io-generator. Do not edit by hand!
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
* Default: None.
*/
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
* Default: None.
*/
@ -98,6 +99,13 @@ export enum Inputs {
* Default: None.
*/
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
* Required: false

View file

@ -10,7 +10,7 @@ import { Inputs, Outputs } from "./generated/inputs-outputs";
import { BuildahCli, BuildahConfigSettings } from "./buildah";
import {
getArch, getPlatform, getContainerfiles, getInputList, splitByNewline,
isFullImageName, getFullImageName,
isFullImageName, getFullImageName, removeIllegalCharacters,
} from "./utils";
export async function run(): Promise<void> {
@ -55,34 +55,67 @@ export async function run(): Promise<void> {
const newImage = getFullImageName(image, tagsList[0]);
const useOCI = core.getInput(Inputs.OCI) === "true";
const arch = getArch();
const platform = getPlatform();
const archs = getArch();
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.");
}
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 {
if (platform) {
if (platforms.length > 0) {
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(", ")}"`);
const builtImage = [];
const builtManifest = [];
for (const tag of tagsList) {
const manifestName = getFullImageName(image, tag);
await cli.manifestCreate(manifestName);
builtManifest.push(manifestName);
for (const arch of archs) {
const tagSuffix = removeIllegalCharacters(arch);
builtImage.push(`${newImage}-${tagSuffix}`);
await cli.manifestAdd(manifestName, `${newImage}-${tagSuffix}`);
}
for (const platform of platforms) {
const tagSuffix = removeIllegalCharacters(platform);
builtImage.push(`${newImage}-${tagSuffix}`);
await cli.manifestAdd(manifestName, `${newImage}-${tagSuffix}`);
}
}
core.info(`✅ Successfully built image${builtImage.length !== 1 ? "s" : ""} "${builtImage.join(", ")}" `
+ `and manifest${builtManifest.length !== 1 ? "s" : ""} "${builtManifest.join(", ")}"`);
}
else if (tagsList.length > 1) {
await cli.tag(image, tagsList);
}
else if (tagsList.length === 1) {
core.info(`✅ Successfully built image "${getFullImageName(image, tagsList[0])}"`);
}
core.setOutput(Outputs.IMAGE, image);
core.setOutput(Outputs.TAGS, tags);
core.setOutput(Outputs.IMAGE_WITH_TAG, newImage);
}
async function doBuildUsingContainerFiles(
cli: BuildahCli, newImage: string, workspace: string, containerFiles: string[], useOCI: boolean, arch: string,
platform: string, labels: string[],
cli: BuildahCli, newImage: string, workspace: string, containerFiles: string[], useOCI: boolean, archs: string[],
platforms: string[], labels: string[],
): Promise<void> {
if (containerFiles.length === 1) {
core.info(`Performing build from Containerfile`);
@ -104,13 +137,36 @@ async function doBuildUsingContainerFiles(
const lines = splitByNewline(inputExtraArgsStr);
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 = removeIllegalCharacters(arch);
await cli.buildUsingDocker(
`${newImage}-${tagSuffix}`, context, containerFileAbsPaths, buildArgs,
useOCI, labels, layers, buildahBudExtraArgs, arch, undefined
);
}
for (const platform of platforms) {
const tagSuffix = removeIllegalCharacters(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(
cli: BuildahCli, newImage: string, useOCI: boolean, arch: string, labels: string[],
cli: BuildahCli, newImage: string, useOCI: boolean, archs: string[], labels: string[],
): Promise<void> {
core.info(`Performing build from scratch`);
@ -124,17 +180,35 @@ async function doBuildFromScratch(
const container = await cli.from(baseImage);
const containerId = container.output.replace("\n", "");
const newImageConfig: BuildahConfigSettings = {
entrypoint,
port,
workingdir: workingDir,
envs,
arch,
labels,
};
await cli.config(containerId, newImageConfig);
await cli.copy(containerId, content);
await cli.commit(containerId, newImage, useOCI);
if (archs.length > 0) {
for (const arch of archs) {
const tagSuffix = removeIllegalCharacters(arch);
const newImageConfig: BuildahConfigSettings = {
entrypoint,
port,
workingdir: workingDir,
envs,
arch,
labels,
};
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);

View file

@ -65,24 +65,50 @@ export function splitByNewline(s: string): string[] {
return s.split(/\r?\n/);
}
export function getArch(): string {
// 'arch' should be used over 'archs', see https://github.com/redhat-actions/buildah-build/issues/60
const archs = core.getInput(Inputs.ARCHS);
export function getArch(): string[] {
const archs = getCommaSeperatedInput(Inputs.ARCHS);
const arch = core.getInput(Inputs.ARCH);
if (arch && archs) {
if (arch && archs.length > 0) {
core.warning(
`Both "${Inputs.ARCH}" and "${Inputs.ARCHS}" inputs are set. `
+ `Please use only one of these two inputs, as they are aliases of one another. `
+ `"${Inputs.ARCH}" takes precedence.`
+ `Please use "${Inputs.ARCH}" if you want to provide multiple `
+ `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 {
return core.getInput(Inputs.PLATFORM);
export function getPlatform(): string[] {
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[] {
@ -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 {
return image.indexOf(":") > 0;
}
@ -125,3 +165,7 @@ export function getFullImageName(image: string, tag: string): string {
}
return `${image}:${tag}`;
}
export function removeIllegalCharacters(item: string): string {
return item.replace(/[^a-zA-Z0-9 ]/g, "");
}