Skip to content

Commit bdf7414

Browse files
store/cockpit: long running ibcli as a cockpit systemd service
Use cockpit's `long-running-process` to launch a `one-shot` systemd service that runs the image-build with the image-builder cli. This is a step towards removing our dependency on `osbuild-composer`. Results are saved to a file and are updated with the lifecycle events from the systemd service.
1 parent 03e901c commit bdf7414

File tree

16 files changed

+302
-49
lines changed

16 files changed

+302
-49
lines changed

cockpit/cockpit-image-builder.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ BuildRequires: nodejs
1919
Requires: cockpit
2020
Requires: cockpit-files
2121
Requires: osbuild-composer >= 131
22+
Requires: image-builder >= 33
2223

2324
%description
2425
The image-builder-frontend generates custom images suitable for

fec.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ module.exports = {
6868
// to false
6969
cockpit: false,
7070
'cockpit/fsinfo': false,
71+
'long-running-process': false,
7172
'os-release': false,
7273
},
7374
},

playwright/test.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { readFileSync } from 'node:fs';
1+
import { readFileSync, writeFileSync } from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
24

35
import TOML from '@ltd/j-toml';
46
import { expect, test } from '@playwright/test';
@@ -8,6 +10,12 @@ import { closePopupsIfExist, isHosted } from './helpers/helpers';
810
import { ensureAuthenticated } from './helpers/login';
911
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
1012

13+
const awsCredentials = `
14+
[default]
15+
aws_access_key_id = supersecret
16+
aws_secret_access_key = secretsquirrel
17+
`;
18+
1119
test.describe.serial('test', () => {
1220
const blueprintName = uuidv4();
1321
test('create blueprint', async ({ page }) => {
@@ -266,6 +274,13 @@ test.describe.serial('test', () => {
266274
return;
267275
}
268276

277+
// this needs to be set so ibcli will list aws targets
278+
// as an available image type
279+
writeFileSync(
280+
path.join(os.homedir(), '.aws', 'credentials'),
281+
awsCredentials,
282+
);
283+
269284
await ensureAuthenticated(page);
270285
await closePopupsIfExist(page);
271286
// Navigate to IB landing page and get the frame

schutzbot/Containerfile-Playwright

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM ubuntu:latest AS builder
2+
RUN apt update
3+
RUN apt install -y git golang libgpgme-dev libbtrfs-dev libdevmapper-dev podman
4+
RUN git clone https://github.com/osbuild/image-builder-cli.git ibcli
5+
WORKDIR ibcli
6+
RUN ls -al
7+
RUN go mod tidy && go mod vendor
8+
RUN go build -o /opt ./cmd/image-builder/
9+
10+
FROM mcr.microsoft.com/playwright:v1.51.1-noble
11+
RUN apt update
12+
# Needed for running image-builder
13+
RUN apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev
14+
COPY --from=builder /opt /usr/bin

schutzbot/playwright_tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ sudo useradd admin -p "$(openssl passwd foobar)"
2121
sudo usermod -aG wheel admin
2222
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
2323

24+
sudo podman build --tag playwright -f $(pwd)/schutzbot/Containerfile-Playwright .
25+
2426
function upload_artifacts {
2527
if [ -n "${TMT_TEST_DATA:-}" ]; then
2628
mv playwright-report "$TMT_TEST_DATA"/playwright-report
@@ -88,5 +90,5 @@ sudo podman run \
8890
--privileged \
8991
--rm \
9092
--init \
91-
mcr.microsoft.com/playwright:v1.51.1-noble \
93+
localhost/playwright \
9294
/bin/sh -c "cd tests && npx -y [email protected] test --workers=${PW_WORKERS}"

src/AppCockpit.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,35 @@
11
import '@patternfly/react-core/dist/styles/base.css';
22
import '@patternfly/patternfly/patternfly-addons.css';
33

4-
import React from 'react';
4+
import React, { useEffect, useState } from 'react';
55

66
import 'cockpit-dark-theme';
77
import { Page, PageSection } from '@patternfly/react-core';
8+
import { Spinner } from '@redhat-cloud-services/frontend-components';
89
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
10+
import cockpit from 'cockpit';
911
import { createRoot } from 'react-dom/client';
1012
import { Provider } from 'react-redux';
1113
import { HashRouter } from 'react-router-dom';
1214

1315
import './AppCockpit.scss';
14-
import { NotReady, RequireAdmin } from './Components/Cockpit';
16+
import { RequireAdmin } from './Components/Cockpit';
1517
import { Router } from './Router';
1618
import { onPremStore as store } from './store';
17-
import { useGetComposerSocketStatus } from './Utilities/useComposerStatus';
1819
import { useIsCockpitAdmin } from './Utilities/useIsCockpitAdmin';
1920

2021
const Application = () => {
21-
const { enabled, started } = useGetComposerSocketStatus();
2222
const isAdmin = useIsCockpitAdmin();
23+
const [ready, setReady] = useState(false);
2324

24-
if (!started || !enabled) {
25-
return <NotReady enabled={enabled} />;
25+
useEffect(() => {
26+
if (cockpit) {
27+
setReady(true);
28+
}
29+
}, [cockpit, ready, setReady]);
30+
31+
if (!ready) {
32+
return <Spinner centered />;
2633
}
2734

2835
if (!isAdmin) {

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const CENTOS_9 = 'centos-9';
5959
export const CENTOS_10 = 'centos-10';
6060
export const FEDORA_41 = 'fedora-41';
6161
export const FEDORA_42 = 'fedora-42';
62+
export const FEDORA_43 = 'fedora-43';
6263
export const X86_64 = 'x86_64';
6364
export const AARCH64 = 'aarch64';
6465

@@ -97,6 +98,7 @@ export const ON_PREM_RELEASES = new Map([
9798
[CENTOS_10, 'CentOS Stream 10'],
9899
[FEDORA_41, 'Fedora Linux 41'],
99100
[FEDORA_42, 'Fedora Linux 42'],
101+
[FEDORA_43, 'Fedora Linux 43'],
100102
[RHEL_10, 'Red Hat Enterprise Linux (RHEL) 10'],
101103
]);
102104

@@ -310,3 +312,4 @@ export const PAGINATION_COUNT = 0;
310312
export const SEARCH_INPUT = '';
311313

312314
export const BLUEPRINTS_DIR = '.cache/cockpit-image-builder/';
315+
export const ARTIFACTS_DIR = '/var/lib/osbuild-composer/artifacts';

src/store/cockpit/cockpitApi.ts

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import path from 'path';
99
import TOML, { Section } from '@ltd/j-toml';
1010
import cockpit from 'cockpit';
1111
import { fsinfo } from 'cockpit/fsinfo';
12+
import { LongRunningProcess } from 'long-running-process';
1213
import { v4 as uuidv4 } from 'uuid';
1314

1415
// We have to work around RTK query here, since it doesn't like splitting
@@ -19,17 +20,21 @@ import { v4 as uuidv4 } from 'uuid';
1920
// bit so that the `cockpitApi` doesn't become a monolith.
2021
import { contentSourcesApi } from './contentSourcesApi';
2122
import {
23+
ComposeStatus,
24+
composeStatus,
2225
datastreamDistroLookup,
2326
getBlueprintsPath,
2427
getCloudConfigs,
25-
mapToOnpremRequest,
2628
paginate,
2729
readComposes,
30+
updateComposeStatus,
2831
} from './helpers';
2932
import type {
3033
CockpitCreateBlueprintApiArg,
3134
CockpitCreateBlueprintRequest,
3235
CockpitUpdateBlueprintApiArg,
36+
GetCockpitComposeStatusApiResponse,
37+
ImageStatus,
3338
UpdateWorkerConfigApiArg,
3439
WorkerConfigFile,
3540
WorkerConfigResponse,
@@ -39,6 +44,7 @@ import {
3944
mapHostedToOnPrem,
4045
mapOnPremToHosted,
4146
} from '../../Components/Blueprints/helpers/onPremToHostedBlueprintMapper';
47+
import { ARTIFACTS_DIR } from '../../constants';
4248
import {
4349
BlueprintItem,
4450
ComposeBlueprintApiArg,
@@ -61,7 +67,6 @@ import {
6167
GetComposesApiArg,
6268
GetComposesApiResponse,
6369
GetComposeStatusApiArg,
64-
GetComposeStatusApiResponse,
6570
GetOscapCustomizationsApiArg,
6671
GetOscapCustomizationsApiResponse,
6772
GetOscapProfilesApiArg,
@@ -359,7 +364,7 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({
359364
ComposeBlueprintApiResponse,
360365
ComposeBlueprintApiArg
361366
>({
362-
queryFn: async ({ id: filename }, _, __, baseQuery) => {
367+
queryFn: async ({ id: filename }) => {
363368
try {
364369
const blueprintsDir = await getBlueprintsPath();
365370
const file = cockpit.file(
@@ -386,29 +391,63 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({
386391
image_requests: [ir],
387392
};
388393

389-
const composeResp = await baseQuery({
390-
url: '/compose',
391-
method: 'POST',
392-
body: JSON.stringify(
393-
// since this is the request that gets sent to the cloudapi
394-
// backend, we need to modify it slightly
395-
mapToOnpremRequest(
394+
const uuid = uuidv4();
395+
const composeDir = path.join(blueprintsDir, filename, uuid);
396+
await cockpit.spawn(['mkdir', '-p', composeDir], {});
397+
398+
const ibBpPath = path.join(composeDir, 'bp.json');
399+
await cockpit
400+
.file(ibBpPath)
401+
.replace(
402+
JSON.stringify(
396403
mapHostedToOnPrem(blueprint as CreateBlueprintRequest),
397-
crcComposeRequest.distribution,
398-
[ir],
404+
null,
405+
2,
399406
),
400-
null,
401-
2,
402-
),
403-
headers: {
404-
'content-type': 'application/json',
405-
},
406-
});
407+
);
407408

409+
// save the blueprint request early, since any errors
410+
// in this function cause pretty big headaches with
411+
// the images table
408412
await cockpit
409-
.file(path.join(blueprintsDir, filename, composeResp.data?.id))
413+
.file(path.join(composeDir, 'request.json'))
410414
.replace(JSON.stringify(crcComposeRequest, null, 2));
411-
composes.push({ id: composeResp.data?.id });
415+
416+
const user = await cockpit.user();
417+
const cmd = [
418+
// the image build fails if we don't set
419+
// this for some reason
420+
`HOME=${user.home}`,
421+
'/usr/bin/image-builder',
422+
'build',
423+
'--with-buildlog',
424+
'--blueprint',
425+
ibBpPath,
426+
'--output-dir',
427+
path.join(ARTIFACTS_DIR, uuid),
428+
'--output-name',
429+
uuid,
430+
'--distro',
431+
crcComposeRequest.distribution,
432+
ir.image_type,
433+
];
434+
435+
const process = new LongRunningProcess(
436+
`cockpit-image-builder-${uuid}.service`,
437+
updateComposeStatus(composeDir),
438+
);
439+
440+
// this is a workaround because the process
441+
// can't be started when in `init` state
442+
process.state = 'stopped';
443+
444+
process.run(['bash', '-ec', cmd.join(' ')]).catch(async () => {
445+
await cockpit
446+
.file(path.join(composeDir, 'result'))
447+
.replace(ComposeStatus.FAILURE);
448+
});
449+
450+
composes.push({ id: uuid });
412451
}
413452

414453
return {
@@ -452,37 +491,47 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({
452491
},
453492
}),
454493
getComposeStatus: builder.query<
455-
GetComposeStatusApiResponse,
494+
GetCockpitComposeStatusApiResponse,
456495
GetComposeStatusApiArg
457496
>({
458-
queryFn: async (queryArg, _, __, baseQuery) => {
497+
queryFn: async (queryArg) => {
459498
try {
460-
const resp = await baseQuery({
461-
url: `/composes/${queryArg.composeId}`,
462-
method: 'GET',
463-
});
464499
const blueprintsDir = await getBlueprintsPath();
465500
const info = await fsinfo(blueprintsDir, ['entries'], {
466501
superuser: 'try',
467502
});
468503
const entries = Object.entries(info?.entries || {});
469-
for (const bpEntry of entries) {
504+
for await (const bpEntry of entries) {
505+
const bpComposes = await readComposes(bpEntry[0]);
506+
if (!bpComposes.some((c) => c.id === queryArg.composeId)) {
507+
continue;
508+
}
509+
470510
const request = await cockpit
471-
.file(path.join(blueprintsDir, bpEntry[0], queryArg.composeId))
511+
.file(
512+
path.join(
513+
blueprintsDir,
514+
bpEntry[0],
515+
queryArg.composeId,
516+
'request.json',
517+
),
518+
)
472519
.read();
520+
521+
const status = await composeStatus(
522+
queryArg.composeId,
523+
path.join(blueprintsDir, bpEntry[0], queryArg.composeId),
524+
);
525+
473526
return {
474527
data: {
475-
image_status: resp.data?.image_status,
528+
image_status: status as ImageStatus,
476529
request: JSON.parse(request),
477530
},
478531
};
479532
}
480-
return {
481-
data: {
482-
image_status: '',
483-
request: {},
484-
},
485-
};
533+
534+
throw new Error('Compose not found');
486535
} catch (error) {
487536
return { error };
488537
}

0 commit comments

Comments
 (0)