From eea6a06f1776d718ef536e6af32908e8d878319b Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 2 Apr 2026 10:44:04 +1300 Subject: [PATCH 1/2] fix: use strict_booleans to avoid YAML 1.1 boolean key parsing Enable `strict_booleans` in serde-saphyr options so that only `true`/`false` are parsed as booleans (YAML 1.2 behavior). YAML 1.1 treats `on`/`off`/`yes`/`no` as booleans, which causes `#[serde(flatten)]` to fail when these words appear as map keys in Kubernetes CRD resources. The flatten buffering layer stores the key as boolean `true`, then errors with "expected a string key" when reconstructing the `Map`. This was hit in practice with a PostgresPolicy CRD that uses an `on` field in grant definitions: grants: - on: { type: schema } privileges: [USAGE] With this fix, `on` is treated as a plain string key while `login: true` and `inherit: false` continue to work as booleans. --- kustomizer/src/yaml.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/kustomizer/src/yaml.rs b/kustomizer/src/yaml.rs index 8c0f3dfd..2ddd4d2b 100644 --- a/kustomizer/src/yaml.rs +++ b/kustomizer/src/yaml.rs @@ -2,32 +2,46 @@ use std::io::Read; use serde::de::DeserializeOwned; +/// Default options for YAML deserialization. +/// +/// Uses `strict_booleans` (YAML 1.2 behavior) so that only `true`/`false` are +/// parsed as booleans. YAML 1.1 forms like `on`/`off`/`yes`/`no` are treated +/// as plain strings, avoiding the "Norway problem" and ensuring non-string map +/// keys don't appear when these words are used as field names (e.g. `on:` in +/// Kubernetes CRDs). +fn options() -> serde_saphyr::Options { + serde_saphyr::Options { + strict_booleans: true, + ..Default::default() + } +} + pub fn from_str(s: &str) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_str(s).map_err(Into::into) + serde_saphyr::from_str_with_options(s, options()).map_err(Into::into) } pub fn from_slice(s: &[u8]) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_slice(s).map_err(Into::into) + serde_saphyr::from_slice_with_options(s, options()).map_err(Into::into) } pub fn from_reader(reader: impl Read) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_reader(reader).map_err(Into::into) + serde_saphyr::from_reader_with_options(reader, options()).map_err(Into::into) } pub fn from_reader_multi(mut reader: impl Read) -> anyhow::Result> where T: DeserializeOwned, { - serde_saphyr::read(&mut reader) + serde_saphyr::read_with_options(&mut reader, options()) .map(|res| res.map_err(Into::into)) .collect() } From 8cef4453453673a08f100cc10db5da6252e38895 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 2 Apr 2026 11:45:21 +1300 Subject: [PATCH 2/2] address review: const DESER_OPTS, add regression test for YAML 1.1 boolean key - Make options a `const DESER_OPTS` instead of a function - Add regression test with a CRD using `on:` as a map key alongside `login: true` and `inherit: false` boolean values --- kustomizer/src/yaml.rs | 44 ++++++++++++++----- .../yaml11-boolean-key/kustomization.yaml | 4 ++ .../regression/yaml11-boolean-key/output.yaml | 30 +++++++++++++ .../regression/yaml11-boolean-key/policy.yaml | 21 +++++++++ .../regression/yaml11-boolean-key/test.yaml | 1 + 5 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/kustomization.yaml create mode 100644 kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/output.yaml create mode 100644 kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/policy.yaml create mode 100644 kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/test.yaml diff --git a/kustomizer/src/yaml.rs b/kustomizer/src/yaml.rs index 2ddd4d2b..dd111956 100644 --- a/kustomizer/src/yaml.rs +++ b/kustomizer/src/yaml.rs @@ -9,39 +9,63 @@ use serde::de::DeserializeOwned; /// as plain strings, avoiding the "Norway problem" and ensuring non-string map /// keys don't appear when these words are used as field names (e.g. `on:` in /// Kubernetes CRDs). -fn options() -> serde_saphyr::Options { - serde_saphyr::Options { - strict_booleans: true, - ..Default::default() - } -} +const DESER_OPTS: serde_saphyr::Options = serde_saphyr::Options { + strict_booleans: true, + budget: Some(serde_saphyr::Budget { + max_reader_input_bytes: Some(256 * 1024 * 1024), + max_events: 1_000_000, + max_aliases: 50_000, + max_anchors: 50_000, + max_depth: 2_000, + max_documents: 1_024, + max_nodes: 250_000, + max_total_scalar_bytes: 64 * 1024 * 1024, + max_merge_keys: 10_000, + enforce_alias_anchor_ratio: true, + alias_anchor_min_aliases: 100, + alias_anchor_ratio_multiplier: 10, + }), + budget_report: None, + duplicate_keys: serde_saphyr::options::DuplicateKeyPolicy::Error, + alias_limits: serde_saphyr::options::AliasLimits { + max_total_replayed_events: 1_000_000, + max_replay_stack_depth: 64, + max_alias_expansions_per_anchor: usize::MAX, + }, + legacy_octal_numbers: false, + angle_conversions: false, + ignore_binary_tag_for_string: false, + no_schema: false, + with_snippet: true, + crop_radius: 64, +}; pub fn from_str(s: &str) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_str_with_options(s, options()).map_err(Into::into) + serde_saphyr::from_str_with_options(s, DESER_OPTS).map_err(Into::into) } pub fn from_slice(s: &[u8]) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_slice_with_options(s, options()).map_err(Into::into) + serde_saphyr::from_slice_with_options(s, DESER_OPTS).map_err(Into::into) } pub fn from_reader(reader: impl Read) -> anyhow::Result where T: DeserializeOwned, { - serde_saphyr::from_reader_with_options(reader, options()).map_err(Into::into) + serde_saphyr::from_reader_with_options(reader, DESER_OPTS).map_err(Into::into) } pub fn from_reader_multi(mut reader: impl Read) -> anyhow::Result> where T: DeserializeOwned, { - serde_saphyr::read_with_options(&mut reader, options()) + serde_saphyr::read_with_options(&mut reader, DESER_OPTS) .map(|res| res.map_err(Into::into)) .collect() } diff --git a/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/kustomization.yaml b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/kustomization.yaml new file mode 100644 index 00000000..8fca817a --- /dev/null +++ b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - policy.yaml diff --git a/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/output.yaml b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/output.yaml new file mode 100644 index 00000000..8cd850f4 --- /dev/null +++ b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/output.yaml @@ -0,0 +1,30 @@ +apiVersion: pgroles.io/v1alpha1 +kind: PostgresPolicy +metadata: + name: test-policy +spec: + profiles: + editor: + grants: + - on: + type: schema + privileges: + - USAGE + - on: + type: table + name: "*" + privileges: + - SELECT + - INSERT + - UPDATE + - DELETE + roles: + - name: app_analytics + login: true + - name: some_viewer + memberships: + - role: editor + members: + - name: app_analytics + - name: some_viewer + inherit: false diff --git a/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/policy.yaml b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/policy.yaml new file mode 100644 index 00000000..98575e19 --- /dev/null +++ b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/policy.yaml @@ -0,0 +1,21 @@ +apiVersion: pgroles.io/v1alpha1 +kind: PostgresPolicy +metadata: + name: test-policy +spec: + profiles: + editor: + grants: + - on: {type: schema} + privileges: [USAGE] + - on: {type: table, name: "*"} + privileges: [SELECT, INSERT, UPDATE, DELETE] + roles: + - name: app_analytics + login: true + - name: some_viewer + memberships: + - role: editor + members: + - {name: app_analytics} + - {name: some_viewer, inherit: false} diff --git a/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/test.yaml b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/test.yaml new file mode 100644 index 00000000..d5597891 --- /dev/null +++ b/kustomizer/tests/kustomizer/testdata/regression/yaml11-boolean-key/test.yaml @@ -0,0 +1 @@ +name: yaml11-boolean-key