Skip to content

OpenAPI schema for applications changed#463

Merged
celdrake merged 2 commits intoflightctl:mainfrom
celdrake:openapi-app-schema-changed
Feb 2, 2026
Merged

OpenAPI schema for applications changed#463
celdrake merged 2 commits intoflightctl:mainfrom
celdrake:openapi-app-schema-changed

Conversation

@celdrake
Copy link
Collaborator

@celdrake celdrake commented Feb 2, 2026

OpenAPI schema for application changed to a more structured pattern.

Before:

  • App types were not explicit. Fields varying between app types were defined as optional and validations would prevent defining the wrong field for each app type.

After:

  • There are now explicit app types which include the proper definition for that application type.

Summary by CodeRabbit

  • New Features

    • Updated environment variable labels and added a dedicated Variables panel for managing name/value pairs.
    • Improved editor support for Container, Helm, Quadlet and Compose app variants, with better YAML/values and inline-file handling.
  • Refactor

    • Reworked application form model, conversion and change-detection pipeline for consistent editing, unified file/volume handling, and more granular patch generation across app types.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Walkthrough

Simplifies ApplicationProviderSpec into a union of concrete app types; adds generated application type models and new form variants; implements a typed API↔form conversion pipeline with granular change detection; updates Formik-driven components to derive state from fields; adjusts i18n keys and an ESLint import restriction.

Changes

Cohort / File(s) Summary
ESLint & i18n
/.eslintrc.js, libs/i18n/locales/en/translation.json
Removed a no-restricted-imports entry for @flightctl/types; replaced several variable-related i18n keys (added "Environment variables", "Variable name", "Variable value", "Add variable"; removed older keys).
Type exports & foundations
libs/types/index.ts, libs/types/models/ApplicationProviderBase.ts, libs/types/models/ApplicationUser.ts, libs/types/models/ContainerApplicationProperties.ts
Added new exported base and auxiliary types and re-exports (ApplicationProviderBase, ApplicationUser, ContainerApplicationProperties).
Concrete application models
libs/types/models/ComposeApplication.ts, libs/types/models/ContainerApplication.ts, libs/types/models/HelmApplication.ts, libs/types/models/QuadletApplication.ts
Added generated concrete application model types for Compose, Container, Helm, and Quadlet applications.
Provider spec simplification & related models
libs/types/models/ApplicationProviderSpec.ts, libs/types/models/ImageApplicationProviderSpec.ts, libs/types/models/InlineApplicationProviderSpec.ts
Replaced intersection-shaped ApplicationProviderSpec with a union of concrete application types; simplified Image/Inline provider spec shapes and removed some prior dependencies.
UI form model types & validators
libs/ui-components/src/types/deviceSpec.ts, libs/ui-components/src/types/extraTypes.ts, libs/ui-components/src/components/form/validations.ts
Introduced discriminated AppForm union (SingleContainer/Helm/Quadlet/Compose), InlineFileForm and helpers; removed legacy guards and ApplicationProviderSpecFixed; updated validators to use appType/specType discriminators.
Device form utilities & conversion
libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts
Large refactor: added typed toApiApplication and per-app-type toApi* builders, toFormApps mapping, unified file/volume/variable converters, and granular change-detection helpers used by getApplicationPatches.
Formik-driven component signature changes
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx, .../ApplicationHelmForm.tsx, .../ApplicationImageForm.tsx, .../ApplicationInlineForm.tsx, .../ApplicationVolumeForm.tsx, .../ApplicationTemplates.tsx
Removed direct app props; components now derive app state via Formik fields (useField/useFormikContext), updated ports/volumes/files handling, and adapted rendering to new discriminated app forms.
New environment variables UI
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVariablesForm.tsx
Added new Formik FieldArray-backed ApplicationVariablesForm component to manage environment variables (new exported component).
Usage & import renames
libs/ui-components/src/components/Fleet/CreateFleet/utils.ts
Updated imports/usages to renamed export toApiApplication (from toAPIApplication) to match deviceSpecUtils renaming.

Sequence Diagram(s)

sequenceDiagram
  participant UI as App Form UI
  participant Formik as Formik State
  participant Utils as deviceSpecUtils
  participant API as Patch Builder
  participant Server as Backend

  UI->>Formik: user edits AppForm (Container/Helm/Quadlet/Compose)
  Formik->>Utils: getApplicationPatches(basePath, currentApps, updatedAppForms)
  Utils->>Utils: toFormApps -> toApiApplication -> toApiContainerApp|toApiHelmApp|toApiQuadletApp|toApiComposeApp
  Utils->>Utils: run granular change detectors (env/volumes/files/ports/resources)
  Utils->>API: produce PatchRequest
  API->>Server: send PatchRequest
  Server-->>API: 200 / 4xx
  API-->>UI: success / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rawagner
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title "OpenAPI schema for applications changed" accurately reflects the primary change: the OpenAPI schema for applications was refactored to introduce explicit application types with structured field definitions instead of optional fields with runtime validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx (1)

49-56: ⚠️ Potential issue | 🟡 Minor

Potential conflict between name and value props on TextField.

The Namespace field has both name (for Formik binding) and value={app.namespace || ''}. This could cause the field to behave as a controlled component with a potentially stale value, conflicting with Formik's state management. Other refactored fields in this PR (like ApplicationImageForm) removed the explicit value prop to rely purely on Formik's wiring.

🔧 Suggested fix: Remove explicit value prop
       <FormGroupWithHelperText label={t('Namespace')} content={t('The namespace to install the Helm chart into.')}>
         <TextField
           aria-label={t('Namespace')}
           name={`${appFieldName}.namespace`}
-          value={app.namespace || ''}
           isDisabled={isReadOnly}
           placeholder={t('Type namespace here')}
         />
🤖 Fix all issues with AI agents
In
`@libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts`:
- Around line 697-718: The CELIA-WIP TODO in createInitialAppForm leaves
specType uninitialized and can cause inconsistent defaults when AppType changes;
update createInitialAppForm to set a deterministic specType for each branch
(e.g., set app.specType = <appropriate default> inside the
AppType.AppTypeContainer, AppType.AppTypeHelm, AppType.AppTypeQuadlet, and
AppType.AppTypeCompose cases) so the returned AppForm always has a valid
specType, or if you cannot decide on defaults right now, replace the WIP comment
with a clear TODO referencing a ticket/ID and add code to explicitly set
app.specType = null or a sentinel to make the behavior explicit. Ensure you
modify createInitialAppForm and the branches that call toContainerAppForm,
toHelmAppForm, and createQuadletOrComposeAppForm so specType is consistently
initialized.
🧹 Nitpick comments (2)
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVariablesForm.tsx (1)

21-22: Consider improving type safety for error handling.

The @ts-expect-error comment indicates a typing limitation with Formik's error structure. While this works, it could be improved by using a more explicit type cast or by defining a custom error type interface.

♻️ Optional: Define explicit error type
+type AppVarsError = string | { variables?: string };
+
 const ApplicationVariablesForm = ({ appFieldName, isReadOnly }: ApplicationVariablesFormProps) => {
   const { t } = useTranslation();
-  const [{ value: variables = [] }, { error }] = useField<VariablesForm>(`${appFieldName}.variables`);
+  const [{ value: variables = [] }, { error }] = useField<VariablesForm>(`${appFieldName}.variables`);
+  const typedError = error as AppVarsError | undefined;
 
-  // `@ts-expect-error` Formik error object includes "variables"
-  const appVarsError = typeof error?.variables === 'string' ? (error.variables as string) : undefined;
+  const appVarsError = typeof typedError === 'object' && typeof typedError?.variables === 'string' 
+    ? typedError.variables 
+    : undefined;
libs/ui-components/src/components/form/validations.ts (1)

656-773: Validation schema refactored to discriminated approach - verify default case.

The new discriminated validation using appType and specType is cleaner than the previous shape-based checks. However, the default case at line 763 assumes any remaining form is a Compose inline application. If a new application type is added in the future, it would silently use Compose validation.

Consider adding an explicit check or throwing an error for unknown types to fail fast during development.

♻️ Optional: Make default case explicit
         // Inline compose applications
+        if (value.appType === AppType.AppTypeCompose && value.specType === AppSpecType.INLINE) {
         return Yup.object<ComposeAppForm>().shape({
           specType: appSpecTypeSchema(t),
           appType: Yup.string().oneOf([AppType.AppTypeCompose]).required(t('Application type is required')),
           name: validApplicationAndVolumeName(t).required(t('Name is required for compose applications.')),
           files: inlineAppFileSchema(t)
             .test('unique-file-paths', uniqueFilePathsTest(t))
             .test('compose-file-name', composeFileName(t)),
           volumes: composeQuadletVolumesSchema(t),
           variables: appVariablesSchema(t),
         });
+        }
+
+        // Fallback - should not be reached with current types
+        return Yup.object().shape({
+          appType: Yup.string().required(t('Unknown application type')),
+        });

@celdrake celdrake force-pushed the openapi-app-schema-changed branch from 3341d6c to f352bea Compare February 2, 2026 10:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
`@libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts`:
- Around line 711-731: createInitialAppForm returns a quadlet form with the
wrong appType because toQuadletAppForm constructs/returns a form with
AppType.AppTypeCompose; fix toQuadletAppForm so the returned AppForm has appType
set to AppType.AppTypeQuadlet (and ensure any defaults specific to quadlets are
applied), then leave createInitialAppForm unchanged; verify by calling
createInitialAppForm(AppType.AppTypeQuadlet) returns an AppForm whose appType
=== AppType.AppTypeQuadlet.
- Around line 706-709: toQuadletAppForm currently calls toComposeAppForm(app)
which sets appType to AppType.AppTypeCompose and the spread leaves that value in
place; change toQuadletAppForm (the function) to explicitly set appType to
AppType.AppTypeQuadlet in the returned object while preserving other fields and
the runAs default (i.e., return { ...baseApp, appType: AppType.AppTypeQuadlet,
runAs: app?.runAs || RUN_AS_DEFAULT_USER }); ensure AppType is in scope/imports
if needed.

In
`@libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx`:
- Around line 51-64: The reset misbehavior is caused by toQuadletAppForm
returning a Compose form instead of a Quadlet form; fix toQuadletAppForm so it
constructs and returns a form with appType set to AppType.AppTypeQuadlet (and
correct Quadlet fields), ensure createInitialAppForm delegates to the corrected
toQuadletAppForm for AppType.AppTypeQuadlet, and keep the useEffect logic
(shouldResetApp, createInitialAppForm, setValue) as-is so switching to Quadlet
produces the proper initial Quadlet form.

In
`@libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVariablesForm.tsx`:
- Around line 29-32: The current mapping in variables.map uses the array index
as the React key (FormSection key={variableIndex}), which causes input
reconciliation bugs when items are removed; update the VariablesForm type to
include a stable id field (e.g., id: string), ensure existing variables are
normalized to have ids when loaded, and change the render to use that id as the
key (replace key={variableIndex} with key={variable.id}). Also update the logic
that pushes new variables (where new variable objects are created) to generate a
stable id for each new entry using crypto.randomUUID() (or uuid if you add the
dependency) so newly added rows have unique persistent keys. Ensure any
references that build variableFieldName (const variableFieldName =
`${appFieldName}.variables[${variableIndex}]`) continue to compute form paths
correctly by using the index for field names but use the stable id only for the
React key.
🧹 Nitpick comments (5)
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVariablesForm.tsx (1)

36-49: Consider adding consistent grid column spans for visual alignment.

The Name and Value fields are in a Grid without explicit column sizing. For consistent form layout across the application, consider specifying column spans.

♻️ Optional: Add explicit grid columns
                     <Grid hasGutter>
-                        <FormGroup label={t('Name')} isRequired>
+                        <GridItem span={6}>
+                          <FormGroup label={t('Name')} isRequired>
                           <TextField
                             name={`${variableFieldName}.name`}
                             aria-label={t('Variable name')}
                             isDisabled={isReadOnly}
                           />
-                        </FormGroup>
-                        <FormGroup label={t('Value')} isRequired>
+                          </FormGroup>
+                        </GridItem>
+                        <GridItem span={6}>
+                          <FormGroup label={t('Value')} isRequired>
                           <TextField
                             name={`${variableFieldName}.value`}
                             aria-label={t('Variable value')}
                             isDisabled={isReadOnly}
                           />
-                        </FormGroup>
+                          </FormGroup>
+                        </GridItem>
                       </Grid>
libs/ui-components/src/types/deviceSpec.ts (1)

125-129: Consider logging or handling the empty identifier case more explicitly.

The getAppIdentifier function returns an empty string when both name and image are missing. While this may be intentional for new/unsaved applications, callers might benefit from understanding this case.

If this is expected behavior for new applications, a brief inline comment would improve clarity:

 export const getAppIdentifier = (app: AppForm | ApplicationProviderSpec): string => {
   if (app.name) return app.name;
   if ('image' in app && app.image) return app.image;
+  // Returns empty string for new/unsaved applications without name or image
   return '';
 };
libs/ui-components/src/components/form/validations.ts (1)

763-773: Consider explicit appType check in fallback branch for defensive validation.

The fallback branch validates as ComposeAppForm without explicitly checking appType === AppType.AppTypeCompose. While currently correct (all other types are handled above), this could silently accept unknown app types in the future.

♻️ Suggested defensive check
         // Inline compose applications
+        if (value.appType !== AppType.AppTypeCompose) {
+          throw new Error(`Unknown application type: ${value.appType}`);
+        }
         return Yup.object<ComposeAppForm>().shape({
libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts (1)

296-297: JSON.stringify comparison may produce false positives on object key reordering.

haveHelmValuesChanged uses JSON.stringify which is sensitive to key order. If the same values are reordered (e.g., from YAML parsing), this could flag unchanged values as changed. Consider using a deep equality check if this becomes an issue.

libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationTemplates.tsx (1)

148-148: Dead code: isContainer is always false in this context.

At line 148, execution is within the else branch (neither Container nor Helm), so isContainer is always false. The condition (isQuadlet || isContainer) simplifies to just isQuadlet.

♻️ Suggested simplification
-            {(isQuadlet || isContainer) && <ApplicationIntegritySettings index={index} isReadOnly={isReadOnly} />}
+            {isQuadlet && <ApplicationIntegritySettings index={index} isReadOnly={isReadOnly} />}

@celdrake celdrake force-pushed the openapi-app-schema-changed branch 2 times, most recently from c09bca2 to 5dd6c95 Compare February 2, 2026 11:07
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/ui-components/src/components/form/validations.ts (1)

661-668: ⚠️ Potential issue | 🟠 Major

Container app name now required — verify image-app rule.

Container apps are image-based, but this schema now requires name. If image-based apps should allow omitted names, drop the required constraint to avoid blocking valid input.

Suggested change
-            name: validApplicationAndVolumeName(t).required(t('Name is required for single container applications.')),
+            name: validApplicationAndVolumeName(t),

Based on learnings: In the FlightCtl UI project, name is required for inline applications but optional for image applications.

🤖 Fix all issues with AI agents
In
`@libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts`:
- Around line 426-448: The YAML parsing in toApiHelmApp currently treats any
parsed result as a mapping and omits values silently for non-object YAML; update
toApiHelmApp to only set helmApp.values when yaml.load(app.valuesYaml) returns a
plain object (i.e., typeof === 'object', not null, and not an Array) and has
Object.keys(...).length > 0, and catch parse errors as before. Also update the
Yup 'valid-yaml' custom test used in the form validation to parse the YAML and
explicitly return false (validation fail) if the parsed value is not a mapping
object (same plain-object check) so the form prevents non-mapping YAML from
being submitted.

In
`@libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVolumeForm.tsx`:
- Around line 127-132: The array push in ApplicationVolumeForm is always adding
an entry with mountPath, which can produce invalid payloads for
non-single-container apps; update the logic in ApplicationVolumeForm (the push
call that creates new volume entries) to only include mountPath (and allow
adding volumes at all) when isSingleContainerApp is true — otherwise either
disable the add action or push a minimal object without mountPath (or prevent
push entirely) so non-single-container apps do not get mountPath/volume fields
that break OpenAPI validation.
🧹 Nitpick comments (1)
libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts (1)

285-294: JSON.stringify is order-sensitive for Helm values.
If YAML keys are reordered, you’ll detect a change even when semantics are identical. Consider a stable deep-compare to avoid unnecessary patches.

@celdrake celdrake force-pushed the openapi-app-schema-changed branch from 5701ab4 to cd38750 Compare February 2, 2026 15:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libs/ui-components/src/components/form/validations.ts (1)

446-496: ⚠️ Potential issue | 🟡 Minor

Avoid extension mis-detection when directories contain dots.

quadletFileTypesValidation calls getFileExtension(file.path) on the full path. If a folder contains a dot (e.g., dir.v1/app.container), the extension becomes malformed and the supported/unsupported checks can fail. Consider extracting the basename before the extension.

🔧 Suggested fix (use basename before extracting extension)
-const getFileExtension = (path: string): string => {
-  const lastDot = path.lastIndexOf('.');
-  return lastDot !== -1 ? path.substring(lastDot) : '';
-};
+const getFileExtension = (path: string): string => {
+  const fileName = path.split('/').pop() || path;
+  const lastDot = fileName.lastIndexOf('.');
+  return lastDot !== -1 ? fileName.substring(lastDot) : '';
+};
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx (1)

50-56: ⚠️ Potential issue | 🟡 Minor

Remove the explicit value prop from TextField to prevent controlled/uncontrolled conflict.

TextField uses Formik's useField hook internally and spreads field props before component props. When you pass an explicit value={app.namespace || ''}, it overrides Formik's managed value. This causes the displayed value to diverge from Formik's state when the user types, creating an inconsistency where the form state and UI don't match.

♻️ Suggested fix
         <TextField
           aria-label={t('Namespace')}
           name={`${appFieldName}.namespace`}
-          value={app.namespace || ''}
           isDisabled={isReadOnly}
           placeholder={t('Type namespace here')}
         />
🤖 Fix all issues with AI agents
In `@libs/types/models/ApplicationUser.ts`:
- Around line 6-9: The ApplicationUser.runAs property documentation is out of
date: update the OpenAPI schema description (not the generated libs/types/models
files) for the ApplicationUser model so the runAs default text states the
default user is 'flightctl' (matching RUN_AS_DEFAULT_USER in flightctl-ui)
instead of the agent user/root; modify the runAs description in the openapi.yaml
schema (the ApplicationUser -> properties -> runAs description) to reflect this
change and regenerate the types so ApplicationUser.runAs comment will be
correct.

In
`@libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationContainerForm.tsx`:
- Around line 37-38: The duplicate-port detection is wrong because
isDuplicatePortMapping in validations.ts compares the new mapping against itself
(or the same array index), making every check true when ports is non-empty;
update isDuplicatePortMapping to loop/filter ports and compare each existing
mapping's hostPort and containerPort to the new host port (port) and
containerPort but skip the same entry (by index or by strict identity) so only
other entries are considered; then use that corrected function from
ApplicationContainerForm (and the other call sites around the other occurrence)
so adding or editing a different port mapping no longer reports a false
duplicate.
🧹 Nitpick comments (1)
libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationVariablesForm.tsx (1)

21-22: Consider improving the error typing.

The @ts-expect-error suppression works but indicates a type mismatch. Consider defining a proper error type or using type assertion more explicitly.

♻️ Alternative approach
-  // `@ts-expect-error` Formik error object includes "variables"
-  const appVarsError = typeof error?.variables === 'string' ? (error.variables as string) : undefined;
+  const appVarsError = typeof (error as { variables?: string } | undefined)?.variables === 'string' 
+    ? (error as { variables: string }).variables 
+    : undefined;

@celdrake celdrake merged commit bcc78b1 into flightctl:main Feb 2, 2026
6 checks passed
@celdrake celdrake deleted the openapi-app-schema-changed branch February 2, 2026 15:21
celdrake added a commit to celdrake/flightctl-ui that referenced this pull request Feb 3, 2026
* OpenAPI schema for applications changed

(cherry picked from commit bcc78b1)
celdrake added a commit that referenced this pull request Feb 3, 2026
* OpenAPI schema for applications changed

(cherry picked from commit bcc78b1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants