From d13276eeeea2d13b255a818b68d4060c44605ca9 Mon Sep 17 00:00:00 2001 From: Sanne Raymaekers Date: Tue, 21 Jan 2025 12:11:02 +0100 Subject: [PATCH] store/cockpitApi: support building images from blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The structure of the local cache is now: ``` └── blueprint └── blueprint.json └── image1 └── image2 └── blueprint2 └── blueprint2.json └── image1 ``` Building an image reads the blueprint, and creates a new image file under the relevant blueprint folder, which contains the image request. The image request that's sent off to composer and the request that's saved differs slightly in the upload structures. --- .../helpers/onPremToHostedBlueprintMapper.tsx | 123 +++++++++++++++++- src/store/backendApi.ts | 4 + src/store/cockpitApi.ts | 91 ++++++++++++- 3 files changed, 209 insertions(+), 9 deletions(-) diff --git a/src/Components/Blueprints/helpers/onPremToHostedBlueprintMapper.tsx b/src/Components/Blueprints/helpers/onPremToHostedBlueprintMapper.tsx index 993c0a50..aa3ab462 100644 --- a/src/Components/Blueprints/helpers/onPremToHostedBlueprintMapper.tsx +++ b/src/Components/Blueprints/helpers/onPremToHostedBlueprintMapper.tsx @@ -1,6 +1,7 @@ import { BlueprintExportResponse, Container, + CreateBlueprintRequest, Directory, Distributions, Fdo, @@ -20,7 +21,7 @@ export type BlueprintOnPrem = { description?: string; packages?: PackagesOnPrem[]; groups?: GroupsPackagesOnPrem[]; - distro: Distributions; + distro?: Distributions; customizations?: CustomizationsOnPrem; containers?: Container[]; }; @@ -85,7 +86,7 @@ export type UserOnPrem = { export type GroupOnPrem = { name: string; - gid: number; + gid: number | undefined; }; export type SshKeyOnPrem = { @@ -115,7 +116,7 @@ export const mapOnPremToHosted = ( return { name: blueprint.name, description: blueprint.description || '', - distribution: blueprint.distro, + distribution: blueprint.distro!, customizations: { ...blueprint.customizations, containers: blueprint.containers, @@ -176,3 +177,119 @@ export const mapOnPremToHosted = ( }, }; }; + +export const mapHostedToOnPrem = ( + blueprint: CreateBlueprintRequest +): BlueprintOnPrem => { + const result: BlueprintOnPrem = { + name: blueprint.name, + customizations: {}, + }; + + if (blueprint.customizations?.packages) { + result.packages = blueprint.customizations.packages.map((pkg) => { + return { + name: pkg, + version: '*', + }; + }); + } + + if (blueprint.customizations?.containers) { + result.containers = blueprint.customizations.containers; + } + + if (blueprint.customizations?.directories) { + result.customizations!.directories = blueprint.customizations.directories; + } + + if (blueprint.customizations?.files) { + result.customizations!.files = blueprint.customizations.files; + } + + if (blueprint.customizations?.openscap) { + result.customizations!.openscap = blueprint.customizations.openscap; + } + + if (blueprint.customizations?.filesystem) { + result.customizations!.filesystem = blueprint.customizations.filesystem.map( + (fs) => { + return { + mountpoint: fs.mountpoint, + minsize: fs.min_size, + }; + } + ); + } + + if (blueprint.customizations?.users) { + result.customizations!.user = blueprint.customizations.users.map((u) => { + return { + name: u.name, + key: u.ssh_key || '', + }; + }); + } + + if (blueprint.customizations?.services) { + result.customizations!.services = blueprint.customizations.services; + } + + if (blueprint.customizations?.hostname) { + result.customizations!.hostname = blueprint.customizations.hostname; + } + + if (blueprint.customizations?.kernel) { + result.customizations!.kernel = blueprint.customizations.kernel; + } + + if (blueprint.customizations?.groups) { + result.customizations!.groups = blueprint.customizations.groups.map((g) => { + return { + name: g.name, + gid: g.gid, + }; + }); + } + + if (blueprint.customizations?.timezone) { + result.customizations!.timezone = blueprint.customizations.timezone; + } + + if (blueprint.customizations?.locale) { + result.customizations!.locale = blueprint.customizations.locale; + } + + if (blueprint.customizations?.firewall) { + result.customizations!.firewall = blueprint.customizations.firewall; + } + + if (blueprint.customizations?.installation_device) { + result.customizations!.installation_device = + blueprint.customizations.installation_device; + } + + if (blueprint.customizations?.fdo) { + result.customizations!.fdo = blueprint.customizations.fdo; + } + + if (blueprint.customizations?.ignition) { + result.customizations!.ignition = blueprint.customizations.ignition; + } + + if (blueprint.customizations?.partitioning_mode) { + result.customizations!.partitioning_mode = + blueprint.customizations.partitioning_mode; + } + + if (blueprint.customizations?.fips) { + result.customizations!.fips = + blueprint.customizations.fips?.enabled || false; + } + + if (blueprint.customizations?.installer) { + result.customizations!.installer = blueprint.customizations.installer; + } + + return result; +}; diff --git a/src/store/backendApi.ts b/src/store/backendApi.ts index aae3f9f8..e2103080 100644 --- a/src/store/backendApi.ts +++ b/src/store/backendApi.ts @@ -36,6 +36,10 @@ export const useListSnapshotsByDateMutation = process.env.IS_ON_PREMISE ? cockpitQueries.useListSnapshotsByDateMutation : useContentSourcesListSnapshotsByDateMutation; +export const useComposeBlueprintMutation = process.env.IS_ON_PREMISE + ? cockpitQueries.useComposeBlueprintMutation + : imageBuilderQueries.useComposeBlueprintMutation; + export const useBackendPrefetch = process.env.IS_ON_PREMISE ? cockpitApi.usePrefetch : imageBuilderApi.usePrefetch; diff --git a/src/store/cockpitApi.ts b/src/store/cockpitApi.ts index ede0ba04..35c7db61 100644 --- a/src/store/cockpitApi.ts +++ b/src/store/cockpitApi.ts @@ -8,7 +8,6 @@ import path from 'path'; // We also needed to create an alias in vitest to make this work. import cockpit from 'cockpit'; import { fsinfo } from 'cockpit/fsinfo'; -import toml from 'toml'; import { ListSnapshotsByDateApiArg, @@ -16,6 +15,9 @@ import { } from './contentSourcesApi'; import { emptyCockpitApi } from './emptyCockpitApi'; import { + ComposeBlueprintApiResponse, + ComposeBlueprintApiArg, + CreateBlueprintRequest, GetArchitecturesApiResponse, GetArchitecturesApiArg, GetBlueprintsApiArg, @@ -29,9 +31,10 @@ import { GetBlueprintApiArg, CreateBlueprintApiResponse, CreateBlueprintApiArg, + ComposeResponse, } from './imageBuilderApi'; -import { mapOnPremToHosted } from '../Components/Blueprints/helpers/onPremToHostedBlueprintMapper'; +import { mapHostedToOnPrem } from '../Components/Blueprints/helpers/onPremToHostedBlueprintMapper'; import { BLUEPRINTS_DIR } from '../constants'; const getBlueprintsPath = async () => { @@ -125,16 +128,17 @@ export const cockpitApi = emptyCockpitApi.injectEndpoints({ const entries = Object.entries(info?.entries || {}); let blueprints: BlueprintItem[] = await Promise.all( entries.map(async ([filename]) => { - const file = cockpit.file(path.join(blueprintsDir, filename)); + const file = cockpit.file( + path.join(blueprintsDir, filename, `${filename}.json`) + ); const contents = await file.read(); - const parsed = toml.parse(contents); + const parsed = JSON.parse(contents); file.close(); - const blueprint = mapOnPremToHosted(parsed); const version = (parsed.version as number) ?? 1; return { - ...blueprint, + ...parsed, id: filename as string, version: version, last_modified_at: Date.now().toString(), @@ -249,6 +253,80 @@ export const cockpitApi = emptyCockpitApi.injectEndpoints({ }, }), }), + composeBlueprint: builder.mutation< + ComposeBlueprintApiResponse, + ComposeBlueprintApiArg + >({ + queryFn: async ({ id: filename }) => { + try { + const blueprintsDir = await getBlueprintsPath(); + const file = cockpit.file( + path.join(blueprintsDir, filename, `${filename}.json`) + ); + const contents = await file.read(); + const parsed = JSON.parse(contents); + + const cloudapi = cockpit.http('/run/cloudapi/api.socket', { + superuser: 'try', + }); + + const createBPReq = parsed as CreateBlueprintRequest; + const blueprint = mapHostedToOnPrem(createBPReq); + const composes: ComposeResponse[] = []; + for (const ir of parsed.image_requests) { + const composeReq = { + distribution: createBPReq.distribution, + blueprint: blueprint, + image_requests: [ + { + architecture: ir.architecture, + image_type: ir.image_type, + repositories: [], + upload_targets: [ + { + type: 'local', + upload_options: {}, + }, + ], + }, + ], + }; + const saveReq = { + distribution: createBPReq.distribution, + blueprint: parsed, + image_requests: [ + { + architecture: ir.architecture, + image_type: ir.image_type, + repositories: [], + upload_request: { + type: 'local', + options: {}, + }, + }, + ], + }; + const resp = await cloudapi.post( + '/api/image-builder-composer/v2/compose', + composeReq, + { + 'content-type': 'application/json', + } + ); + const composeResp = JSON.parse(resp); + await cockpit + .file(path.join(blueprintsDir, filename, composeResp.id)) + .replace(JSON.stringify(saveReq)); + composes.push({ id: composeResp.id }); + } + return { + data: composes, + }; + } catch (error) { + return { error }; + } + }, + }), }; }, }); @@ -262,4 +340,5 @@ export const { useDeleteBlueprintMutation, useGetOscapProfilesQuery, useListSnapshotsByDateMutation, + useComposeBlueprintMutation, } = cockpitApi;