diff --git a/packages/backend/src/build-disk-image.ts b/packages/backend/src/build-disk-image.ts index d0c1d979..0afe68b2 100644 --- a/packages/backend/src/build-disk-image.ts +++ b/packages/backend/src/build-disk-image.ts @@ -23,7 +23,7 @@ import path, { resolve } from 'node:path'; import os from 'node:os'; import * as containerUtils from './container-utils'; import { bootcImageBuilder, bootcImageBuilderCentos, bootcImageBuilderRHEL } from './constants'; -import type { BootcBuildInfo, BuildType } from '/@shared/src/models/bootc'; +import type { BootcBuildInfo, BuildConfig, BuildType } from '/@shared/src/models/bootc'; import type { History } from './history'; import * as machineUtils from './machine-utils'; import { getConfigurationValue, telemetryLogger } from './extension'; @@ -465,6 +465,27 @@ export function createBuilderImageOptions( } } + // Check if build.buildConfig has ANYTHING defined, make sure it is not empty. + if (build.buildConfig) { + const buildConfig = createBuildConfigJSON(build.buildConfig); + + // Make sure that cutomizations is exists and is not empty before adding it to the container. + if (buildConfig.customizations && Object.keys(buildConfig.customizations).length > 0) { + // Create a temporary path to store the buildConfig JSON + // with a temporary name + // eslint-disable-next-line sonarjs/pseudo-random + const buildConfigPath = path.join(os.tmpdir(), `${Math.floor(Math.random() * 100000)}.json`); + + // Write the buildConfig JSON to the temporary file with JSON + fs.writeFileSync(buildConfigPath, JSON.stringify(buildConfig, undefined, 2)); + + // Add the mount to the configuration file + if (options.HostConfig?.Binds) { + options.HostConfig.Binds.push(buildConfigPath + ':/config.json:ro'); + } + } + } + // If there is the chown in build, add the --chown flag to the command with the value in chown if (build.chown) { cmd.push('--chown', build.chown); @@ -473,6 +494,45 @@ export function createBuilderImageOptions( return options; } +// Function that takes in BuildConfig and creates a JSON object out of the contents. +// for example: +/* +{ + "customizations": { + "user": [ + { + "name": "alice", + "password": "bob", + "key": "ssh-rsa AAA ... user@email.com", + "groups": [ + "wheel", + "admins" + ] + } + ] + } +} +*/ +// We will then return it as "cutomizations" which is required by bootc-image-builder +export function createBuildConfigJSON(buildConfig: BuildConfig): Record { + const config: Record = {}; + + console.log('This is buldconfig to parse', buildConfig); + if (buildConfig.user && buildConfig.user.length > 0) { + config.user = buildConfig.user; + } + + if (buildConfig.filesystem && buildConfig.filesystem.length > 0) { + config.filesystem = buildConfig.filesystem; + } + + if (buildConfig.kernel?.append) { + config.kernel = buildConfig.kernel; + } + + return { customizations: config }; +} + // 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( diff --git a/packages/frontend/src/Build.svelte b/packages/frontend/src/Build.svelte index 0045659b..fa19bcf5 100644 --- a/packages/frontend/src/Build.svelte +++ b/packages/frontend/src/Build.svelte @@ -6,9 +6,11 @@ import { faCube, faQuestionCircle, faTriangleExclamation, + faMinusCircle, + faPlusCircle, } from '@fortawesome/free-solid-svg-icons'; import { bootcClient } from './api/client'; -import type { BootcBuildInfo, BuildType } from '/@shared/src/models/bootc'; +import type { BootcBuildInfo, BuildType, BuildConfig } from '/@shared/src/models/bootc'; import Fa from 'svelte-fa'; import { onMount } from 'svelte'; import type { ImageInfo, ManifestInspectInfo } from '@podman-desktop/api'; @@ -60,12 +62,25 @@ let awsAmiName: string = ''; let awsBucket: string = ''; let awsRegion: string = ''; +// Build Config related, we only support one entry for now +let buildConfigUsers: { name: string; password: string; key: string; groups: string }[] = [ + { name: '', password: '', key: '', groups: '' }, +]; +let buildConfigFilesystems: { mountpoint: string; minsize: string }[] = [{ mountpoint: '', minsize: '' }]; +let buildConfigKernelArguments: string; + // Show/hide advanced options let showAdvanced = false; // State to show/hide advanced options function toggleAdvanced() { showAdvanced = !showAdvanced; } +// Show/hide build config options +let showBuildConfig = false; +function toggleBuildConfig() { + showBuildConfig = !showBuildConfig; +} + function findImage(repoTag: string): ImageInfo | undefined { return bootcAvailableImages.find( image => image.RepoTags && image.RepoTags.length > 0 && image.RepoTags[0] === repoTag, @@ -203,6 +218,33 @@ async function buildBootcImage() { // The build options const image = findImage(selectedImage); + // Per bootc-image-builder spec, users with empty names are not valid + if (buildConfigUsers) { + buildConfigUsers = buildConfigUsers.filter(user => user.name); + } + + // Per bootc-image-builder spec, filesystems with empty mountmounts are not valid + if (buildConfigFilesystems) { + buildConfigFilesystems = buildConfigFilesystems.filter(filesystem => filesystem.mountpoint); + } + + // In the UI we accept comma deliminated, however the spec must require an array, so we convert any users.groups to an array. + let convertedBuildConfigUsers = buildConfigUsers.map(user => { + return { + ...user, + groups: user.groups.split(',').map(group => group.trim()), + }; + }); + + // Final objec, we also remove any empty string values from the object. + const buildConfig = removeEmptyStrings({ + user: convertedBuildConfigUsers, + filesystem: buildConfigFilesystems, + kernel: { + append: buildConfigKernelArguments, + }, + }) as BuildConfig; + const buildOptions: BootcBuildInfo = { id: buildID, image: buildImageName, @@ -210,6 +252,8 @@ async function buildBootcImage() { tag: selectedImage.split(':')[1], engineId: image?.engineId ?? '', folder: buildFolder, + // If all the entries are empty, we will not provide the buildConfig + buildConfig, buildConfigFilePath: buildConfigFile, type: buildType, arch: buildArch, @@ -297,6 +341,39 @@ function cleanup() { errorFormValidation = ''; } +function addUser() { + buildConfigUsers = [...buildConfigUsers, { name: '', password: '', key: '', groups: '' }]; +} + +function deleteUser(index: number) { + buildConfigUsers = buildConfigUsers.filter((_, i) => i !== index); +} + +function addFilesystem() { + buildConfigFilesystems = [...buildConfigFilesystems, { mountpoint: '', minsize: '' }]; +} + +function deleteFilesystem(index: number) { + buildConfigFilesystems = buildConfigFilesystems.filter((_, i) => i !== index); +} + +// Remove any empty strings in the object before passing it in to the backend +// this is useful as we are using "bind:input" with groups / form fields and the first entry will always be blank when submitting +// this will remove any empty strings from the object before passing it in. +function removeEmptyStrings(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(removeEmptyStrings); // Recurse for each item in arrays + } else if (typeof obj === 'object' && obj !== null) { + return Object.entries(obj) + .filter(([_, value]) => value !== '' && value !== undefined) // Filter out entries with empty string or undefined values + .reduce((acc, [key, value]) => { + acc[key] = removeEmptyStrings(value); // Recurse for nested objects/arrays + return acc; + }, {} as any); + } + return obj; +} + onMount(async () => { isLinux = await bootcClient.isLinux(); const images = await bootcClient.listBootcImages(); @@ -684,14 +761,115 @@ $: if (availableArchitectures) { Advanced Options + aria-label="build-config-options" + on:click={toggleBuildConfig} + >Build config - {#if showAdvanced} + {#if showBuildConfig} +

+ Supplying the following fields will create a build config file that contains the build options for the + disk image. Customizations include user, password, SSH keys and kickstart files. More information can + be found in the bootc-image-builder documentation. +

+ +
+ Users +
+ + {#each buildConfigUsers as user, index} +
+ + + + + + + + + +
+ {/each} + +
+ Filesystems +
+ + {#each buildConfigFilesystems as filesystem, index} +
+ + + + +
+ {/each} + +
+ Kernel +
+ + + +
+ File +
+
-
getBuildConfigFile()}>Browse...

- The build configuration file is a TOML or JSON file that contains the build options for the disk - image. Customizations include user, password, SSH keys and kickstart files. More information can be - found in the bootc-image-builder documentation. + This will override any above user-specific input and use the supplied file only.

- + {/if} + +
+ + + + Advanced options + + {#if showAdvanced} {#if isLinux}
diff --git a/packages/shared/src/models/bootc.ts b/packages/shared/src/models/bootc.ts index 55b3fb24..afddf53d 100644 --- a/packages/shared/src/models/bootc.ts +++ b/packages/shared/src/models/bootc.ts @@ -18,6 +18,35 @@ export type BuildType = 'qcow2' | 'ami' | 'raw' | 'vmdk' | 'anaconda-iso' | 'vhd'; +// Follows https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#-build-config convention +// users = array +// filesystems = array +// kernel = mapping +export interface BuildConfig { + user?: BuildConfigUser[]; + filesystem?: BuildConfigFilesystem[]; + kernel?: BuildConfigKernel; + // In the future: + // * Add installer.kickstart https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#anaconda-iso-installer-options-installer-mapping + // * Add anaconda iso modules https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#anaconda-iso-installer-modules +} + +export interface BuildConfigUser { + name: string; + password?: string; + key?: string; + groups?: string[]; +} + +export interface BuildConfigFilesystem { + mountpoint: string; + minsize: string; +} + +export interface BuildConfigKernel { + append: string; +} + export interface BootcBuildInfo { id: string; image: string; @@ -27,6 +56,7 @@ export interface BootcBuildInfo { type: BuildType[]; folder: string; chown?: string; + buildConfig?: BuildConfig; buildConfigFilePath?: string; filesystem?: string; arch?: string;