jkcfg / jk

Configuration as Code with ECMAScript
https://jkcfg.github.io
Apache License 2.0
404 stars 30 forks source link

Deep merge from `@jkcfg/std/merge` errors if one of the keys to merge is an empty array #318

Open shimmerjs opened 4 years ago

shimmerjs commented 4 years ago

I have really enjoyed building Kube manifests in TypeScript using jk for my home cluster and personal projects, but I think there is an issue with the deep merge function:

Bug Description

Attempting to perform a merge via deep when one of the child keys is an empty array results in the following error:

@jkcfg/std/internal/flatbuffers.js:686
        while (i < s.length) {
                     ^
TypeError: Cannot read property 'length' of undefined
    at flatbuffers.Builder.createString (@jkcfg/std/internal/flatbuffers.js:686:22)
    at write (@jkcfg/std/write.js:37:28)
    at print (@jkcfg/std/write.js:65:5)
    at /Users/shimmerjs/dev/megazord/kubernetes/dist/jkcfg-merge-bug.js:6:1

Reproduction Steps

  1. Create input-1.json:
{
  "image": "tautulli/tautulli",
  "ingress": {
    "annotations": {},
    "enabled": false,
    "tls": []
  },
  "name": "tautulli",
  "namespace": "default",
  "persistence": {
    "size": "1Gi"
  },
  "timezone": "EST5EDT"
}
  1. Create input-2.json:

{
  "ingress": {
    "annotations": {
      "cert-manager.io/cluster-issuer": "letsencrypt-prod"
    },
    "enabled": true,
    "tls": [
      {
        "hosts": [
          "tautulli.fakeurl.dev"
        ],
        "secretName": "tautulli-ingress"
      }
    ]
  },
  "persistence": {
    "storageClass": "local-path"
  }
}
  1. I ran the following (compiled) TypeScript file:
import { print } from '@jkcfg/std';
import { deep } from '@jkcfg/std/merge';
import { $INLINE_JSON } from 'ts-transformer-inline-file';

const input1 = $INLINE_JSON('./input-1.json');
const input2 = $INLINE_JSON('./input-2.json');
const result = deep(input1, input2);

print(result, {});

export default [
  {
    path: 'jkcfg-merg.json',
    value: result,
  },
];

Note that I am using the ts-transformer-inline-file transformer and the ttsc project to run tsc with plugins. Here is the resultant JS file for running without needing to set that up:

import { print } from '@jkcfg/std';
import { deep } from '@jkcfg/std/merge';
const input1 = { "image": "tautulli/tautulli", "ingress": { "annotations": {}, "enabled": false, "tls": [] }, "name": "tautulli", "namespace": "default", "persistence": { "size": "1Gi" }, "timezone": "EST5EDT" };
const input2 = { "ingress": { "annotations": { "cert-manager.io/cluster-issuer": "letsencrypt-prod" }, "enabled": true, "tls": [{ "hosts": ["tautulli.fakeurl.dev"], "secretName": "tautulli-ingress" }] }, "persistence": { "storageClass": "local-path" } };
const result = deep(input1, input2);
print(result, {});
export default [
    {
        path: 'jkcfg-merg.json',
        value: result,
    },
];
  1. Run jk generate to cause the error (ignore the file paths, I wrote the script in my kube workspace for convenience):
 jk generate -o kubernetes/generated -i kubernetes/ kubernetes/dist/jkcfg-merge-bug.js
@jkcfg/std/internal/flatbuffers.js:686
        while (i < s.length) {
                     ^
TypeError: Cannot read property 'length' of undefined
    at flatbuffers.Builder.createString (@jkcfg/std/internal/flatbuffers.js:686:22)
    at write (@jkcfg/std/write.js:37:28)
    at print (@jkcfg/std/write.js:65:5)
    at /Users/shimmerjs/dev/megazord/kubernetes/dist/jkcfg-merge-bug.js:6:1
Module (kubernetes/dist/jkcfg-merge-bug.js) has not been loaded

To double check my assumptions on what a standard deep merge would produce, I wrote the same script using lodash-es to verify the result:

import { print } from '@jkcfg/std';
import { merge } from 'lodash-es';
const input1 = { "image": "tautulli/tautulli", "ingress": { "annotations": {}, "enabled": false, "tls": [] }, "name": "tautulli", "namespace": "default", "persistence": { "size": "1Gi" }, "timezone": "EST5EDT" };
const input2 = { "ingress": { "annotations": { "cert-manager.io/cluster-issuer": "letsencrypt-prod" }, "enabled": true, "tls": [{ "hosts": ["tautulli.fakeurl.dev"], "secretName": "tautulli-ingress" }] }, "persistence": { "storageClass": "local-path" } };
const result = merge(input1, input2);
print(result, {});
export default [
    {
        path: 'lodash-merge.json',
        value: result,
    },
];

Which produced this result:

» jk generate -o kubernetes/generated -i kubernetes/ kubernetes/dist/lodash-merge.js
{
  "image": "tautulli/tautulli",
  "ingress": {
    "annotations": {
      "cert-manager.io/cluster-issuer": "letsencrypt-prod"
    },
    "enabled": true,
    "tls": [
      {
        "hosts": [
          "tautulli.fakeurl.dev"
        ],
        "secretName": "tautulli-ingress"
      }
    ]
  },
  "name": "tautulli",
  "namespace": "default",
  "persistence": {
    "size": "1Gi",
    "storageClass": "local-path"
  },
  "timezone": "EST5EDT"
}

Versions: