From cf6826c42a4b0d5ac32173aecc54d964202be777 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 31 Jul 2024 15:48:59 -0400 Subject: [PATCH] feat: use native podman build for linux ### What does this PR do? * Switches to using native podman building for Linux rather than using podman machine * Tested against using a manifest as well as a normal image. * Uses CLI commands the equivalant of doing `sudo podman run`. PD does not support running / viewing / using sudo root connections. So we use the CLI instead * Uses CLI commands for saving the image / importing as well. The reasoning is that importing requires `sudo` / privileged and retrieving via image ID does not work for saving via the API. ### Screenshot / video of UI ### What issues does this PR fix or reference? Closes https://github.com/containers/podman-desktop-extension-bootc/issues/623 ### How to test this PR? 1. Try on Linux (Fedora 40 or above) 2. Go to build and it should ask for credentials after a few moments of building 3. Successful image build Signed-off-by: Charlie Drage --- README.md | 18 +- packages/backend/src/api-impl.ts | 6 +- packages/backend/src/build-disk-image.spec.ts | 52 ++-- packages/backend/src/build-disk-image.ts | 238 ++++++++++++------ packages/backend/src/history.spec.ts | 4 + packages/backend/src/machine-utils.spec.ts | 19 +- packages/backend/src/machine-utils.ts | 17 +- packages/frontend/src/Build.spec.ts | 5 + packages/frontend/src/Build.svelte | 37 +++ packages/frontend/src/Homepage.spec.ts | 2 + .../frontend/src/lib/BootcActions.spec.ts | 1 + .../src/lib/BootcColumnActions.spec.ts | 1 + .../src/lib/BootcFolderColumn.spec.ts | 1 + .../frontend/src/lib/BootcImageColumn.spec.ts | 1 + packages/shared/src/BootcAPI.ts | 1 + packages/shared/src/models/bootc.ts | 1 + 16 files changed, 293 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index d4632997..773d0a55 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ The list above is what is supported by the underlying `bootc-image-builder` tech ## Requirements -### Requirement 1. Software and hardware requirements +### Prerequisites: Software and hardware requirements **OS:** @@ -126,7 +126,9 @@ Compatible on Windows, macOS & Linux * [Podman Desktop 1.10.0+](https://github.com/containers/podman-desktop) * [Podman 5.0.1+](https://github.com/containers/podman) -### Requirement 2. Rootful mode on Podman Machine +### Podman Machine (macOS / Windows) + +Podman Machine is required for macOS and Windows in order to run Podman as well as utilize filesystem privileges to build a disk image. Podman Machine requirements: * **Rootful mode enabled** @@ -144,14 +146,14 @@ Or set when initially creating a Podman Machine via Podman Desktop: ![rootful setup](https://raw.githubusercontent.com/containers/podman-desktop-extension-bootc/main/docs/img/rootful_setup.png) -**Linux users:** +### Escalated Privileges (Linux) -On Linux, you are unable to create a Podman Machine through the GUI of Podman Desktop, to create a rootful Podman Machine you can run the following commands: +During the build process, **you will be asked to enter your credentials** so that the bootc extension may run a `sudo podman run` underlying CLI command. + +Podman Desktop is ran as the logged-in user. However, bootc-image-builder requires escalated / sudo privileges to run a rootful container. + +You can find more information about what specific commands are being ran from the console logs of Podman Desktop. -```sh -podman machine init --memory 6144 --rootful -podman machine start -``` ## Installation diff --git a/packages/backend/src/api-impl.ts b/packages/backend/src/api-impl.ts index bc00e023..09bf10eb 100644 --- a/packages/backend/src/api-impl.ts +++ b/packages/backend/src/api-impl.ts @@ -25,7 +25,7 @@ import { History } from './history'; import * as containerUtils from './container-utils'; import { Messages } from '/@shared/src/messages/Messages'; import { telemetryLogger } from './extension'; -import { checkPrereqs } from './machine-utils'; +import { checkPrereqs, isLinux } from './machine-utils'; export class BootcApiImpl implements BootcApi { private history: History; @@ -240,6 +240,10 @@ export class BootcApiImpl implements BootcApi { telemetryLogger.logError(eventName, data); } + async isLinux(): Promise { + return isLinux(); + } + // The API does not allow callbacks through the RPC, so instead // we send "notify" messages to the frontend to trigger a refresh // this method is internal and meant to be used by the API implementation diff --git a/packages/backend/src/build-disk-image.spec.ts b/packages/backend/src/build-disk-image.spec.ts index d282a94a..98a422c6 100644 --- a/packages/backend/src/build-disk-image.spec.ts +++ b/packages/backend/src/build-disk-image.spec.ts @@ -21,7 +21,7 @@ import os from 'node:os'; import { buildExists, createBuilderImageOptions, - createPodmanRunCommand, + createPodmanCLIRunCommand, getBuilder, getUnusedName, } from './build-disk-image'; @@ -279,7 +279,7 @@ test('check uses Centos builder', async () => { expect(builder).toEqual(bootcImageBuilderCentos); }); -test('create podman run command', async () => { +test('create podman run CLI command', async () => { const name = 'test123-bootc-image-builder'; const build = { image: 'test-image', @@ -290,25 +290,35 @@ test('create podman run command', async () => { } as BootcBuildInfo; const options = createBuilderImageOptions(name, build); - const command = createPodmanRunCommand(options); - - const expectedCommand = `podman run \\ - --name test123-bootc-image-builder \\ - --tty \\ - --privileged \\ - --security-opt label=type:unconfined_t \\ - -v /Users/cdrage/bootc/qemutest4:/output/ \\ - -v /var/lib/containers/storage:/var/lib/containers/storage \\ - --label bootc.image.builder=true \\ - ${bootcImageBuilderCentos} \\ - test-image:latest \\ - --output \\ - /output/ \\ - --local \\ - --type \\ - raw \\ - --target-arch \\ - amd64`; + const command = createPodmanCLIRunCommand(options); + + // Expect an array of the above + const expectedCommand = [ + 'podman', + 'run', + '--rm', + '--name', + 'test123-bootc-image-builder', + '--tty', + '--privileged', + '--security-opt', + 'label=type:unconfined_t', + '-v', + '/Users/cdrage/bootc/qemutest4:/output/', + '-v', + '/var/lib/containers/storage:/var/lib/containers/storage', + '--label', + 'bootc.image.builder=true', + 'quay.io/centos-bootc/bootc-image-builder:latest-1720185748', + 'test-image:latest', + '--output', + '/output/', + '--local', + '--type', + 'raw', + '--target-arch', + 'amd64', + ]; expect(command).toEqual(expectedCommand); }); diff --git a/packages/backend/src/build-disk-image.ts b/packages/backend/src/build-disk-image.ts index b9bf5c47..97c16b09 100644 --- a/packages/backend/src/build-disk-image.ts +++ b/packages/backend/src/build-disk-image.ts @@ -140,7 +140,9 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov const buildImageContainer = createBuilderImageOptions(containerName, build, builder); logData += JSON.stringify(buildImageContainer, undefined, 2); logData += '\n----------\n'; - logData += createPodmanRunCommand(buildImageContainer); + // Output new line with `\` added at end for each in the array. + // logData += createPodmanCLIRunCommand(buildImageContainer); + logData += createPodmanCLIRunCommand(buildImageContainer).join(' \\\n'); logData += '\n----------\n'; try { await fs.promises.writeFile(logPath, logData); @@ -153,64 +155,121 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov return; } try { - // Step 1. Pull bootcImageBuilder - // Pull the bootcImageBuilder since that - // is what is being used to build images within BootC - // Do progress report here so it doesn't look like it's stuck - // since we are going to pull an image - progress.report({ increment: 4 }); - if (buildImageContainer.Image) { - await containerUtils.pullImage(buildImageContainer.Image); + /* LINUX BUILD SUPPORT INFORMATION + * Linux will use the CLI directly in order to build without having to use podman machine. + * The reasoning is that we require sudo / escalated privileges support in order for bootc-image-builder to work. + * In the below code, we transfer the current non-root image, to the 'sudo' root image directory, then ask for + * escalated privileges to run the build command. + * + * This is a short-term solution until we have either non-root building support in bootc-image-builder or an alternative + * solution to the problem. + */ + if (machineUtils.isLinux()) { + console.log( + 'Linux OS detected. Using Linux build support to build the image. This will include escalated privileges / asking for password.', + ); + + // Set as 'running' before we start the build. + build.status = 'running'; + await history.addOrUpdateBuildInfo(build); + + // Create random name for the image to be imported as. + // of the id of the image + a random number and .tar + // must create a random one each time to avoid conflicts when transfering. + const imagePath = path.join( + '/tmp', + `${build.imageId.replace('sha256:', '')}-${Math.floor(Math.random() * 100000)}.tar`, + ); + + // Step 1. Save the image to a tar file on the hosts /tmp/ directory. + console.log('Linux build support: Exporting image to: ', imagePath); + // Note: It is **VERY** important that we save it based upon the ID and NOT the name, or else it may + // use the image that is based upon a different OS (ex. amd64, vs arm64), or even the manifest "root" image, + // so instead we will save and transfer based on imageID. + // Trying the built-in save functionality of the PD API does not work correctly at the moment with saving image ID's. + const { + command: saveCommand, + stdout: saveStdout, + stderr: saveStderr, + } = await extensionApi.process.exec('podman', ['save', '-o', imagePath, build.imageId]); + console.log( + `Linux build support: Save command: ${saveCommand}\nstdout: ${saveStdout}\nstderr: ${saveStderr}`, + ); + // No 'safe' way to report information at the moment, so we will just increment by 50% as it's done + // in two steps anyways. We cannot get a callback of the progress of the exec command yet. + progress.report({ increment: 50 }); + + // Step 2. Run the command to import and build the image in one command. + const command = linuxBuildCommand(buildImageContainer, build, logPath, imagePath); + console.log('Linux build support: Running command: ', command); + const { + command: buildCommand, + stdout: buildStdout, + stderr: buildStderr, + } = await extensionApi.process.exec('sh', ['-c', `${command}`], { isAdmin: true }); + console.log( + `Linux build support: Build command: ${buildCommand}\nstdout: ${buildStdout}\nstderr: ${buildStderr}`, + ); } else { - throw new Error('No image to pull'); - } - - // Step 2. Check if there are any previous builds and remove them - progress.report({ increment: 5 }); - if (buildImageContainer.name) { - await containerUtils.removeContainerIfExists(build.engineId, buildImageContainer.name); - } else { - throw new Error('No container name to remove'); - } - - // Step 3. Create and start the container for the actual build - progress.report({ increment: 6 }); - build.status = 'running'; - await history.addOrUpdateBuildInfo(build); - const containerId = await containerUtils.createAndStartContainer(build.engineId, buildImageContainer); + // Step 1. Pull bootcImageBuilder + // Pull the bootcImageBuilder since that + // is what is being used to build images within BootC + // Do progress report here so it doesn't look like it's stuck + // since we are going to pull an image + progress.report({ increment: 4 }); + if (buildImageContainer.Image) { + await containerUtils.pullImage(buildImageContainer.Image); + } else { + throw new Error('No image to pull'); + } - // Update the history with the container id that was used to build the image - build.buildContainerId = containerId; - await history.addOrUpdateBuildInfo(build); + // Step 2. Check if there are any previous builds and remove them + progress.report({ increment: 5 }); + if (buildImageContainer.name) { + await containerUtils.removeContainerIfExists(build.engineId, buildImageContainer.name); + } else { + throw new Error('No container name to remove'); + } - // Step 3.1 Since we have started the container, we can now go get the logs - await logContainer(build.engineId, containerId, progress, data => { - // update the log file asyncronously - fs.promises.appendFile(logPath, data).catch((error: unknown) => { - console.debug('Could not write bootc build log: ', error); + // Step 3. Create and start the container for the actual build + progress.report({ increment: 6 }); + build.status = 'running'; + await history.addOrUpdateBuildInfo(build); + const containerId = await containerUtils.createAndStartContainer(build.engineId, buildImageContainer); + + // Update the history with the container id that was used to build the image + build.buildContainerId = containerId; + await history.addOrUpdateBuildInfo(build); + + // Step 3.1 Since we have started the container, we can now go get the logs + await logContainer(build.engineId, containerId, progress, data => { + // update the log file asyncronously + fs.promises.appendFile(logPath, data).catch((error: unknown) => { + console.debug('Could not write bootc build log: ', error); + }); }); - }); - - // Step 4. Wait for the container to exit - // This function will ensure it exits with a zero exit code - // if it does not, it will error out. - progress.report({ increment: 7 }); - - try { - await containerUtils.waitForContainerToExit(containerId); - } catch (error) { - // If we error out, BUT the container does not exist in the history, we will silently error - // as it's possible that the container was removed by the user during the build cycle / deleted from history. - - // Check if history has an entry with a containerId - const historyExists = history.getHistory().some(info => info.buildContainerId === containerId); - if (!historyExists) { - console.error( - `Container ${build.buildContainerId} for build ${build.image}:${build.arch} has errored out, but there is no container history. This is likely due to the container being removed intentionally during the build cycle. Ignore this. Error: ${error}`, - ); - return; - } else { - throw error; + + // Step 4. Wait for the container to exit + // This function will ensure it exits with a zero exit code + // if it does not, it will error out. + progress.report({ increment: 7 }); + + try { + await containerUtils.waitForContainerToExit(containerId); + } catch (error) { + // If we error out, BUT the container does not exist in the history, we will silently error + // as it's possible that the container was removed by the user during the build cycle / deleted from history. + + // Check if history has an entry with a containerId + const historyExists = history.getHistory().some(info => info.buildContainerId === containerId); + if (!historyExists) { + console.error( + `Container ${build.buildContainerId} for build ${build.image}:${build.arch} has errored out, but there is no container history. This is likely due to the container being removed intentionally during the build cycle. Ignore this. Error: ${error}`, + ); + return; + } else { + throw error; + } } } @@ -228,7 +287,9 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov // ########### // Regardless what happens, we will need to clean up what we started (if anything) // which could be containers, volumes, images, etc. - if (buildImageContainer.name) { + + // Only do this on mac or windows, as linux uses the CLI directly and with --rm so no need to remove container / volumes after. + if (buildImageContainer.name && !machineUtils.isLinux()) { await containerUtils.removeContainerAndVolumes(build.engineId, buildImageContainer.name); } } @@ -398,53 +459,88 @@ export function createBuilderImageOptions( return options; } -export function createPodmanRunCommand(options: ContainerCreateOptions): string { - let command = 'podman run \\'; +// Creates a command that will be used to build the image on Linux. This includes adding the transfer-to-root script as well as the actual build command. +// we also export to the log file during this process too. +export function linuxBuildCommand( + options: ContainerCreateOptions, + build: BootcBuildInfo, + logPath: string, + imagePath: string, +): string { + if (!options.name) { + throw new Error('Container name is required'); + } + + // Create the script that we will use to transfer the image to the root user + const transferToRoot = transferUserImageToRoot(imagePath, build.imageId, build.image, build.tag); + + // Create the CLI command that will be used to run the the actual build. + const run = createPodmanCLIRunCommand(options); + + // Combine the commands so that this will be ran in one individual sudo-prompt command. This is needed to avoid asking for credentials + // multiple times. + // We add >> ${logPath} 2>&1 to ensure that the output is written to the log file as we are not using the API for streaming the logs. + return `${transferToRoot} && ${run.join(' ')} >> ${logPath} 2>&1`; +} + +// Transfer the image from the 'normal' user to the root user. +// MUST be just the ID, as that is the only thing preserved (no name or tag) when importing +// after importing we must rename to the correct name and tag. +export function transferUserImageToRoot(path: string, imageId: string, imageName: string, imageTag: string): string { + // Remove the 'sha256:' from the imageId as that is not needed when importing. + imageId = imageId.replace('sha256:', ''); + + // This is the "recommended" way to transfer between root and non-root without confliction (prompting for overriding image, problems with transfer, etc.). + // We will cat the /tmp file to podman import and rename at the same time, this allows a seamless transition to the image being built with bootc-image-builder by + // just supplying the name and tag. + return `podman load --input ${path} && podman tag ${imageId} ${imageName}:${imageTag}`; +} + +// LINUX SUPPORT. +// this is itended to be ran with `--rm` as well to auto-remove after. +export function createPodmanCLIRunCommand(options: ContainerCreateOptions): string[] { + // --rm to make it temporary. + const command = ['podman', 'run', '--rm']; if (options.name) { - command += `\n --name ${options.name} \\`; + command.push('--name', options.name); } if (options.Tty) { - command += `\n --tty \\`; + command.push('--tty'); } if (options.HostConfig?.Privileged) { - command += `\n --privileged \\`; + command.push('--privileged'); } if (options.HostConfig?.SecurityOpt) { options.HostConfig.SecurityOpt.forEach(opt => { - command += `\n --security-opt ${opt} \\`; + command.push('--security-opt', opt); }); } if (options.HostConfig?.Binds) { options.HostConfig.Binds.forEach(bind => { - command += `\n -v ${bind} \\`; + command.push('-v', bind); }); } if (options.Labels) { for (const [key, value] of Object.entries(options.Labels)) { - command += `\n --label ${key}=${value} \\`; + command.push('--label', `${key}=${value}`); } } if (options.Image) { - command += `\n ${options.Image} \\`; + command.push(options.Image); } if (options.Cmd) { options.Cmd.forEach(cmd => { - command += `\n ${cmd} \\`; + command.push(cmd); }); } - // Remove the trailing backslash - if (command.endsWith(' \\')) { - command = command.slice(0, -2); - } - return command; } diff --git a/packages/backend/src/history.spec.ts b/packages/backend/src/history.spec.ts index 236049ce..422187f8 100644 --- a/packages/backend/src/history.spec.ts +++ b/packages/backend/src/history.spec.ts @@ -43,6 +43,7 @@ describe('History class tests', () => { await history.addOrUpdateBuildInfo({ id: 'exampleName', image: 'exampleImageName', + imageId: 'exampleImageId', tag: 'exampleTag', engineId: 'exampleEngineId', type: ['iso'], @@ -71,6 +72,7 @@ describe('History class tests', () => { await history.addOrUpdateBuildInfo({ id: 'name1', image: 'exampleName0', + imageId: 'exampleImageId0', tag: 'exampleTag0', engineId: 'exampleEngineId0', type: ['iso'], @@ -82,6 +84,7 @@ describe('History class tests', () => { await history.addOrUpdateBuildInfo({ id: 'name1', image: 'exampleName1', + imageId: 'exampleImageId1', tag: 'exampleTag1', engineId: 'exampleEngineId1', type: ['iso'], @@ -93,6 +96,7 @@ describe('History class tests', () => { await history.addOrUpdateBuildInfo({ id: 'name1', image: 'exampleName2', + imageId: 'exampleImageId2', tag: 'exampleTag2', engineId: 'exampleEngineId2', type: ['iso'], diff --git a/packages/backend/src/machine-utils.spec.ts b/packages/backend/src/machine-utils.spec.ts index 2ee961c1..40e41223 100644 --- a/packages/backend/src/machine-utils.spec.ts +++ b/packages/backend/src/machine-utils.spec.ts @@ -21,6 +21,7 @@ import * as extensionApi from '@podman-desktop/api'; import type { Configuration } from '@podman-desktop/api'; import * as machineUtils from './machine-utils'; import * as fs from 'node:fs'; +import * as os from 'node:os'; const config: Configuration = { get: () => { @@ -237,7 +238,9 @@ test('Fail if machine version is 4.0.0-dev for isPodmanV5Machine', async () => { await expect(machineUtils.isPodmanV5Machine()).resolves.toBe(false); }); -test('Fail prereq if not Podman v5', async () => { +test('Fail prereq if not Podman v5 (macos/windows)', async () => { + vi.mock('node:os'); + vi.spyOn(os, 'platform').mockImplementation(() => 'darwin'); const fakeMachineInfoJSON = { Version: { Version: '4.9.0', @@ -250,7 +253,9 @@ test('Fail prereq if not Podman v5', async () => { expect(await machineUtils.checkPrereqs()).toEqual('Podman v5.0 or higher is required to build disk images.'); }); -test('Fail prereq if not rootful', async () => { +test('Fail prereq if not rootful (macos/windows)', async () => { + vi.mock('node:os'); + vi.spyOn(os, 'platform').mockImplementation(() => 'darwin'); const fakeMachineInfoJSON = { Host: { CurrentMachine: '', @@ -275,7 +280,9 @@ test('Fail prereq if not rootful', async () => { ); }); -test('Pass prereq if rootful v5 machine', async () => { +test('Pass prereq if rootful v5 machine (macos/windows)', async () => { + vi.mock('node:os'); + vi.spyOn(os, 'platform').mockImplementation(() => 'darwin'); const fakeMachineInfoJSON = { Host: { CurrentMachine: '', @@ -300,3 +307,9 @@ test('Pass prereq if rootful v5 machine', async () => { expect(await machineUtils.checkPrereqs()).toEqual(undefined); }); + +test('Pass prereq (linux)', async () => { + vi.mock('node:os'); + vi.spyOn(os, 'platform').mockImplementation(() => 'linux'); + expect(await machineUtils.checkPrereqs()).toEqual(undefined); +}); diff --git a/packages/backend/src/machine-utils.ts b/packages/backend/src/machine-utils.ts index 979addb5..b8026a14 100644 --- a/packages/backend/src/machine-utils.ts +++ b/packages/backend/src/machine-utils.ts @@ -97,14 +97,17 @@ export async function isPodmanV5Machine() { } export async function checkPrereqs(): Promise { - const isPodmanV5 = await isPodmanV5Machine(); - if (!isPodmanV5) { - return 'Podman v5.0 or higher is required to build disk images.'; - } + // Podman Machine checks are applicable to non-Linux platforms only + if (!isLinux()) { + const isPodmanV5 = await isPodmanV5Machine(); + if (!isPodmanV5) { + return 'Podman v5.0 or higher is required to build disk images.'; + } - const isRootful = await isPodmanMachineRootful(); - if (!isRootful) { - return 'The podman machine is not set as rootful. Please recreate the podman machine with rootful privileges set and try again.'; + const isRootful = await isPodmanMachineRootful(); + if (!isRootful) { + return 'The podman machine is not set as rootful. Please recreate the podman machine with rootful privileges set and try again.'; + } } return undefined; } diff --git a/packages/frontend/src/Build.spec.ts b/packages/frontend/src/Build.spec.ts index d650b78d..388dd4f4 100644 --- a/packages/frontend/src/Build.spec.ts +++ b/packages/frontend/src/Build.spec.ts @@ -26,6 +26,7 @@ import { bootcClient } from './api/client'; const mockHistoryInfo: BootcBuildInfo[] = [ { id: 'name1', + imageId: 'sha256:image1', image: 'image1', engineId: 'engine1', tag: 'latest', @@ -36,6 +37,7 @@ const mockHistoryInfo: BootcBuildInfo[] = [ { id: 'name2', image: 'image2', + imageId: 'sha256:image', engineId: 'engine2', tag: 'latest', type: ['iso'], @@ -84,6 +86,8 @@ const mockImageInspect = { Architecture: 'amd64', } as unknown as ImageInspectInfo; +const mockIsLinux = false; + vi.mock('./api/client', async () => { return { bootcClient: { @@ -93,6 +97,7 @@ vi.mock('./api/client', async () => { listBootcImages: vi.fn(), inspectImage: vi.fn(), inspectManifest: vi.fn(), + isLinux: vi.fn().mockImplementation(() => mockIsLinux), }, rpcBrowser: { subscribe: () => { diff --git a/packages/frontend/src/Build.svelte b/packages/frontend/src/Build.svelte index b40abd0f..00d1b615 100644 --- a/packages/frontend/src/Build.svelte +++ b/packages/frontend/src/Build.svelte @@ -50,6 +50,7 @@ let errorFormValidation: string | undefined = undefined; // SPECIFICALLY fedora, where we **need** to select the filesystem, as it is not auto-selected. // this boolean will be set to true if the selected image is Fedora and shown as a warning to the user. let fedoraDetected = false; +let isLinux: boolean; // AWS Related let awsAmiName: string = ''; @@ -69,6 +70,7 @@ function findImage(repoTag: string): ImageInfo | undefined { } // Find images associated to the manifest +// optionally, filter by the archiecture. async function findImagesAssociatedToManifest(manifest: ManifestInspectInfo): Promise { const images = await bootcClient.listAllImages(); return images.filter(image => { @@ -179,9 +181,11 @@ async function buildBootcImage() { // The build options const image = findImage(selectedImage); + const buildOptions: BootcBuildInfo = { id: buildID, image: buildImageName, + imageId: image?.Id ?? '', tag: selectedImage.split(':')[1], engineId: image?.engineId ?? '', folder: buildFolder, @@ -194,6 +198,31 @@ async function buildBootcImage() { awsRegion: awsRegion, }; + // If manifest is detected, we will instead use the child image ID, as that is the correct one associated to the selection. This is needed + // for Linux support as we are transfering the image to the root podman connection and an ID is needed. + if (image?.isManifest) { + try { + const manifest = await bootcClient.inspectManifest(image); + const foundImages = await findImagesAssociatedToManifest(manifest); + + // Inspect each image and find the image that matches the buildArch + for (const foundImage of foundImages) { + const inspectedImage = await bootcClient.inspectImage(foundImage); + if (inspectedImage.Architecture === buildArch) { + buildOptions.imageId = foundImage.Id; + break; + } + } + + // If no matching architecture found, throw an error + if (!buildOptions.imageId) { + throw new Error(`No matching architecture found for ${buildArch}`); + } + } catch (error) { + console.error('Error inspecting manifest to retrieve image ID', error); + } + } + buildInProgress = true; try { // Do not await.. just start the build. @@ -251,6 +280,7 @@ function cleanup() { } onMount(async () => { + isLinux = await bootcClient.isLinux(); const images = await bootcClient.listBootcImages(); // filter to images that have a repo tag here, to avoid doing it everywhere @@ -710,6 +740,13 @@ export function goToHomePage(): void { {:else} + + {#if isLinux} +

+ For Linux users during the build, you will be asked for your credentials in order to run an escalated + privileged build prompt for the build process. +

+ {/if} {/if} {/if} diff --git a/packages/frontend/src/Homepage.spec.ts b/packages/frontend/src/Homepage.spec.ts index d28e624c..0ee88716 100644 --- a/packages/frontend/src/Homepage.spec.ts +++ b/packages/frontend/src/Homepage.spec.ts @@ -26,6 +26,7 @@ const mockHistoryInfo: BootcBuildInfo[] = [ { id: 'name1', image: 'image1', + imageId: 'sha256:imageId1', engineId: 'engine1', tag: 'latest', type: ['iso'], @@ -35,6 +36,7 @@ const mockHistoryInfo: BootcBuildInfo[] = [ { id: 'name2', image: 'image2', + imageId: 'sha256:imageId2', engineId: 'engine2', tag: 'latest', type: ['iso'], diff --git a/packages/frontend/src/lib/BootcActions.spec.ts b/packages/frontend/src/lib/BootcActions.spec.ts index 86951775..d26a6e71 100644 --- a/packages/frontend/src/lib/BootcActions.spec.ts +++ b/packages/frontend/src/lib/BootcActions.spec.ts @@ -39,6 +39,7 @@ vi.mock('../api/client', async () => { const mockHistoryInfo: BootcBuildInfo = { id: 'name1', image: 'image1', + imageId: 'sha256:imageId1', engineId: 'engine1', tag: 'latest', type: ['iso'], diff --git a/packages/frontend/src/lib/BootcColumnActions.spec.ts b/packages/frontend/src/lib/BootcColumnActions.spec.ts index 47b6fff2..724f26dd 100644 --- a/packages/frontend/src/lib/BootcColumnActions.spec.ts +++ b/packages/frontend/src/lib/BootcColumnActions.spec.ts @@ -23,6 +23,7 @@ import BootcColumnActions from './BootcColumnActions.svelte'; const mockHistoryInfo: BootcBuildInfo = { id: 'name1', image: 'image1', + imageId: 'sha256:imageId1', engineId: 'engine1', tag: 'latest', type: ['iso'], diff --git a/packages/frontend/src/lib/BootcFolderColumn.spec.ts b/packages/frontend/src/lib/BootcFolderColumn.spec.ts index 65ff1509..2a3284d8 100644 --- a/packages/frontend/src/lib/BootcFolderColumn.spec.ts +++ b/packages/frontend/src/lib/BootcFolderColumn.spec.ts @@ -23,6 +23,7 @@ import BootcFolderColumn from './BootcFolderColumn.svelte'; const mockHistoryInfo: BootcBuildInfo = { id: 'name1', image: 'image1', + imageId: 'sha256:imageId1', engineId: 'engine1', tag: 'latest', type: ['iso'], diff --git a/packages/frontend/src/lib/BootcImageColumn.spec.ts b/packages/frontend/src/lib/BootcImageColumn.spec.ts index 34198f76..811848f1 100644 --- a/packages/frontend/src/lib/BootcImageColumn.spec.ts +++ b/packages/frontend/src/lib/BootcImageColumn.spec.ts @@ -23,6 +23,7 @@ import BootcImageColumn from './BootcImageColumn.svelte'; const mockHistoryInfo: BootcBuildInfo = { id: 'name1', image: 'image1', + imageId: 'sha256:imageId1', engineId: 'engine1', tag: 'latest', type: ['iso'], diff --git a/packages/shared/src/BootcAPI.ts b/packages/shared/src/BootcAPI.ts index fc777b94..a873fea0 100644 --- a/packages/shared/src/BootcAPI.ts +++ b/packages/shared/src/BootcAPI.ts @@ -35,6 +35,7 @@ export abstract class BootcApi { abstract openFolder(folder: string): Promise; abstract generateUniqueBuildID(name: string): Promise; abstract openLink(link: string): Promise; + abstract isLinux(): Promise; abstract telemetryLogUsage(eventName: string, data?: Record | undefined): Promise; abstract telemetryLogError(eventName: string, data?: Record | undefined): Promise; } diff --git a/packages/shared/src/models/bootc.ts b/packages/shared/src/models/bootc.ts index 7181cd31..23e5800e 100644 --- a/packages/shared/src/models/bootc.ts +++ b/packages/shared/src/models/bootc.ts @@ -21,6 +21,7 @@ export type BuildType = 'qcow2' | 'ami' | 'raw' | 'vmdk' | 'iso'; export interface BootcBuildInfo { id: string; image: string; + imageId: string; tag: string; engineId: string; type: BuildType[];