diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json
index 348fc6dc..032ddf32 100644
--- a/libs/i18n/locales/en/translation.json
+++ b/libs/i18n/locales/en/translation.json
@@ -407,7 +407,7 @@
"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.",
+ "The recommended user identity is '{{ runAsUser }}'. To specify a custom user identity, edit the application configuration via YAML or CLI.": "The recommended user identity is '{{ runAsUser }}'. 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/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx
index 2bc43d1c..6fd11de6 100644
--- a/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx
+++ b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx
@@ -6,7 +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';
+import { RUN_AS_ROOT_USER } from '../../../types/deviceSpec';
type ApplicationsTableProps = {
appsStatus: DeviceApplicationStatus[];
@@ -45,7 +45,7 @@ const ApplicationsTable = ({ appsStatus }: ApplicationsTableProps) => {
{app.appType ? : '-'}
-
{app.runAs || RUN_AS_DEFAULT_USER}
+
{app.runAs || RUN_AS_ROOT_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 79f3cd3a..039dccd7 100644
--- a/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts
+++ b/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts
@@ -38,7 +38,8 @@ import {
InlineFileForm,
KubeSecretTemplate,
QuadletAppForm,
- RUN_AS_DEFAULT_USER,
+ RUN_AS_FLIGHTCTL_USER,
+ RUN_AS_ROOT_USER,
SingleContainerAppForm,
SpecConfigTemplate,
SystemdUnitFormValue,
@@ -292,6 +293,15 @@ const haveValuesFilesChanged = (current: string[], updated: string[]): boolean =
const haveHelmValuesChanged = (current: Record, updated: Record): boolean =>
JSON.stringify(current) !== JSON.stringify(updated);
+const hasRunAsChanged = (current: string | undefined, updated: string | undefined): boolean => {
+ if (!current) {
+ // For empty "runAs", we mark the app as changed.
+ // This means that we'll set the field explicitly to the default user (currently "root").
+ return true;
+ }
+ return current !== updated;
+};
+
// Single container apps always have an image, and it doesn't have an inline variant
const hasContainerAppChanged = (current: ContainerApplication, updated: ContainerApplication): boolean =>
hasStringChanged(current.name, updated.name) ||
@@ -299,7 +309,7 @@ const hasContainerAppChanged = (current: ContainerApplication, updated: Containe
havePortsChanged(current.ports || [], updated.ports || []) ||
haveResourceLimitsChanged(current.resources?.limits, updated.resources?.limits) ||
haveEnvVarsChanged(current.envVars || {}, updated.envVars || {}) ||
- hasStringChanged(current.runAs, updated.runAs, RUN_AS_DEFAULT_USER) ||
+ hasRunAsChanged(current.runAs, updated.runAs) ||
haveVolumesChanged(current.volumes || [], updated.volumes || []);
// Helm apps always have an image (chart), and it doesn't have an inline variant
@@ -346,7 +356,7 @@ const hasQuadletAppChanged = (
if (baseChanged) {
return true;
}
- return hasStringChanged(current.runAs, updated.runAs, RUN_AS_DEFAULT_USER);
+ return hasRunAsChanged(current.runAs, updated.runAs);
};
const hasApplicationChanged = (current: ApplicationProviderSpec, updated: ApplicationProviderSpec): boolean => {
@@ -452,7 +462,7 @@ const toApiContainerApp = (app: SingleContainerAppForm): ContainerApplication =>
name: app.name,
image: app.image,
appType: app.appType,
- runAs: app.runAs || RUN_AS_DEFAULT_USER,
+ runAs: app.runAs || RUN_AS_ROOT_USER,
envVars: variablesToEnvVars(app.variables || []),
volumes: formVolumesToApi(app.volumes || [], AppType.AppTypeContainer),
};
@@ -494,7 +504,7 @@ const toApiComposeApp = (app: ComposeAppForm): ComposeApplication => {
// Quadlet apps are currently the same as Compose apps, plus an optional "runAs" field.
const toApiQuadletApp = (app: QuadletAppForm): QuadletApplication => {
const baseApp = toApiComposeApp(app);
- return { ...baseApp, appType: AppType.AppTypeQuadlet, runAs: app.runAs || RUN_AS_DEFAULT_USER };
+ return { ...baseApp, appType: AppType.AppTypeQuadlet, runAs: app.runAs || RUN_AS_ROOT_USER };
};
export const toApiApplication = (app: AppForm): ApplicationProviderSpec => {
@@ -626,6 +636,15 @@ export const getApiConfig = (ct: SpecConfigTemplate): ConfigSourceProvider => {
};
};
+const getRunAsUser = (app: ContainerApplication | QuadletApplication | undefined): string => {
+ if (app) {
+ // Existing apps that don't have a "runAs" user are actually running as the "root" user.
+ return app.runAs || RUN_AS_ROOT_USER;
+ }
+ // For new applications, we want to promote "flightctl" as the default user.
+ return RUN_AS_FLIGHTCTL_USER;
+};
+
const toContainerAppForm = (containerApp: ContainerApplication | undefined): SingleContainerAppForm => {
const ports =
containerApp?.ports?.map((portString) => {
@@ -645,7 +664,7 @@ const toContainerAppForm = (containerApp: ContainerApplication | undefined): Sin
ports,
cpuLimit: limits?.cpu || '',
memoryLimit: limits?.memory || '',
- runAs: containerApp?.runAs || RUN_AS_DEFAULT_USER,
+ runAs: getRunAsUser(containerApp),
};
};
@@ -690,10 +709,11 @@ const toComposeAppForm = (app: ComposeApplication | undefined): ComposeAppForm =
const toQuadletAppForm = (app: QuadletApplication | undefined): QuadletAppForm => {
const baseApp = toComposeAppForm(app);
+
return {
...baseApp,
appType: AppType.AppTypeQuadlet,
- runAs: app?.runAs || RUN_AS_DEFAULT_USER,
+ runAs: getRunAsUser(app),
};
};
diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx
index 2b61e1c6..f081908b 100644
--- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx
+++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx
@@ -5,7 +5,7 @@ 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';
+import { RUN_AS_FLIGHTCTL_USER, RUN_AS_ROOT_USER } from '../../../../types/deviceSpec';
type ApplicationIntegritySettingsProps = {
index: number;
@@ -27,7 +27,7 @@ const ApplicationIntegritySettings = ({ index, isReadOnly }: ApplicationIntegrit
label={isRootless ? t('System integrity protection enabled') : t('System integrity protection disabled')}
isChecked={isRootless}
onChange={async (_, checked) => {
- await setRunAs(checked ? RUN_AS_DEFAULT_USER : RUN_AS_ROOT_USER);
+ await setRunAs(checked ? RUN_AS_FLIGHTCTL_USER : RUN_AS_ROOT_USER);
}}
isDisabled={isReadOnly}
/>
@@ -46,13 +46,13 @@ const ApplicationIntegritySettings = ({ index, isReadOnly }: ApplicationIntegrit
diff --git a/libs/ui-components/src/types/deviceSpec.ts b/libs/ui-components/src/types/deviceSpec.ts
index b85bb868..14bf48c5 100644
--- a/libs/ui-components/src/types/deviceSpec.ts
+++ b/libs/ui-components/src/types/deviceSpec.ts
@@ -17,8 +17,9 @@ import {
import { FlightCtlLabel } from './extraTypes';
import { UpdateScheduleMode } from '../utils/time';
-export const RUN_AS_DEFAULT_USER = 'flightctl';
+// At the moment the "root" user is the default user when no user is specified.
export const RUN_AS_ROOT_USER = 'root';
+export const RUN_AS_FLIGHTCTL_USER = 'flightctl';
export enum ConfigType {
GIT = 'git',