diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 03c3d90d..a77d83e7 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -265,6 +265,7 @@ "Device applications table": "Device applications table", "Ready": "Ready", "Restarts": "Restarts", + "Run as user": "Run as user", "Embedded": "Embedded", "Yes": "Yes", "No": "No", @@ -400,6 +401,13 @@ "Content is base64 encoded": "Content is base64 encoded", "Delete file": "Delete file", "Add file": "Add file", + "Access & permissions": "Access & permissions", + "System integrity protection": "System integrity protection", + "System integrity protection enabled": "System integrity protection enabled", + "System integrity protection disabled": "System integrity protection disabled", + "Prevents this workload from modifying critical host operating system files. We recommend keeping this enabled to maintain system integrity.": "Prevents this workload from modifying critical host operating system files. We recommend keeping this enabled to maintain system integrity.", + "Rootless user identity": "Rootless user identity", + "By default, workloads run as the '{{ runAsUser }}' user. To specify a custom user identity, edit the application configuration via YAML or CLI.": "By default, workloads run as the '{{ runAsUser }}' user. To specify a custom user identity, edit the application configuration via YAML or CLI.", "Application {{ appNum }}": "Application {{ appNum }}", "Application type": "Application type", "Select an application type": "Select an application type", diff --git a/libs/types/models/ApplicationProviderSpec.ts b/libs/types/models/ApplicationProviderSpec.ts index 8d6f6457..36259cf9 100644 --- a/libs/types/models/ApplicationProviderSpec.ts +++ b/libs/types/models/ApplicationProviderSpec.ts @@ -12,5 +12,9 @@ export type ApplicationProviderSpec = (ApplicationEnvVars & { */ name?: string; appType: AppType; + /** + * The username of the system user this application should be run under. This is not the same as the user within any containers of the application (if applicable). Defaults to the user that the agent runs as (generally root) if not specified. + */ + runAs?: string; } & (ImageApplicationProviderSpec | InlineApplicationProviderSpec)); diff --git a/libs/types/models/DeviceApplicationStatus.ts b/libs/types/models/DeviceApplicationStatus.ts index fe941c85..c2b5f5ff 100644 --- a/libs/types/models/DeviceApplicationStatus.ts +++ b/libs/types/models/DeviceApplicationStatus.ts @@ -24,6 +24,10 @@ export type DeviceApplicationStatus = { */ embedded: boolean; appType: AppType; + /** + * The username of the system user this application is runing under. If blank, the application is run as the same user as the agent (generally root). + */ + runAs?: string; /** * Status of volumes used by this application. */ diff --git a/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx index 4325cc4a..2bc43d1c 100644 --- a/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx +++ b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx @@ -6,6 +6,7 @@ import { DeviceApplicationStatus } from '@flightctl/types'; import { useTranslation } from '../../../hooks/useTranslation'; import ApplicationStatus from '../../Status/ApplicationStatus'; import { getAppTypeLabel } from '../../../utils/apps'; +import { RUN_AS_DEFAULT_USER } from '../../../types/deviceSpec'; type ApplicationsTableProps = { appsStatus: DeviceApplicationStatus[]; @@ -27,6 +28,7 @@ const ApplicationsTable = ({ appsStatus }: ApplicationsTableProps) => { {t('Ready')} {t('Restarts')} {t('Type')} + {t('Run as user')} {t('Embedded')} @@ -43,6 +45,7 @@ const ApplicationsTable = ({ appsStatus }: ApplicationsTableProps) => { {app.appType ? : '-'} + {app.runAs || RUN_AS_DEFAULT_USER} {app.embedded ? t('Yes') : t('No')} ); diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts b/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts index bed3bdec..490c4f20 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts @@ -28,12 +28,15 @@ import { ConfigSourceProvider, ConfigType, GitConfigTemplate, + HelmImageAppForm, HttpConfigTemplate, InlineConfigTemplate, KubeSecretTemplate, PortMapping, QuadletImageAppForm, QuadletInlineAppForm, + RUN_AS_DEFAULT_USER, + SingleContainerAppForm, SpecConfigTemplate, SystemdUnitFormValue, isComposeImageAppForm, @@ -47,9 +50,10 @@ import { isKubeProviderSpec, isKubeSecretTemplate, isQuadletImageAppForm, + isQuadletInlineAppForm, isSingleContainerAppForm, } from '../../../types/deviceSpec'; -import { ApplicationProviderSpecFixed, InlineApplicationFileFixed } from '../../../types/extraTypes'; +import { InlineApplicationFileFixed } from '../../../types/extraTypes'; const DEFAULT_INLINE_FILE_MODE = 420; // In Octal: 0644 const DEFAULT_INLINE_FILE_USER = 'root'; @@ -293,6 +297,7 @@ export const toAPIApplication = (app: AppForm): ApplicationProviderSpec => { limits: appLimits, }; } + data.runAs = app.runAs || RUN_AS_DEFAULT_USER; return data; } @@ -306,23 +311,24 @@ export const toAPIApplication = (app: AppForm): ApplicationProviderSpec => { if (app.name) { data.name = app.name; } + if (isQuadletImageAppForm(app) && app.runAs) { + data.runAs = app.runAs; + } return data; } // Inline applications (Quadlet or Compose) - return { + const inlineData: ApplicationProviderSpec = { name: app.name, appType: app.appType, - inline: app.files.map( - (file): InlineApplicationFileFixed => ({ - path: file.path, - content: file.content || '', - contentEncoding: file.base64 ? EncodingType.EncodingBase64 : EncodingType.EncodingPlain, - }), - ), + inline: toAPIFiles(app.files), envVars, volumes, }; + if (isQuadletInlineAppForm(app) && app.runAs) { + inlineData.runAs = app.runAs; + } + return inlineData; }; const hasInlineApplicationChanged = ( @@ -344,6 +350,15 @@ const hasInlineApplicationChanged = ( if (filesChanged) { return true; } + + // Check runAs for Quadlet inline apps + if (isQuadletInlineAppForm(updatedApp)) { + const currentAppWithRunAs = currentApp as InlineApplicationProviderSpec & { runAs?: string }; + if ((currentAppWithRunAs.runAs || RUN_AS_DEFAULT_USER) !== (updatedApp.runAs || RUN_AS_DEFAULT_USER)) { + return true; + } + } + return !areVolumesEqual(currentApp.volumes || [], updatedApp.volumes || []); }; @@ -445,6 +460,9 @@ const hasSingleContainerAppChanged = (currentApp: ApplicationProviderSpec, updat return true; } + if ((imageApp.runAs || RUN_AS_DEFAULT_USER) !== (updatedApp.runAs || RUN_AS_DEFAULT_USER)) { + return true; + } return !areVolumesEqual(imageApp.volumes || [], updatedApp.volumes || []); }; @@ -501,6 +519,13 @@ const hasApplicationChanged = (currentApp: ApplicationProviderSpec, updatedApp: return true; } + // Check runAs for Quadlet image apps + if (isQuadletImageAppForm(updatedApp)) { + if ((currentApp.runAs || RUN_AS_DEFAULT_USER) !== (updatedApp.runAs || RUN_AS_DEFAULT_USER)) { + return true; + } + } + return !areVolumesEqual(imageApp.volumes || [], updatedApp.volumes || []); } @@ -610,8 +635,24 @@ export const getApiConfig = (ct: SpecConfigTemplate): ConfigSourceProvider => { }; }; -const getAppFormVariables = (app: ApplicationProviderSpecFixed) => - Object.entries(app.envVars || {}).map(([varName, varValue]) => ({ name: varName, value: varValue })); +const getAppFormVariables = (envVars: Record | undefined) => + Object.entries(envVars || {}).map(([varName, varValue]) => ({ name: varName, value: varValue })); + +const toFormFiles = (files: InlineApplicationFileFixed[]) => { + return files.map((file) => ({ + path: file.path || '', + content: file.content || '', + base64: file.contentEncoding === EncodingType.EncodingBase64, + })); +}; + +const toAPIFiles = (files: ComposeInlineAppForm['files']) => { + return files.map((file) => ({ + path: file.path, + content: file.content || '', + contentEncoding: file.base64 ? EncodingType.EncodingBase64 : EncodingType.EncodingPlain, + })); +}; const convertVolumesToForm = (volumes?: ApplicationVolume[]) => { if (!volumes) return []; @@ -630,73 +671,144 @@ const convertVolumesToForm = (volumes?: ApplicationVolume[]) => { }); }; +const createContainerApp = ( + containerApp: (ApplicationProviderSpec & ImageApplicationProviderSpec) | undefined, +): SingleContainerAppForm => { + const ports = + containerApp?.ports?.map((portString) => { + const [hostPort, containerPort] = portString.split(':'); + return { hostPort: hostPort || '', containerPort: containerPort || '' }; + }) || []; + + const limits = containerApp?.resources?.limits; + return { + appType: AppType.AppTypeContainer, + specType: AppSpecType.OCI_IMAGE, + name: containerApp?.name || '', + image: containerApp?.image || '', + variables: getAppFormVariables(containerApp?.envVars), + volumes: convertVolumesToForm(containerApp?.volumes), + ports, + limits: limits + ? { + cpu: limits.cpu || '', + memory: limits.memory || '', + } + : undefined, + runAs: containerApp?.runAs || RUN_AS_DEFAULT_USER, + }; +}; + +const createHelmApp = ( + helmApp: (ApplicationProviderSpec & ImageApplicationProviderSpec) | undefined, +): HelmImageAppForm => { + const values = helmApp?.values || {}; + return { + appType: AppType.AppTypeHelm, + specType: AppSpecType.OCI_IMAGE, + name: helmApp?.name || '', + image: helmApp?.image || '', + namespace: helmApp?.namespace, + valuesYaml: Object.keys(values).length > 0 ? yaml.dump(values) : '', + valuesFiles: helmApp?.valuesFiles || [''], + }; +}; + +const createQuadletImageApp = ( + quadletApp: (ApplicationProviderSpec & ImageApplicationProviderSpec) | undefined, +): QuadletImageAppForm => { + return { + appType: AppType.AppTypeQuadlet, + specType: AppSpecType.OCI_IMAGE, + name: quadletApp?.name || '', + image: quadletApp?.image || '', + variables: getAppFormVariables(quadletApp?.envVars), + volumes: convertVolumesToForm(quadletApp?.volumes), + runAs: quadletApp?.runAs || RUN_AS_DEFAULT_USER, + }; +}; + +const createQuadletInlineApp = ( + quadletApp: (ApplicationProviderSpec & InlineApplicationProviderSpec) | undefined, +): QuadletInlineAppForm => { + return { + appType: AppType.AppTypeQuadlet, + specType: AppSpecType.INLINE, + name: quadletApp?.name || '', + files: toFormFiles(quadletApp?.inline || []), + variables: getAppFormVariables(quadletApp?.envVars), + volumes: convertVolumesToForm(quadletApp?.volumes), + runAs: quadletApp?.runAs || RUN_AS_DEFAULT_USER, + }; +}; + +const createComposeImageApp = ( + composeApp: (ApplicationProviderSpec & ImageApplicationProviderSpec) | undefined, +): ComposeImageAppForm => { + return { + appType: AppType.AppTypeCompose, + specType: AppSpecType.OCI_IMAGE, + name: composeApp?.name || '', + image: composeApp?.image || '', + variables: getAppFormVariables(composeApp?.envVars), + volumes: convertVolumesToForm(composeApp?.volumes), + }; +}; + +const createComposeInlineApp = ( + composeApp: (ApplicationProviderSpec & InlineApplicationProviderSpec) | undefined, +): ComposeInlineAppForm => { + return { + appType: AppType.AppTypeCompose, + specType: AppSpecType.INLINE, + name: composeApp?.name || '', + files: toFormFiles(composeApp?.inline || []), + variables: getAppFormVariables(composeApp?.envVars), + volumes: convertVolumesToForm(composeApp?.volumes), + }; +}; + +export const createInitialAppForm = (appType: AppType, specType: AppSpecType, name: string = ''): AppForm => { + let app: AppForm; + switch (appType) { + case AppType.AppTypeContainer: + app = createContainerApp(undefined); + break; + case AppType.AppTypeHelm: + app = createHelmApp(undefined); + break; + case AppType.AppTypeQuadlet: + app = specType === AppSpecType.OCI_IMAGE ? createQuadletImageApp(undefined) : createQuadletInlineApp(undefined); + break; + case AppType.AppTypeCompose: + app = specType === AppSpecType.OCI_IMAGE ? createComposeImageApp(undefined) : createComposeInlineApp(undefined); + break; + } + app.name = name; + return app; +}; + export const getApplicationValues = (deviceSpec?: DeviceSpec): AppForm[] => { const apps = deviceSpec?.applications || []; return apps.map((app) => { if (!app.appType) { throw new Error('Application appType is required'); } - // Single container application - if (app.appType === AppType.AppTypeContainer && isImageAppProvider(app)) { - const imageApp = app as ImageApplicationProviderSpec; - const ports = - imageApp.ports?.map((portString) => { - const [hostPort, containerPort] = portString.split(':'); - return { hostPort: hostPort || '', containerPort: containerPort || '' }; - }) || []; - - return { - appType: AppType.AppTypeContainer, - specType: AppSpecType.OCI_IMAGE, - name: app.name || '', - image: app.image, - variables: getAppFormVariables(app), - volumes: convertVolumesToForm(app.volumes), - ports, - limits: imageApp.resources?.limits - ? { - cpu: imageApp.resources.limits.cpu || '', - memory: imageApp.resources.limits.memory || '', - } - : undefined, - }; - } - - // Helm application - if (app.appType === AppType.AppTypeHelm && isImageAppProvider(app)) { - return { - appType: AppType.AppTypeHelm, - specType: AppSpecType.OCI_IMAGE, - name: app.name || '', - image: app.image, - namespace: app.namespace, - valuesYaml: app.values && Object.keys(app.values).length > 0 ? yaml.dump(app.values) : undefined, - valuesFiles: app.valuesFiles || [''], - }; - } - // Compose or Quadlet image application - if (isImageAppProvider(app)) { - return { - appType: app.appType, - specType: AppSpecType.OCI_IMAGE, - name: app.name || '', - image: app.image, - variables: getAppFormVariables(app), - volumes: convertVolumesToForm(app.volumes), - } as QuadletImageAppForm | ComposeImageAppForm; + switch (app.appType) { + case AppType.AppTypeContainer: + return createContainerApp(app as ApplicationProviderSpec & ImageApplicationProviderSpec); + case AppType.AppTypeHelm: + return createHelmApp(app as ApplicationProviderSpec & ImageApplicationProviderSpec); + case AppType.AppTypeQuadlet: + return isImageAppProvider(app) + ? createQuadletImageApp(app) + : createQuadletInlineApp(app as ApplicationProviderSpec & InlineApplicationProviderSpec & { runAs?: string }); + case AppType.AppTypeCompose: + return isImageAppProvider(app) + ? createComposeImageApp(app) + : createComposeInlineApp(app as ApplicationProviderSpec & InlineApplicationProviderSpec); } - - // Compose or Quadlet inline application - const inlineApp = app as InlineApplicationProviderSpec; - return { - appType: app.appType, - specType: AppSpecType.INLINE, - name: app.name || '', - files: inlineApp.inline, - variables: getAppFormVariables(app), - volumes: convertVolumesToForm(app.volumes), - } as QuadletInlineAppForm | ComposeInlineAppForm; }); }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx index 6f2c244a..ce2296d7 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx @@ -9,6 +9,7 @@ import ErrorHelperText from '../../../form/FieldHelperText'; import { isDuplicatePortMapping, isValidPortMapping, validatePortNumber } from '../../../form/validations'; import { useTranslation } from '../../../../hooks/useTranslation'; import { PortMapping, SingleContainerAppForm } from '../../../../types/deviceSpec'; +import ApplicationIntegritySettings from './ApplicationIntegritySettings'; import './ApplicationContainerForm.css'; @@ -24,6 +25,7 @@ const ApplicationContainerForm = ({ const { t } = useTranslation(); const appFieldName = `applications[${index}]`; const [{ value: ports }, , { setValue: setPorts, setTouched }] = useField(`${appFieldName}.ports`); + const [hostPort, setHostPort] = React.useState(''); const [containerPort, setContainerPort] = React.useState(''); const [hostPortTouched, setHostPortTouched] = React.useState(false); @@ -293,6 +295,7 @@ const ApplicationContainerForm = ({ + ); }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationImageForm.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationImageForm.tsx index 2489bde1..53409ccd 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationImageForm.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationImageForm.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Grid } from '@patternfly/react-core'; import { FormGroupWithHelperText } from '../../../common/WithHelperText'; import TextField from '../../../form/TextField'; @@ -20,22 +21,25 @@ const ApplicationImageForm = ({ const createAppLink = useAppLinks('createApp'); return ( - - {t('The application image. Learn how to create one')} - - } - isRequired - > - - + + + {t('The application image. Learn how to create one')}{' '} + + + } + isRequired + > + + + ); }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationInlineForm.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationInlineForm.tsx index a6a573ee..7ea6b3a5 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationInlineForm.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationInlineForm.tsx @@ -70,57 +70,59 @@ const ApplicationInlineForm = ({ } return ( - - {({ push, remove }) => ( - <> - {app.files?.map((file, fileIndex) => { - const fieldName = `applications[${index}].files[${fileIndex}]`; - return ( - - - - - {!isReadOnly && app.files.length > 1 && ( - - - - )} - - )} - + {!isReadOnly && ( + + + + )} + + )} + + ); }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx new file mode 100644 index 00000000..2b61e1c6 --- /dev/null +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { useField } from 'formik'; +import { Content, FormGroup, FormSection, Switch } from '@patternfly/react-core'; + +import { useTranslation } from '../../../../hooks/useTranslation'; +import TextField from '../../../form/TextField'; +import { DefaultHelperText } from '../../../form/FieldHelperText'; +import { RUN_AS_DEFAULT_USER, RUN_AS_ROOT_USER } from '../../../../types/deviceSpec'; + +type ApplicationIntegritySettingsProps = { + index: number; + isReadOnly?: boolean; +}; + +const ApplicationIntegritySettings = ({ index, isReadOnly }: ApplicationIntegritySettingsProps) => { + const { t } = useTranslation(); + const appFieldName = `applications[${index}]`; + const [{ value: runAs }, , { setValue: setRunAs }] = useField(`${appFieldName}.runAs`); + const isRootless = runAs !== RUN_AS_ROOT_USER; + + return ( + + + { + await setRunAs(checked ? RUN_AS_DEFAULT_USER : RUN_AS_ROOT_USER); + }} + isDisabled={isReadOnly} + /> + + {t( + 'Prevents this workload from modifying critical host operating system files. We recommend keeping this enabled to maintain system integrity.', + )} + + } + /> + + {isRootless && ( + + + + )} + + ); +}; + +export default ApplicationIntegritySettings; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx index e281c5a1..5ebfa7cf 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx @@ -27,6 +27,7 @@ import { isQuadletInlineAppForm, isSingleContainerAppForm, } from '../../../../types/deviceSpec'; +import { createInitialAppForm } from '../deviceSpecUtils'; import { useTranslation } from '../../../../hooks/useTranslation'; import TextField from '../../../form/TextField'; import FormSelect from '../../../form/FormSelect'; @@ -40,6 +41,7 @@ import ApplicationInlineForm from './ApplicationInlineForm'; import ApplicationContainerForm from './ApplicationContainerForm'; import ApplicationHelmForm from './ApplicationHelmForm'; import ApplicationVolumeForm from './ApplicationVolumeForm'; +import ApplicationIntegritySettings from './ApplicationIntegritySettings'; import './ApplicationsForm.css'; @@ -51,6 +53,7 @@ const ApplicationSection = ({ index, isReadOnly }: { index: number; isReadOnly?: const isContainer = isSingleContainerAppForm(app); const isHelm = isHelmImageAppForm(app); + const isQuadlet = isQuadletImageAppForm(app) || isQuadletInlineAppForm(app); const isImageIncomplete = !isContainer && specType === AppSpecType.OCI_IMAGE && !('image' in app); const isInlineIncomplete = !isContainer && specType === AppSpecType.INLINE && !('files' in app); const isContainerIncomplete = isContainer && (!('ports' in app) || !('volumes' in app)); @@ -64,69 +67,10 @@ const ApplicationSection = ({ index, isReadOnly }: { index: number; isReadOnly?: const appTypesOptions = appTypeOptions(t); React.useEffect(() => { - if (!shouldResetApp) { - return; - } - // When switching appType to Container, initialize Container-specific fields - if (appType === AppType.AppTypeContainer) { - setValue( - { - appType: AppType.AppTypeContainer, - specType: AppSpecType.OCI_IMAGE, - name: appName || '', - image: '', - variables: [], - ports: [], - volumes: [], - } as AppForm, - false, - ); - return; - } - - if (appType === AppType.AppTypeHelm) { - setValue( - { - appType: AppType.AppTypeHelm, - specType: AppSpecType.OCI_IMAGE, - name: appName || '', - image: '', - namespace: undefined, - valuesYaml: '', - valuesFiles: [''], // We want to show a "values files" field by default - } as AppForm, - false, - ); - return; - } - - // When switching specType, the app becomes "incomplete" and we must add the required fields for the new type - if (specType === AppSpecType.INLINE) { - // Switching to inline - need files - setValue( - { - specType: AppSpecType.INLINE, - appType, - name: appName || '', - files: [{ path: '', content: '' }], - variables: [], - volumes: [], - } as AppForm, - false, - ); - } else if (specType === AppSpecType.OCI_IMAGE) { - // Switching to image - need image field - setValue( - { - specType: AppSpecType.OCI_IMAGE, - appType, - name: appName || '', - image: '', - variables: [], - volumes: [], - } as AppForm, - false, - ); + // When switching types we must ensure all mandatory fields are initialized for the new type + if (shouldResetApp) { + const app = createInitialAppForm(appType, specType, appName || ''); + setValue(app, false); } }, [shouldResetApp, specType, appType, appName, setValue]); @@ -214,6 +158,7 @@ const ApplicationSection = ({ index, isReadOnly }: { index: number; isReadOnly?: {(isQuadletInlineAppForm(app) || isComposeInlineAppForm(app)) && ( )} + {isQuadlet && } )} @@ -342,15 +287,7 @@ const ApplicationTemplates = ({ isReadOnly }: { isReadOnly?: boolean }) => { icon={} iconPosition="start" onClick={() => { - push({ - appType: AppType.AppTypeContainer, - specType: AppSpecType.OCI_IMAGE, - name: '', - image: '', - variables: [], - ports: [], - volumes: [], - }); + push(createInitialAppForm(AppType.AppTypeContainer, AppSpecType.OCI_IMAGE)); }} > {t('Add application')} diff --git a/libs/ui-components/src/components/form/validations.ts b/libs/ui-components/src/components/form/validations.ts index 1e5c012b..c6b16f71 100644 --- a/libs/ui-components/src/components/form/validations.ts +++ b/libs/ui-components/src/components/form/validations.ts @@ -704,6 +704,7 @@ export const validApplicationsSchema = (t: TFunction) => { }), volumes: singleContainerVolumesSchema(t), variables: appVariablesSchema(t), + runAs: Yup.string(), }); } diff --git a/libs/ui-components/src/types/deviceSpec.ts b/libs/ui-components/src/types/deviceSpec.ts index d66f2a0e..b70ea234 100644 --- a/libs/ui-components/src/types/deviceSpec.ts +++ b/libs/ui-components/src/types/deviceSpec.ts @@ -13,6 +13,9 @@ import { import { ApplicationProviderSpecFixed, FlightCtlLabel } from './extraTypes'; import { UpdateScheduleMode } from '../utils/time'; +export const RUN_AS_DEFAULT_USER = 'flightctl'; +export const RUN_AS_ROOT_USER = 'root'; + export enum ConfigType { GIT = 'git', HTTP = 'http', @@ -63,12 +66,14 @@ export type SingleContainerAppForm = AppBase & { image: string; ports?: PortMapping[]; limits?: ApplicationResourceLimits; + runAs?: string; }; export type QuadletImageAppForm = AppBase & { appType: AppType.AppTypeQuadlet; specType: AppSpecType.OCI_IMAGE; image: string; + runAs?: string; }; export type QuadletInlineAppForm = AppBase & { @@ -76,6 +81,7 @@ export type QuadletInlineAppForm = AppBase & { specType: AppSpecType.INLINE; name: string; // transforms the field in required files: InlineContent[]; + runAs?: string; }; export type ComposeImageAppForm = AppBase & {