Skip to content

Commit

Permalink
feat: support rhel bib
Browse files Browse the repository at this point in the history
Adds a new preference that allows you to switch between:
- 'image' - the default, picks up builder from image label (or use centos
  if none is defined)
- 'Centos' - always use Centos builder
- 'RHEL' - always use RHEL builder

getBuilder() is added to determine which builder to use, and
createBuilderImageOptions() has an optional parameter to specify the
builder to use.

Signed-off-by: Tim deBoer <[email protected]>
  • Loading branch information
deboer-tim committed May 6, 2024
1 parent bd1c8d5 commit b174a70
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 13 deletions.
10 changes: 10 additions & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
"default": 30,
"maximum": 120,
"description": "Build timeout (in minutes)"
},
"bootc.builder": {
"type": "string",
"default": "image",
"enum": [
"image",
"Centos",
"RHEL"
],
"description": "Builder image"
}
}
},
Expand Down
107 changes: 102 additions & 5 deletions packages/backend/src/build-disk-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@
***********************************************************************/

import { beforeEach, expect, test, vi } from 'vitest';
import { buildExists, createBuilderImageOptions, getUnusedName } from './build-disk-image';
import { bootcImageBuilderName } from './constants';
import type { ContainerInfo } from '@podman-desktop/api';
import { buildExists, createBuilderImageOptions, getBuilder, getUnusedName } from './build-disk-image';
import { bootcImageBuilderCentos, bootcImageBuilderRHEL } from './constants';
import type { ContainerInfo, Configuration } from '@podman-desktop/api';
import { containerEngine } from '@podman-desktop/api';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import * as fs from 'node:fs';
import { resolve } from 'node:path';

const configurationGetConfigurationMock = vi.fn();

const config: Configuration = {
get: configurationGetConfigurationMock,
has: () => true,
update: vi.fn(),
};

vi.mock('@podman-desktop/api', async () => {
return {
env: {
Expand All @@ -33,6 +41,9 @@ vi.mock('@podman-desktop/api', async () => {
containerEngine: {
listContainers: vi.fn().mockReturnValue([]),
},
configuration: {
getConfiguration: () => config,
},
};
});

Expand All @@ -53,7 +64,7 @@ test('check image builder options', async () => {

expect(options).toBeDefined();
expect(options.name).toEqual(name);
expect(options.Image).toEqual(bootcImageBuilderName);
expect(options.Image).toEqual(bootcImageBuilderCentos);
expect(options.HostConfig).toBeDefined();
if (options.HostConfig?.Binds) {
expect(options.HostConfig.Binds[0]).toEqual(build.folder + ':/output/');
Expand Down Expand Up @@ -84,7 +95,7 @@ test('check image builder with multiple types', async () => {

expect(options).toBeDefined();
expect(options.name).toEqual(name);
expect(options.Image).toEqual(bootcImageBuilderName);
expect(options.Image).toEqual(bootcImageBuilderCentos);
expect(options.HostConfig).toBeDefined();
if (options.HostConfig?.Binds) {
expect(options.HostConfig.Binds[0]).toEqual(build.folder + ':/output/');
Expand Down Expand Up @@ -181,6 +192,18 @@ test('test if blank string is passed into filesystem, it is not included in the
expect(options.Cmd).not.toContain('--rootfs');
});

test('test specified builder is used', async () => {
const builder = 'foo-builder';
const build = {
image: 'test-image',
type: ['vmdk'],
} as BootcBuildInfo;
const options = createBuilderImageOptions('my-image', build, builder);

expect(options).toBeDefined();
expect(options.Image).toEqual(builder);
});

test('check we pick unused container name', async () => {
const basename = 'test';
let name = await getUnusedName(basename);
Expand Down Expand Up @@ -230,3 +253,77 @@ test('check build exists', async () => {
exists = await buildExists(folder, ['iso', 'raw']);
expect(exists).toEqual(false);
});

test('check uses RHEL builder', async () => {
configurationGetConfigurationMock.mockReturnValue('RHEL');

const build = {
image: 'test-image',
type: ['ami'],
} as BootcBuildInfo;
const builder = await getBuilder(build);

expect(builder).toBeDefined();
expect(builder).toEqual(bootcImageBuilderRHEL);
});

test('check uses Centos builder', async () => {
configurationGetConfigurationMock.mockReturnValue('centos');

const build = {
image: 'test-image',
type: ['ami'],
} as BootcBuildInfo;
const builder = await getBuilder(build);

expect(builder).toBeDefined();
expect(builder).toEqual(bootcImageBuilderCentos);
});

test('check uses image preferred builder (RHEL)', async () => {
configurationGetConfigurationMock.mockReturnValue('image');

const listImagesMock = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(containerEngine as any).listImages = listImagesMock;
listImagesMock.mockResolvedValue([
{
RepoTags: ['test-image:latest'],
Labels: { 'bootc.diskimage-builder': 'registry.redhat.io/rhel9/bootc-image-builder' },
},
]);

const build = {
image: 'test-image',
tag: 'latest',
type: ['iso'],
} as BootcBuildInfo;
const builder = await getBuilder(build);

expect(builder).toBeDefined();
expect(builder).toEqual(bootcImageBuilderRHEL);
});

test('check uses image preferred builder (Centos)', async () => {
configurationGetConfigurationMock.mockReturnValue('image');

const listImagesMock = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(containerEngine as any).listImages = listImagesMock;
listImagesMock.mockResolvedValue([
{
RepoTags: ['test-image:latest'],
Labels: { 'bootc.diskimage-builder': 'quay.io/centos-bootc/bootc-image-builder' },
},
]);

const build = {
image: 'test-image',
tag: 'latest',
type: ['iso'],
} as BootcBuildInfo;
const builder = await getBuilder(build);

expect(builder).toBeDefined();
expect(builder).toEqual(bootcImageBuilderCentos);
});
40 changes: 34 additions & 6 deletions packages/backend/src/build-disk-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import * as extensionApi from '@podman-desktop/api';
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import * as containerUtils from './container-utils';
import { bootcImageBuilderContainerName, bootcImageBuilderName } from './constants';
import { bootcImageBuilder, bootcImageBuilderCentos, bootcImageBuilderRHEL } from './constants';
import type { BootcBuildInfo, BuildType } from '/@shared/src/models/bootc';
import type { History } from './history';
import * as machineUtils from './machine-utils';
import { telemetryLogger } from './extension';
import { getConfigurationValue, telemetryLogger } from './extension';

export async function buildExists(folder: string, types: BuildType[]) {
let exists = false;
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov
.withProgress(
{ location: extensionApi.ProgressLocation.TASK_WIDGET, title: `Building disk image ${build.image}` },
async progress => {
const buildContainerName = build.image.split('/').pop() + bootcImageBuilderContainerName;
const buildContainerName = build.image.split('/').pop() + '-' + bootcImageBuilder;
let successful: boolean = false;
let logData: string = 'Build Image Log --------\n';
logData += 'ID: ' + build.id + '\n';
Expand All @@ -118,11 +118,15 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov
fs.unlinkSync(logPath);
}

// determine which bootc image builder to use based on the image
// being built and the current preferences
const builder = await getBuilder(build);

// Preliminary Step 0. Create the "bootc-image-builder" container
// options that we will use to build the image. This will help with debugging
// as well as making sure we delete the previous build, etc.
const containerName = await getUnusedName(buildContainerName);
const buildImageContainer = createBuilderImageOptions(containerName, build);
const buildImageContainer = createBuilderImageOptions(containerName, build, builder);
logData += JSON.stringify(buildImageContainer, undefined, 2);
logData += '\n----------\n';
try {
Expand Down Expand Up @@ -304,8 +308,32 @@ export async function getUnusedName(name: string): Promise<string> {
return unusedName;
}

export async function getBuilder(build: BootcBuildInfo): Promise<string> {
// check image for builder to use
const buildProp = await getConfigurationValue<string>('builder');

if (buildProp === 'RHEL') {
// use to rhel if that's the preference
return bootcImageBuilderRHEL;
} else if (buildProp === 'image') {
// or use rhel if the preference comes from the image label
// AND we detect the rhel label
const image = `${build.image}:${build.tag}`;
const buildLabel = await containerUtils.getImageBuilderLabel(image);
if (buildLabel === 'registry.redhat.io/rhel9/bootc-image-builder') {
return bootcImageBuilderRHEL;
}
}
// otherwise, always use centos
return bootcImageBuilderCentos;
}

// Create builder options for the "bootc-image-builder" container
export function createBuilderImageOptions(name: string, build: BootcBuildInfo): ContainerCreateOptions {
export function createBuilderImageOptions(
name: string,
build: BootcBuildInfo,
builder?: string,
): ContainerCreateOptions {
const cmd = [`${build.image}:${build.tag}`, '--output', '/output/', '--local'];

build.type.forEach(t => cmd.push('--type', t));
Expand All @@ -323,7 +351,7 @@ export function createBuilderImageOptions(name: string, build: BootcBuildInfo):
// Create the image options for the "bootc-image-builder" container
const options: ContainerCreateOptions = {
name: name,
Image: bootcImageBuilderName,
Image: builder ?? bootcImageBuilderCentos,
Tty: true,
HostConfig: {
Privileged: true,
Expand Down
6 changes: 4 additions & 2 deletions packages/backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
***********************************************************************/

// Image related
export const bootcImageBuilderContainerName = '-bootc-image-builder';
export const bootcImageBuilderName = 'quay.io/centos-bootc/bootc-image-builder:latest-1714633180';
export const bootcImageBuilder = 'bootc-image-builder';
export const bootcImageBuilderCentos = 'quay.io/centos-bootc/bootc-image-builder:latest-1714474808';
export const bootcImageBuilderRHEL = 'registry.redhat.io/rhel9/bootc-image-builder:9.4';
export const bootcImageBuilderLabel = 'bootc.diskimage-builder';
17 changes: 17 additions & 0 deletions packages/backend/src/container-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
deleteOldImages,
inspectImage,
inspectManifest,
getImageBuilderLabel,
} from './container-utils';

const mocks = vi.hoisted(() => ({
Expand Down Expand Up @@ -265,3 +266,19 @@ test('test running inspectManifest', async () => {
expect(result.engineName).toBe('podman');
expect(result.manifests).toBeDefined();
});

test('test image builder label', async () => {
const listImagesMock = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(extensionApi.containerEngine as any).listImages = listImagesMock;
listImagesMock.mockResolvedValue([
{ RepoTags: ['test.io/name:1'] },
{ RepoTags: ['test.io/name:2'], Labels: { 'bootc.diskimage-builder': 'foo' } },
{ RepoTags: ['test.io/name:3'] },
{ RepoTags: ['test.io/name:4', 'keep-me'] },
]);

// Test that it'll find the right image and label
const result = await getImageBuilderLabel('test.io/name:2');
expect(result).toBe('foo');
});
14 changes: 14 additions & 0 deletions packages/backend/src/container-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import type { ContainerCreateOptions } from '@podman-desktop/api';
import * as extensionApi from '@podman-desktop/api';
import { getConfigurationValue, telemetryLogger } from './extension';
import { bootcImageBuilderLabel } from './constants';

// Get the running container engine
export async function getContainerEngine(): Promise<extensionApi.ContainerProviderConnection> {
Expand Down Expand Up @@ -347,3 +348,16 @@ export async function getImagesFromManifest(
// Filter out the images that have the same digest value
return images.filter(image => image.Digest && digestValues.includes(image.Digest));
}

// Return the image builder label from the given image
export async function getImageBuilderLabel(imageId: string): Promise<string | undefined> {
try {
const images = await extensionApi.containerEngine.listImages();

const foundImage = images.find(i => i.RepoTags?.find(tag => tag === imageId));
return foundImage?.Labels[bootcImageBuilderLabel];
} catch (err) {
console.error('Error getting image label: ', err);
}
return undefined;
}

0 comments on commit b174a70

Please sign in to comment.