Skip to content

[js-yaml to yaml migration] @elastic/fleet#252345

Merged
jeramysoucy merged 55 commits intoelastic:mainfrom
jeramysoucy:migrate-js-yaml-to-yaml--elastic-fleet
Apr 14, 2026
Merged

[js-yaml to yaml migration] @elastic/fleet#252345
jeramysoucy merged 55 commits intoelastic:mainfrom
jeramysoucy:migrate-js-yaml-to-yaml--elastic-fleet

Conversation

@jeramysoucy
Copy link
Copy Markdown
Contributor

@jeramysoucy jeramysoucy commented Feb 9, 2026

Migration: js-yaml → yaml (with async loading)

Part of #233198

This PR migrates the Fleet plugin from js-yaml to the yaml package, with asynchronous loading on the client side to avoid increasing the plugin's bundle size.

Summary

  • Replaced all js-yaml imports with the yaml package across ~160 Fleet files (server + client + common + tests)
  • Created a new shared @kbn/yaml-loader package that provides async loading of the yaml module via loadYaml() (dynamic import('yaml'))
  • Client-side code loads yaml asynchronously through a useYaml() React hook and getYamlFormatters() utility, keeping the yaml module out of the synchronous bundle
  • Server-side code imports yaml directly (no async needed)
  • Common/shared utilities (fullAgentPolicyToYaml, fullAgentConfigMapToYaml, createYamlKeysSorter, etc.) use dependency injection — callers pass the yaml module (or specific functions like parse/stringify) so these utilities remain environment-agnostic

Key changes

New package: @kbn/yaml-loader

  • src/platform/packages/shared/kbn-yaml-loader/ — provides loadYaml() which returns Promise<typeof import('yaml')>
  • Registered in tsconfig.base.json, config-paths.json, and root package.json

Async loading infrastructure (client-side)

  • public/services/use_yaml.tsuseYaml() React hook that loads the yaml module asynchronously and caches it globally so subsequent hook calls resolve synchronously
  • public/services/yaml_formatters.tsgetYamlFormatters() returns a cached promise of formatter functions (fullAgentPolicyToYaml, fullAgentConfigMapToYaml)

Dependency injection in common services

  • common/services/yaml_utils.tsYamlModule interface + createYamlKeysSorter() and toYaml() now accept a yaml module parameter
  • common/services/agent_cm_to_yaml.tsfullAgentConfigMapToYaml() accepts yaml: YamlModule
  • common/services/full_agent_policy_to_yaml.tsfullAgentPolicyToYaml() accepts yaml: YamlModule
  • common/settings/agent_policy_settings.tsx — Zod YAML validation uses async loadYaml() in .refine() callbacks via safeParseAsync()

Client-side component updates

Components that previously imported js-yaml synchronously now use useYaml() or getYamlFormatters():

  • edit_output_flyout/index.tsx + use_output_form.tsx — output form YAML config validation and preset detection
  • edit_fleet_proxy_flyout/use_fleet_proxy_form.tsx — proxy headers YAML validation/parsing
  • agent_policy_yaml_flyout.tsx — agent policy YAML display/download
  • agent_effective_config_flyout.tsx — agent effective config display
  • package_policy_input_panel.tsx — stream visibility based on YAML validation
  • agent_enrollment_flyout/hooks.tsx — enrollment YAML generation
  • use_change_log.ts — changelog YAML parsing
  • has_invalid_but_required_var.ts, output_form_validators.tsx — accept parse function parameter
  • Various form hooks (form.tsx, use_package_policy.tsx, add_integration.tsx) — pass yaml to validatePackagePolicy

Preset sync fix for async race condition

  • edit_output_flyout/index.tsx — added a useEffect that syncs the output preset to "custom" when the yaml module loads and the YAML config already contains reserved performance keys. This prevents a race condition where typing reserved keys before yaml loads would leave the preset incorrectly set to "balanced".

Server-side changes

  • All server files import yaml directly (import yaml from 'yaml' or import { parse, stringify } from 'yaml')
  • Pass yaml module to common utilities via dependency injection

API mapping (js-yaml → yaml)

js-yaml yaml
load() parse()
dump() stringify()
noRefs: true aliasDuplicateObjects: false
skipInvalid: true strict: false
sortKeys sortMapEntries
JSON_SCHEMA schema: 'core'

Test updates

  • Updated Jest unit tests with jest.mock for useYaml in test files where async loading would cause issues (single_page_layout/index.test.tsx, edit_package_policy_page/index.test.tsx, etc.)
  • Updated inline snapshots to reflect yaml package output formatting
  • Updated test calls to pass parse function or mock YamlModule where dependency injection was added
  • Zod validation tests updated to use safeParseAsync() for async YAML validation

Testing

  • All Fleet public Jest tests pass (184 suites, 1806 tests)
  • All Fleet common/server Jest tests pass
  • Type checks pass
  • ESLint passes

Note: This is part of a larger js-yamlyaml migration effort across Kibana. Other teams' files are handled in separate PRs. These changes were generated using Cursor.

`"{\\"id\\":\\"1234\\",\\"outputs\\":{\\"default\\":{\\"type\\":\\"elasticsearch\\",\\"hosts\\":[\\"http://localhost:9200\\"]}},\\"inputs\\":[{\\"id\\":\\"test_input-secrets-abcd1234\\",\\"revision\\":1,\\"name\\":\\"secrets-1\\",\\"type\\":\\"test_input\\",\\"data_stream\\":{\\"namespace\\":\\"default\\"},\\"use_output\\":\\"default\\",\\"package_policy_id\\":\\"abcd1234\\",\\"package_var_secret\\":\\"\${SECRET_0}\\",\\"input_var_secret\\":\\"\${SECRET_1}\\",\\"streams\\":[{\\"id\\":\\"test_input-secrets.log-abcd1234\\",\\"data_stream\\":{\\"type\\":\\"logs\\",\\"dataset\\":\\"secrets.log\\"},\\"package_var_secret\\":\\"\${SECRET_0}\\",\\"input_var_secret\\":\\"\${SECRET_1}\\",\\"stream_var_secret\\":\\"\${SECRET_2}\\"}],\\"meta\\":{\\"package\\":{\\"name\\":\\"secrets\\",\\"version\\":\\"1.0.0\\"}}}],\\"secret_references\\":[{\\"id\\":\\"secret-id-1\\"},{\\"id\\":\\"secret-id-2\\"},{\\"id\\":\\"secret-id-3\\"}],\\"revision\\":2,\\"agent\\":{},\\"signed\\":{},\\"output_permissions\\":{},\\"fleet\\":{}}"`
);
expect(yaml).toMatchInlineSnapshot(`
"id: \\"1234\\"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This snapshot now reflects an actual yaml dump of the object. I tested it against the output from js-yaml and it is identical. I could not find a need to pass the stringify/dump function to fullAgentPolicyToYaml, as every call site just passed in the imported dump function as-is from js-yaml. That said if, if there is a reason to keep this modular, then we can put that back in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If I remember the why it was modular it was to avoid the fleet bundle to be too big and to not include the yaml dependencies, but if we are confortable with increasing the limit it should be fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the context @nchaulet! I don't have a strong opinion on bundle size. Maybe @elastic/kibana-operations has insight here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Preference is async in most cases, unless it's needed immediately on app load. The less we load synchronously the quicker the page will appear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jbudz Are you ok moving the "toYaml" utility function to the ui-shared-deps-npm package as @nchaulet suggested? Or is there a better place for it to live?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth a try. It depends on how many packages end up using it. If it's only fleet we're just shifting the sync load to another bundle, but if there's a few plugins using the package the tradeoff may be worth it.

@jeramysoucy jeramysoucy marked this pull request as ready for review February 17, 2026 12:36
@jeramysoucy jeramysoucy requested review from a team as code owners February 17, 2026 12:36
@jeramysoucy jeramysoucy added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// labels Feb 17, 2026
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/kibana-security (Team:Security)

@botelastic botelastic Bot added the Team:Fleet Team label for Observability Data Collection Fleet team label Feb 17, 2026
@nchaulet nchaulet self-requested a review February 17, 2026 12:51
Copy link
Copy Markdown
Member

@nchaulet nchaulet left a comment

Choose a reason for hiding this comment

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

Pulled this locally and test a few of the Fleet flows and it seems to work as expected, just one small suggestion to use error code instead of looking at message but otherwise LGTM 🚀

Comment thread x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts Outdated
Comment thread packages/kbn-optimizer/limits.yml Outdated
filesManagement: 5208
fileUpload: 22957
fleet: 209495
fleet: 322115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we know what's happening with this? We may want to move yaml to ui-shared-deps-npm.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We used to have dynamic import for js-yaml that code path changed #252345 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missed it, thanks. Replied in that thread.

Copy link
Copy Markdown
Contributor

@jbudz jbudz left a comment

Choose a reason for hiding this comment

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

limits.yml related changes LGTM.

@elasticmachine
Copy link
Copy Markdown
Contributor

elasticmachine commented Mar 18, 2026

💔 Build Failed

Failed CI Steps

Test Failures

  • [job] [logs] Fleet Cypress Tests #1 / Add Integration - Automatic Import should create an integration
  • [job] [logs] Fleet Cypress Tests #1 / Add Integration - Automatic Import should create an integration
  • [job] [logs] Scout: [ platform / streams_app-stateful-classic ] plugin / local-stateful-classic - Query streams - Create query stream - should create a query stream as a child of an ingest stream

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
fleet 1471 1549 +78

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
fleet 2.3MB 2.4MB +66.5KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
fleet 200.2KB 201.0KB +766.0B
Unknown metric groups

API count

id before after diff
@kbn/yaml-loader - 87 +87

async chunk count

id before after diff
fleet 11 12 +1

History

@jeramysoucy jeramysoucy requested a review from a team as a code owner March 24, 2026 07:46
// Fallback to the first model value if the requested index is out of range
return values[0]!;
}, nthIndex);
}).toPass({ timeout: 30_000 });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This might be overkill

@jeramysoucy
Copy link
Copy Markdown
Contributor Author

@nchaulet & @jbudz I finally got a clean CI run here. The yaml package is now loaded asynchronously on the browser side, which as intended, eliminates the need to increase the Fleet package size limit.

Could you both re-review, as there have been quite a few changes to this PR recently? What we're doing here sets the foundation for the rest of js-yaml -> yaml migration throughout the codebase. Manual testing of the UI is a good idea since that is affected the most with the later changes.

@jeramysoucy jeramysoucy requested a review from nchaulet March 24, 2026 10:20
@juliaElastic
Copy link
Copy Markdown
Contributor

juliaElastic commented Mar 27, 2026

While testing locally, I found an issue, though I'm not sure it's caused by this pr.
The same works correctly in a new cloud deployment in QA (9.4.0-SNAPSHOT). Tested locally on latest main too, and the issue doesn't happen there.

When adding yaml text to Advanced internal YAML settings to an Agent policy:
image

In cloud:
image

}
);
}
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low form_settings/form_settings.ts:45

When setting.type === 'yaml', the early return on line 45 bypasses the Zod schema validation on lines 47-49. This means YAML settings are only validated for syntax via parse(val) — any constraints in setting.schema (type checks, required fields, min/max values) are never enforced, allowing invalid data through.

Suggested change
return;
return;
🤖 Copy this AI Prompt to have your agent fix this:
In file x-pack/platform/plugins/shared/fleet/server/services/form_settings/form_settings.ts around line 45:

When `setting.type === 'yaml'`, the early `return` on line 45 bypasses the Zod schema validation on lines 47-49. This means YAML settings are only validated for syntax via `parse(val)` — any constraints in `setting.schema` (type checks, required fields, min/max values) are never enforced, allowing invalid data through.

Evidence trail:
x-pack/platform/plugins/shared/fleet/server/services/form_settings/form_settings.ts lines 33-50 (the validate function with early return for YAML type); x-pack/platform/plugins/shared/fleet/common/settings/types.ts lines 24-35 (SettingsConfig interface showing schema is required); x-pack/platform/plugins/shared/fleet/common/settings/agent_policy_settings.tsx lines 29-42 (zodStringWithYamlValidation definition) and lines 286-291 (YAML setting with type: 'yaml' and schema property); x-pack/platform/plugins/shared/fleet/server/services/form_settings/form_settings.test.ts lines 35-43 (test showing YAML setting with schema: z.string())

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 1, 2026

Approvability

Verdict: Needs human review

1 blocking correctness issue found. CODEOWNERS file was modified by a non-owner — requires human review

You can customize Macroscope's approvability policy. Learn more.

@jeramysoucy jeramysoucy requested review from a team as code owners April 1, 2026 13:17
);
const yaml = useYaml();
// Showing streams toggle state (initialized once when yaml loads so we can validate)
const [isShowingStreams, setIsShowingStreams] = useState<boolean>(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low components/package_policy_input_panel.tsx:152

If a user clicks the "Change defaults" button to toggle the streams section before the yaml module finishes loading, the useEffect at lines 154–178 overwrites their choice when yaml finally resolves. This happens because isShowingStreamsInitialized.current is still false during the click, so the effect recomputes and sets isShowingStreams to the default value, collapsing a section the user just expanded.

🤖 Copy this AI Prompt to have your agent fix this:
In file x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx around line 152:

If a user clicks the "Change defaults" button to toggle the streams section before the `yaml` module finishes loading, the `useEffect` at lines 154–178 overwrites their choice when `yaml` finally resolves. This happens because `isShowingStreamsInitialized.current` is still `false` during the click, so the effect recomputes and sets `isShowingStreams` to the default value, collapsing a section the user just expanded.

Evidence trail:
x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.tsx lines 150-178 (useYaml() call, state/ref initialization, useEffect with yaml dependency), line 447 (onClick handler that only calls setIsShowingStreams without setting isShowingStreamsInitialized.current), x-pack/platform/plugins/shared/fleet/public/services/use_yaml.ts lines 22-43 (useYaml hook returns null while loading)

Comment on lines +53 to +55
useEffect(() => {
getYamlFormatters().then(setFormatters);
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low components/agent_policy_yaml_flyout.tsx:53

If getYamlFormatters() rejects (e.g., the yaml module fails to load), the error is unhandled and formatters stays null, leaving the UI stuck on <Loading /> forever with no error shown to the user. Consider adding .catch() to handle the error.

-  useEffect(() => {
-    getYamlFormatters().then(setFormatters);
-  }, []);
+  useEffect(() => {
+    getYamlFormatters().then(setFormatters).catch((err) => {
+      // Handle error appropriately
+    });
+  }, []);
🤖 Copy this AI Prompt to have your agent fix this:
In file x-pack/platform/plugins/shared/fleet/public/applications/fleet/components/agent_policy_yaml_flyout.tsx around lines 53-55:

If `getYamlFormatters()` rejects (e.g., the yaml module fails to load), the error is unhandled and `formatters` stays `null`, leaving the UI stuck on `<Loading />` forever with no error shown to the user. Consider adding `.catch()` to handle the error.

Evidence trail:
x-pack/platform/plugins/shared/fleet/public/applications/fleet/components/agent_policy_yaml_flyout.tsx at REVIEWED_COMMIT:
- Line 50: `const [formatters, setFormatters] = useState<YamlFormatters | null>(null);`
- Lines 52-54: `useEffect(() => { getYamlFormatters().then(setFormatters); }, []);` - no .catch() handler
- Lines 65-67: Rendering logic shows `<Loading />` when `!formatters` is true
- Line 60: `error` variable only captures errors from `useGetOneAgentPolicyFull`, not from formatters loading

When the OTel exporter PR was merged into main and then merged into this branch,
otelExporterConfigInput was left using the removed validateYamlConfig import.
Use validateYamlConfigFn consistently, matching additionalYamlConfigInput above it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

When yaml is null (still loading asynchronously), validateYamlConfigFn is set to () => undefined on line 236. This validator is passed to useInput for both additionalYamlConfigInput and otelExporterConfigInput. Since returning undefined means "no validation errors", if a user submits the form before the yaml module finishes loading, invalid YAML configuration will pass validation and be submitted. The submit button has no guard for !yaml (see isDisabled on line 1187), making this race condition exploitable.

🤖 Copy this AI Prompt to have your agent fix this:
In file x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx around line 1187:

When `yaml` is `null` (still loading asynchronously), `validateYamlConfigFn` is set to `() => undefined` on line 236. This validator is passed to `useInput` for both `additionalYamlConfigInput` and `otelExporterConfigInput`. Since returning `undefined` means "no validation errors", if a user submits the form before the yaml module finishes loading, invalid YAML configuration will pass validation and be submitted. The submit button has no guard for `!yaml` (see `isDisabled` on line 1187), making this race condition exploitable.

Evidence trail:
1. x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx line 236: `const validateYamlConfigFn = yaml ? createValidateYamlConfig(yaml.parse) : () => undefined;`
2. Same file lines 243-252: `additionalYamlConfigInput` and `otelExporterConfigInput` use `validateYamlConfigFn`
3. Same file lines 1187-1188: `isDisabled` return value doesn't check for `!yaml`
4. x-pack/platform/plugins/shared/fleet/public/services/use_yaml.ts lines 22-24: `useYaml` returns `null` initially while loading async
5. x-pack/platform/plugins/shared/fleet/public/hooks/use_input.ts lines 62-71: `validate()` returns `true` when validator returns `undefined`

@jeramysoucy
Copy link
Copy Markdown
Contributor Author

@juliaElastic Sorry to ask again but could you give this another manual test? I had to make several changes after your last review due to merge conflicts and other issues.

Copy link
Copy Markdown
Contributor

@juliaElastic juliaElastic left a comment

Choose a reason for hiding this comment

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

LGTM

@elasticmachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] affected Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store History Snapshot - history snapshot: copies latest to history index and resets behaviors on latest

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
fleet 1609 1688 +79

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
fleet 2.3MB 2.4MB +89.0KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
fleet 202.8KB 203.6KB +796.0B
Unknown metric groups

API count

id before after diff
@kbn/yaml-loader - 87 +87

async chunk count

id before after diff
fleet 11 12 +1

ESLint disabled line counts

id before after diff
fleet 43 44 +1

Total ESLint disabled count

id before after diff
fleet 50 51 +1

History

@efd6
Copy link
Copy Markdown

efd6 commented Apr 22, 2026

The date behaviour in parse(compiledTemplate) after this change will break some integrations. For example an integration that needs to report a date-only API version will have that version replaced with an RFC3339 string approximating the date (concrete example is microsoft_defender_cloud with docs here).

@jeramysoucy
Copy link
Copy Markdown
Contributor Author

@efd6 Thanks for identifying this. I am looking into a fix now, and have asked Fleet to assess test coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:Fleet Team label for Observability Data Collection Fleet team Team:Security Platform Security: Auth, Users, Roles, Spaces, Audit Logging, etc t// v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.