cockpitApi: get worker config

Add a query to load the `/etc/osbuild-worker/osbuild-worker.toml` config
and use this to set the state of the `cloudConfig` store slice.
This commit is contained in:
Gianluca Zuccarelli 2025-04-15 16:05:22 +00:00 committed by Sanne Raymaekers
parent d7945a458a
commit ecc1c2c8cd
4 changed files with 113 additions and 5 deletions

View file

@ -1,17 +1,62 @@
import React from 'react'; import React, { useCallback, useEffect } from 'react';
import { PageSection, Wizard, WizardStep } from '@patternfly/react-core'; import { PageSection, Wizard, WizardStep } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AWSConfig } from './AWSConfig'; import { AWSConfig } from './AWSConfig';
import { isAwsStepValid } from './validators';
import {
changeAWSBucketName,
changeAWSCredsPath,
reinitializeAWSConfig,
} from '../../store/cloudProviderConfigSlice';
import { useGetWorkerConfigQuery } from '../../store/cockpit/cockpitApi';
import { AWSWorkerConfig } from '../../store/cockpit/types';
import { useAppDispatch } from '../../store/hooks';
import { resolveRelPath } from '../../Utilities/path'; import { resolveRelPath } from '../../Utilities/path';
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader'; import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
export const CloudProviderConfig = () => { export const CloudProviderConfig = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch();
const handleClose = () => navigate(resolveRelPath('')); const handleClose = () => navigate(resolveRelPath(''));
const { data, error } = useGetWorkerConfigQuery({});
const initAWSConfig = useCallback(
(config: AWSWorkerConfig | undefined) => {
if (!config) {
dispatch(reinitializeAWSConfig());
return;
}
const { bucket, credentials } = config;
if (bucket && bucket !== '') {
dispatch(changeAWSBucketName(bucket));
}
if (credentials && credentials !== '') {
dispatch(changeAWSCredsPath(credentials));
}
},
[dispatch]
);
useEffect(() => {
initAWSConfig(data?.aws);
}, [data, initAWSConfig]);
if (error) {
// TODO: improve error alert
return (
<div>
There was an error reading the `/etc/osbuild-worker/osbuild-worker.toml`
config file
</div>
);
}
return ( return (
<> <>
<ImageBuilderHeader inWizard={true} /> <ImageBuilderHeader inWizard={true} />
@ -22,6 +67,7 @@ export const CloudProviderConfig = () => {
id="aws-config" id="aws-config"
footer={{ footer={{
nextButtonText: 'Submit', nextButtonText: 'Submit',
isNextDisabled: !isAwsStepValid(config),
isBackDisabled: true, isBackDisabled: true,
}} }}
> >

View file

@ -1,11 +1,37 @@
import path from 'path'; import path from 'path';
export const isAwsBucketValid = (bucket: string): boolean => { import { AWSWorkerConfig } from '../../../store/cockpit/types';
export const isAwsBucketValid = (bucket?: string): boolean => {
if (!bucket || bucket === '') {
return false;
}
const regex = /^[a-z0-9](?:[a-z0-9]|[-.](?=[a-z0-9])){1,61}[a-z0-9]$/; const regex = /^[a-z0-9](?:[a-z0-9]|[-.](?=[a-z0-9])){1,61}[a-z0-9]$/;
return regex.test(bucket); return regex.test(bucket);
}; };
export const isAwsCredsPathValid = (credsPath: string): boolean => { export const isAwsCredsPathValid = (credsPath?: string): boolean => {
if (!credsPath || credsPath === '') {
return false;
}
const validPathPattern = /^(\/[^/\0]*)+\/?$/; const validPathPattern = /^(\/[^/\0]*)+\/?$/;
return path.isAbsolute(credsPath) && validPathPattern.test(credsPath); return path.isAbsolute(credsPath) && validPathPattern.test(credsPath);
}; };
export const isAwsStepValid = (
config: AWSWorkerConfig | undefined
): boolean => {
if (!config) {
return true;
}
if (!config.bucket && !config.credentials) {
return false;
}
return (
isAwsBucketValid(config.bucket) && isAwsCredsPathValid(config.credentials)
);
};

View file

@ -8,6 +8,16 @@ export const initialState: CloudProviderConfigState = {
aws: {}, aws: {},
}; };
export const selectAWSConfig = (state: RootState) => {
if (Object.keys(state.cloudConfig.aws).length === 0) {
// just return undefined since the config is empty
// and we don't want to save `[aws]` header to the
// worker config file with no body
return undefined;
}
return state.cloudConfig.aws;
};
export const selectAWSBucketName = (state: RootState) => { export const selectAWSBucketName = (state: RootState) => {
return state.cloudConfig.aws.bucket; return state.cloudConfig.aws.bucket;
}; };
@ -20,6 +30,9 @@ export const cloudProviderConfigSlice = createSlice({
name: 'cloudConfig', name: 'cloudConfig',
initialState, initialState,
reducers: { reducers: {
reinitializeAWSConfig: (state) => {
state.aws = {};
},
changeAWSBucketName: (state, action: PayloadAction<string>) => { changeAWSBucketName: (state, action: PayloadAction<string>) => {
state.aws.bucket = action.payload; state.aws.bucket = action.payload;
}, },
@ -29,5 +42,8 @@ export const cloudProviderConfigSlice = createSlice({
}, },
}); });
export const { changeAWSBucketName, changeAWSCredsPath } = export const {
cloudProviderConfigSlice.actions; reinitializeAWSConfig,
changeAWSBucketName,
changeAWSCredsPath,
} = cloudProviderConfigSlice.actions;

View file

@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid';
// the same unix socket. This allows us to split out the code a little // the same unix socket. This allows us to split out the code a little
// bit so that the `cockpitApi` doesn't become a monolith. // bit so that the `cockpitApi` doesn't become a monolith.
import { contentSourcesApi } from './contentSourcesApi'; import { contentSourcesApi } from './contentSourcesApi';
import type { WorkerConfigResponse } from './types';
import { import {
mapHostedToOnPrem, mapHostedToOnPrem,
@ -584,6 +585,24 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({
} }
}, },
}), }),
getWorkerConfig: builder.query<WorkerConfigResponse, unknown>({
queryFn: async () => {
try {
const config = await cockpit
.file('/etc/osbuild-worker/osbuild-worker.toml')
.read();
return { data: TOML.parse(config) };
} catch (error) {
// no worker file error message
if (error.message === 'input is null') {
return { data: {} };
}
return { error };
}
},
}),
}; };
}, },
// since we are inheriting some endpoints, // since we are inheriting some endpoints,
@ -607,4 +626,5 @@ export const {
useGetComposesQuery, useGetComposesQuery,
useGetBlueprintComposesQuery, useGetBlueprintComposesQuery,
useGetComposeStatusQuery, useGetComposeStatusQuery,
useGetWorkerConfigQuery,
} = cockpitApi; } = cockpitApi;