Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
cdrage committed Nov 12, 2024
1 parent e6b1efb commit 47d059f
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 14 deletions.
62 changes: 61 additions & 1 deletion packages/backend/src/build-disk-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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 ... [email protected]",
"groups": [
"wheel",
"admins"
]
}
]
}
}
*/
// We will then return it as "cutomizations" which is required by bootc-image-builder
export function createBuildConfigJSON(buildConfig: BuildConfig): Record<string, unknown> {
const config: Record<string, unknown> = {};

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(
Expand Down
211 changes: 198 additions & 13 deletions packages/frontend/src/Build.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -203,13 +218,42 @@ 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,
imageId: image?.Id ?? '',
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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -684,14 +761,115 @@ $: if (availableArchitectures) {
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="font-semibold mb-2 block cursor-pointer"
aria-label="advanced-options"
on:click={toggleAdvanced}
><Fa icon={showAdvanced ? faCaretDown : faCaretRight} class="inline-block mr-1" />Advanced Options
aria-label="build-config-options"
on:click={toggleBuildConfig}
><Fa icon={showBuildConfig ? faCaretDown : faCaretRight} class="inline-block mr-1" />Build config
</span>
{#if showAdvanced}
{#if showBuildConfig}
<p class="text-sm text-[var(--pd-content-text)] mb-2">
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 <Link
externalRef="https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#-build-config"
>bootc-image-builder documentation</Link
>.
</p>

<div>
<span class="block mt-4">Users</span>
</div>

{#each buildConfigUsers as user, index}
<div class="flex flex-row justify-center items-center w-full py-1">
<Input
bind:value={user.name}
id="buildConfigName.${index}"
placeholder="Username"
aria-label="buildConfigName.${index}"
class="mr-2" />

<Input
bind:value={user.password}
id="buildConfigPassword.${index}"
placeholder="Password"
aria-label="buildConfigPassword.${index}"
class="mr-2" />

<Input
bind:value={user.key}
id="buildConfigKey.${index}"
placeholder="SSH key"
aria-label="buildConfigKey.${index}"
class="mr-2" />

<!-- On input, comma deliminate, as user.groups is string[] -->
<Input
bind:value={user.groups}
id="buildConfigGroups.${index}"
placeholder="Groups (comma deliminated)"
aria-label="buildConfigGroups.${index}" />

<Button
type="link"
hidden={index === buildConfigUsers.length - 1}
on:click={() => deleteUser(index)}
icon={faMinusCircle} />
<Button
type="link"
hidden={index < buildConfigUsers.length - 1}
on:click={addUser}
icon={faPlusCircle} />
</div>
{/each}

<div>
<span class="block mt-6">Filesystems</span>
</div>

{#each buildConfigFilesystems as filesystem, index}
<div class="flex flex-row justify-center items-center w-full py-1">
<Input
bind:value={filesystem.mountpoint}
id="buildConfigFilesystemMountpoint.${index}"
placeholder="Mountpoint (ex. /mnt)"
class="mr-2" />

<Input
bind:value={filesystem.minsize}
id="buildConfigFilesystemMinimumSize.${index}"
placeholder="Minimum size (ex. 30 GiB)" />

<Button
type="link"
hidden={index === buildConfigFilesystems.length - 1}
on:click={() => deleteFilesystem(index)}
icon={faMinusCircle} />

<Button
type="link"
hidden={index < buildConfigFilesystems.length - 1}
on:click={addFilesystem}
icon={faPlusCircle} />
</div>
{/each}

<div>
<span class="block mt-6">Kernel</span>
</div>

<Input
bind:value={buildConfigKernelArguments}
name="buildConfigKernelArguments"
id="buildConfigKernelArguments"
placeholder="Kernel arguments (ex. quiet)"
class="w-full" />

<div>
<span class="block mt-6">File</span>
</div>

<!-- Build config -->
<div class="mb-2">
<label for="buildconfig" class="block mb-2 font-semibold">Build config</label>
<div class="flex flex-row space-x-3">
<Input
name="buildconfig"
Expand All @@ -703,15 +881,22 @@ $: if (availableArchitectures) {
<Button on:click={() => getBuildConfigFile()}>Browse...</Button>
</div>
<p class="text-sm text-[var(--pd-content-text)] pt-2">
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 <Link
externalRef="https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#-build-config"
>bootc-image-builder documentation</Link
>.
This will override any above user-specific input and use the supplied file only.
</p>
</div>

{/if}
</div>
<div class="mb-2">
<!-- Use a span for this until we have a "dropdown toggle" UI element implemented. -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="font-semibold mb-2 block cursor-pointer"
aria-label="advanced-options"
on:click={toggleAdvanced}
><Fa icon={showAdvanced ? faCaretDown : faCaretRight} class="inline-block mr-1" />Advanced options
</span>
{#if showAdvanced}
<!-- chown, this option is only available for Linux users -->
{#if isLinux}
<div class="mb-2">
Expand Down
30 changes: 30 additions & 0 deletions packages/shared/src/models/bootc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +56,7 @@ export interface BootcBuildInfo {
type: BuildType[];
folder: string;
chown?: string;
buildConfig?: BuildConfig;
buildConfigFilePath?: string;
filesystem?: string;
arch?: string;
Expand Down

0 comments on commit 47d059f

Please sign in to comment.