From 80cbb7a5ece27401d0501d6d18233721d4015c80 Mon Sep 17 00:00:00 2001 From: Tanmayshi Date: Tue, 8 Jul 2025 20:24:08 +0530 Subject: [PATCH 1/4] chore: replace js-yaml with yaml for YAML 1.1 compliance --- package-lock.json | 8 +++++--- package.json | 4 ++-- src/yaml.ts | 21 ++++++++++----------- src/yaml_test.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2747dfa6e9..bbee17287c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "form-data": "^4.0.0", "hpagent": "^1.2.0", "isomorphic-ws": "^5.0.0", - "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", "node-fetch": "^2.6.9", "openid-client": "^6.1.3", @@ -24,7 +23,8 @@ "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.9", - "ws": "^8.18.2" + "ws": "^8.18.2", + "yaml": "^2.8.0" }, "devDependencies": { "@eslint/js": "^9.18.0", @@ -1349,6 +1349,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/asynckit": { @@ -2659,6 +2660,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4022,7 +4024,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 173117514f..322741b006 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "form-data": "^4.0.0", "hpagent": "^1.2.0", "isomorphic-ws": "^5.0.0", - "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", "node-fetch": "^2.6.9", "openid-client": "^6.1.3", @@ -70,7 +69,8 @@ "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.9", - "ws": "^8.18.2" + "ws": "^8.18.2", + "yaml": "^2.8.0" }, "devDependencies": { "@eslint/js": "^9.18.0", diff --git a/src/yaml.ts b/src/yaml.ts index 09468a3e5d..3098b7291b 100644 --- a/src/yaml.ts +++ b/src/yaml.ts @@ -1,4 +1,4 @@ -import yaml from 'js-yaml'; +import YAML from 'yaml'; import { getSerializationType } from './util.js'; import { KubernetesObject } from './types.js'; import { ObjectSerializer } from './serializer.js'; @@ -9,13 +9,12 @@ import { ObjectSerializer } from './serializer.js'; * @param opts - Optional YAML load options. * @returns The deserialized Kubernetes object. */ -export function loadYaml(data: string, opts?: yaml.LoadOptions): T { - const yml = yaml.load(data, opts) as any as KubernetesObject; +export function loadYaml(data: string): T { + const yml = YAML.parse(data, { version: '1.1' }) as any as KubernetesObject; if (!yml) { throw new Error('Failed to load YAML'); } const type = getSerializationType(yml.apiVersion, yml.kind); - return ObjectSerializer.deserialize(yml, type) as T; } @@ -25,12 +24,12 @@ export function loadYaml(data: string, opts?: yaml.LoadOptions): T { * @param opts - Optional YAML load options. * @returns An array of deserialized Kubernetes objects. */ -export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] { - const ymls = yaml.loadAll(data, undefined, opts); - return ymls.map((yml) => { - const obj = yml as KubernetesObject; +export function loadAllYaml(data: string): any[] { + const ymls = YAML.parseAllDocuments(data, { version: '1.1' }); + return ymls.map((doc) => { + const obj = doc.toJS() as KubernetesObject; const type = getSerializationType(obj.apiVersion, obj.kind); - return ObjectSerializer.deserialize(yml, type); + return ObjectSerializer.deserialize(obj, type); }); } @@ -40,9 +39,9 @@ export function loadAllYaml(data: string, opts?: yaml.LoadOptions): any[] { * @param opts - Optional YAML dump options. * @returns The YAML string representation of the serialized Kubernetes object. */ -export function dumpYaml(object: any, opts?: yaml.DumpOptions): string { +export function dumpYaml(object: any): string { const kubeObject = object as KubernetesObject; const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind); const serialized = ObjectSerializer.serialize(kubeObject, type); - return yaml.dump(serialized, opts); + return YAML.stringify(serialized); } diff --git a/src/yaml_test.ts b/src/yaml_test.ts index 958f7ba45b..d99c08b869 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -154,4 +154,39 @@ spec: // not using strict equality as types are not matching deepEqual(actual, expected); }); + + it('should parse octal-like strings as numbers (YAML 1.1 style)', () => { + const yaml = ` + defaultMode: 0644 + fileMode: 0755 + `; + const result = loadYaml<{ + defaultMode: number; + fileMode: number; + }>(yaml); + + // 0644 (octal) = 420 decimal, 0755 = 493 + strictEqual(result.defaultMode, 420); + strictEqual(result.fileMode, 493); + }); + + it('should parse boolean-like strings as booleans (YAML 1.1 style)', () => { + const yaml = ` + enableFeature: yes + debugMode: ON + maintenance: no + safeMode: off + `; + const result = loadYaml<{ + enableFeature: boolean; + debugMode: boolean; + maintenance: boolean; + safeMode: boolean; + }>(yaml); + + strictEqual(result.enableFeature, true); + strictEqual(result.debugMode, true); + strictEqual(result.maintenance, false); + strictEqual(result.safeMode, false); + }); }); From edd426df8043b06af8e0c455bd39994e1914d8d9 Mon Sep 17 00:00:00 2001 From: Tanmayshi Date: Tue, 8 Jul 2025 22:34:04 +0530 Subject: [PATCH 2/4] chore: address reviewer feedback --- package.json | 1 - src/yaml.ts | 32 ++++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 322741b006..cd5a51ff52 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "author": "Kubernetes Authors", "license": "Apache-2.0", "dependencies": { - "@types/js-yaml": "^4.0.1", "@types/node": "^24.0.0", "@types/node-fetch": "^2.6.9", "@types/stream-buffers": "^3.0.3", diff --git a/src/yaml.ts b/src/yaml.ts index 3098b7291b..f4fbad9218 100644 --- a/src/yaml.ts +++ b/src/yaml.ts @@ -1,47 +1,47 @@ -import YAML from 'yaml'; +import yaml from 'yaml'; import { getSerializationType } from './util.js'; import { KubernetesObject } from './types.js'; import { ObjectSerializer } from './serializer.js'; /** - * Load a Kubernetes object from YAML. + * Load a single Kubernetes object from YAML. * @param data - The YAML string to load. - * @param opts - Optional YAML load options. + * @param opts - Optional YAML parse options. * @returns The deserialized Kubernetes object. */ -export function loadYaml(data: string): T { - const yml = YAML.parse(data, { version: '1.1' }) as any as KubernetesObject; +export function loadYaml(data: string, opts?: yaml.ParseOptions): T { + const yml = yaml.parse(data, { version: '1.1', ...opts }) as any as KubernetesObject; if (!yml) { - throw new Error('Failed to load YAML'); + throw new Error('Failed to load yaml'); } const type = getSerializationType(yml.apiVersion, yml.kind); return ObjectSerializer.deserialize(yml, type) as T; } /** - * Load all Kubernetes objects from YAML. + * Load all Kubernetes objects from a multi-document YAML string. * @param data - The YAML string to load. - * @param opts - Optional YAML load options. + * @param opts - Optional YAML parse options. * @returns An array of deserialized Kubernetes objects. */ -export function loadAllYaml(data: string): any[] { - const ymls = YAML.parseAllDocuments(data, { version: '1.1' }); +export function loadAllYaml(data: string, opts?: yaml.ParseOptions): KubernetesObject[] { + const ymls = yaml.parseAllDocuments(data, { version: '1.1', ...opts }); return ymls.map((doc) => { - const obj = doc.toJS() as KubernetesObject; + const obj = doc.toJSON() as KubernetesObject; const type = getSerializationType(obj.apiVersion, obj.kind); return ObjectSerializer.deserialize(obj, type); }); } /** - * Dump a Kubernetes object to YAML. + * Dump a Kubernetes object to a YAML string. * @param object - The Kubernetes object to dump. - * @param opts - Optional YAML dump options. - * @returns The YAML string representation of the serialized Kubernetes object. + * @param opts - Optional YAML stringify options. + * @returns The YAML string representation of the serialized object. */ -export function dumpYaml(object: any): string { +export function dumpYaml(object: any, opts?: yaml.ToStringOptions): string { const kubeObject = object as KubernetesObject; const type = getSerializationType(kubeObject.apiVersion, kubeObject.kind); const serialized = ObjectSerializer.serialize(kubeObject, type); - return YAML.stringify(serialized); + return yaml.stringify(serialized, opts); } From 06d57316f73f83bcb3ba777829d10338af9f0456 Mon Sep 17 00:00:00 2001 From: Tanmayshi Date: Tue, 8 Jul 2025 23:27:09 +0530 Subject: [PATCH 3/4] chore: address reviewer feedback 2 --- package-lock.json | 1 - src/yaml_test.ts | 31 ++++++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index bbee17287c..aa2c17d5d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.3.0", "license": "Apache-2.0", "dependencies": { - "@types/js-yaml": "^4.0.1", "@types/node": "^24.0.0", "@types/node-fetch": "^2.6.9", "@types/stream-buffers": "^3.0.3", diff --git a/src/yaml_test.ts b/src/yaml_test.ts index d99c08b869..587f689bdd 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert'; -import { V1CustomResourceDefinition, V1Namespace } from './api.js'; +import { V1CustomResourceDefinition, V1Namespace, V1Pod } from './api.js'; import { dumpYaml, loadAllYaml, loadYaml } from './yaml.js'; describe('yaml', () => { @@ -84,22 +84,27 @@ spec: const objects = loadAllYaml(yaml); strictEqual(objects.length, 3); - strictEqual(objects[0].kind, 'Namespace'); - strictEqual(objects[1].kind, 'Pod'); - strictEqual(objects[0].metadata.name, 'some-namespace'); - strictEqual(objects[1].metadata.name, 'some-pod'); - strictEqual(objects[1].metadata.namespace, 'some-ns'); - strictEqual(objects[2].apiVersion, 'apiextensions.k8s.io/v1'); - strictEqual(objects[2].kind, 'CustomResourceDefinition'); - strictEqual(objects[2].metadata!.name, 'my-crd.example.com'); + // Assert specific types for each object + const ns = objects[0] as V1Namespace; + const pod = objects[1] as V1Pod; + const crd = objects[2] as V1CustomResourceDefinition; + + strictEqual(ns.kind, 'Namespace'); + strictEqual(pod.kind, 'Pod'); + strictEqual(ns.metadata!.name, 'some-namespace'); + strictEqual(pod.metadata!.name, 'some-pod'); + strictEqual(pod.metadata!.namespace, 'some-ns'); + + strictEqual(crd.apiVersion, 'apiextensions.k8s.io/v1'); + strictEqual(crd.kind, 'CustomResourceDefinition'); + strictEqual(crd.metadata!.name, 'my-crd.example.com'); strictEqual( - objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'] - .x_kubernetes_int_or_string, + crd.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'].x_kubernetes_int_or_string, true, ); strictEqual( - objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][ - 'x-kubernetes-int-or-string' + crd.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][ + 'x-kubernetes-int-or-string' // This access is still on the parsed object before type mapping, hence `undefined` is correct ], undefined, ); From dddce79a59bb72f5c1b20c2d493ff4781413162d Mon Sep 17 00:00:00 2001 From: Tanmayshi Date: Wed, 9 Jul 2025 12:50:58 +0530 Subject: [PATCH 4/4] chore: address reviewer feedback 3 --- src/yaml.ts | 7 +++++-- src/yaml_test.ts | 31 +++++++++++++------------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/yaml.ts b/src/yaml.ts index f4fbad9218..2218fd47d3 100644 --- a/src/yaml.ts +++ b/src/yaml.ts @@ -9,7 +9,7 @@ import { ObjectSerializer } from './serializer.js'; * @param opts - Optional YAML parse options. * @returns The deserialized Kubernetes object. */ -export function loadYaml(data: string, opts?: yaml.ParseOptions): T { +export function loadYaml(data: string, opts?: yaml.ParseOptions & yaml.DocumentOptions): T { const yml = yaml.parse(data, { version: '1.1', ...opts }) as any as KubernetesObject; if (!yml) { throw new Error('Failed to load yaml'); @@ -24,7 +24,10 @@ export function loadYaml(data: string, opts?: yaml.ParseOptions): T { * @param opts - Optional YAML parse options. * @returns An array of deserialized Kubernetes objects. */ -export function loadAllYaml(data: string, opts?: yaml.ParseOptions): KubernetesObject[] { +export function loadAllYaml( + data: string, + opts?: yaml.ParseOptions & yaml.DocumentOptions & yaml.SchemaOptions, +): any[] { const ymls = yaml.parseAllDocuments(data, { version: '1.1', ...opts }); return ymls.map((doc) => { const obj = doc.toJSON() as KubernetesObject; diff --git a/src/yaml_test.ts b/src/yaml_test.ts index 587f689bdd..d99c08b869 100644 --- a/src/yaml_test.ts +++ b/src/yaml_test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert'; -import { V1CustomResourceDefinition, V1Namespace, V1Pod } from './api.js'; +import { V1CustomResourceDefinition, V1Namespace } from './api.js'; import { dumpYaml, loadAllYaml, loadYaml } from './yaml.js'; describe('yaml', () => { @@ -84,27 +84,22 @@ spec: const objects = loadAllYaml(yaml); strictEqual(objects.length, 3); - // Assert specific types for each object - const ns = objects[0] as V1Namespace; - const pod = objects[1] as V1Pod; - const crd = objects[2] as V1CustomResourceDefinition; - - strictEqual(ns.kind, 'Namespace'); - strictEqual(pod.kind, 'Pod'); - strictEqual(ns.metadata!.name, 'some-namespace'); - strictEqual(pod.metadata!.name, 'some-pod'); - strictEqual(pod.metadata!.namespace, 'some-ns'); - - strictEqual(crd.apiVersion, 'apiextensions.k8s.io/v1'); - strictEqual(crd.kind, 'CustomResourceDefinition'); - strictEqual(crd.metadata!.name, 'my-crd.example.com'); + strictEqual(objects[0].kind, 'Namespace'); + strictEqual(objects[1].kind, 'Pod'); + strictEqual(objects[0].metadata.name, 'some-namespace'); + strictEqual(objects[1].metadata.name, 'some-pod'); + strictEqual(objects[1].metadata.namespace, 'some-ns'); + strictEqual(objects[2].apiVersion, 'apiextensions.k8s.io/v1'); + strictEqual(objects[2].kind, 'CustomResourceDefinition'); + strictEqual(objects[2].metadata!.name, 'my-crd.example.com'); strictEqual( - crd.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'].x_kubernetes_int_or_string, + objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'] + .x_kubernetes_int_or_string, true, ); strictEqual( - crd.spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][ - 'x-kubernetes-int-or-string' // This access is still on the parsed object before type mapping, hence `undefined` is correct + objects[2].spec.versions[0]!.schema!.openAPIV3Schema!.properties!['foobar'][ + 'x-kubernetes-int-or-string' ], undefined, );