vmware-archive / kubecfg

A tool for managing complex enterprise Kubernetes environments as code.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

kubecfg's recursive jsonnet walk should be exposed to jsonnet somehow

anguslees opened this issue · comments

Several times I have wanted to convert kubecfg's "hierarchical" k8s resources into a simple flattened list, for use elsewhere in jsonnet (eg: to do an "add a label to all things" post-processing step).

This should be something that is easier to do than writing out a recursive jsonnet function from scratch every time.

what about a "deep map" function that can do things such "add a label to all things" but preserving the original structure?

Oh interesting. Yes, that would work too, and be more useful.

There's a big gotcha here with jsonnet (and kube.libsonnet in particular) - passing an object through a jsonnet function evaluates all the self expressions, so the nice lazy kube.libsonnet stuff can't be used to make further changes on the other side of a deepMap().

Both flatten and deepMap are quite easy to implement in jsonnet fwiw.

passing an object through a jsonnet function evaluates all the self expressions

what do you mean?

local x = {
  a: self.b + 2,
  b:: 2,
};

local f(x) = x { a: super.a * 10, c: self.b + 2 };

f(x) {
  b: 40,
}

-->

{
   "a": 420,
   "c": 42
}

There is indeed a problem with std.mapWithKey though: it does effectively manifest the object (killing all lazy expressions and also hidden fields)

A more practical example of what I'm talking about.

Given:

// mapObjects applies func on every object in a tree.
local mapObjects(obj, func=function(n, o) {}) = {
  [n]+: mapObjects(obj[n], func) + func(n, obj[n])
  for n in std.objectFields(obj)
  if std.isObject(obj[n])
};

// deepMerge applies a patch to every object matching a predicate.
local deepMerge(obj, patch, pred=std.isObject) = obj + mapObjects(obj, function(n, o) if pred(o) then patch else {});

and see how you can apply an override deeply in the structure while preserving super/self:

local tree = {
  universe: {
    life: {
      kind: 'foo',

      everything: self.b * $.params.mult,
      b:: 0,
      x:: 1,
    },
  },

  params:: {
    mult: 1,
  },
};


local isFoo(o) = std.objectHas(o, 'kind') && o.kind == 'foo';

local patch = {
  b: 20 + self.x,
};

deepMerge(tree, patch, isFoo) {
  params+: { mult: 2 },
},

would return:

{
   "universe": {
      "life": {
         "everything": 42,
         "kind": "foo"
      }
   },
}

While, using mapWithKey would "disconnect" the root object:

local patch = {
  b: 20 + self.x,
};

tree + std.mapWithKey(function(n, o) if isFoo(o) then o + patch else o, tree) + {
  params+: { mult: 2 },
}

->

{
   "universe": {
      "life": {
         "everything": 21,
         "kind": "foo"
      }
   }
}

A more detailed example in https://gist.github.com/mkmik/aa7f495541e4c883ad2426615d6e3525

I'm glad to know the late-bound-self semantics do indeed survive in more situations than I thought :)

(I've been caught out by this in the past, and I can't recall exactly when - it was long before std.mapWithKey existed, so it isn't limited to just that function. I obviously over-learned to just avoid functions 😛 )

I think a structure-preserving map is a great idea, in addition to a flatten (they're both useful, and different).

it seems that the recursive jsonnet walk fits as a kubecfg primitive (so we know it uses the very same logic used by kubecfg internally).

On the same note, perhaps the predicate that tells whether a given object is a k8s resource (and of which kind) could be exposed as a kubecfg function (I assume it's used by the aforementioned walk logic).

What about, deepMerge, should it be bundled by kubecfg or delegated to a library like https://github.com/bitnami-labs/kube-libsonnet ?

What about, deepMerge, should it be bundled by kubecfg or delegated to a library like https://github.com/bitnami-labs/kube-libsonnet ?

I don't care particularly. If we add it to kubecfg.libsonnet, then there's an implication that it will be supported going forward.

... 🤔 I think I would like to stick to "obviously correct" function signatures for kubecfg.libsonnet for now - and I think that means (in some pseudo type syntax): (Happy to bikeshed the specific names)

  • flatten(tree) -> [objects]
  • deepMap(func(obj)->obj, treeish) -> treeish
  • isK8sObject(x) -> bool

In particular, deepMerge is useful but can easily be derived from deepMap, so I don't think we should add deepMerge (yet). Oh, and I'm assuming flatten/deepMap do not recurse inside k8s objects - ie: they're explicitly just the kubecfg-encouraged jsonnet structure above/around k8s resources (except v1.List?) and not general-purpose jsonnet library routines. I think that's appropriate for kubecfg.libsonnet, but I could be convinced otherwise...

Note the above deepMap doesn't provide any context to the function. I could imagine an extended version where we also pass some representation of "where" in the tree we are currently (perhaps as an array (stack) of (parent object/array, name string/integer) pairs).

Thoughts?