Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested lists when discovering manifests #166

Merged
merged 12 commits into from
Jan 19, 2020

Conversation

dyson
Copy link
Contributor

@dyson dyson commented Jan 14, 2020

Resolves feedback in #122

This commit modifies the generation of kubernetes manifests so that we
can support lists of resources, at arbitrary levels of nesting. As an
example, where before the following jsonnet would fail:

{
  resources: [
    {
      apiVersion: "v1",
      kind: "ConfigMap",
      metadata: {
        name: "config",
      },
    }
  ],
}

We will now return a valid configmap resource.

This commit modifies the generation of kubernetes manifests so that we
can support lists of resources, at arbitrary levels of nesting. As an
example, where before the following jsonnet would fail:

    ```json
    {
      resources: [
        {
          apiVersion: "v1",
          kind: "ConfigMap",
          metadata: {
            name: "config",
          },
        }
      ],
    }
    ```

We will now return a valid configmap resource.
@sh0rez sh0rez self-assigned this Jan 14, 2020
@sh0rez sh0rez added component/kubernetes Working with Kubernetes clusters kind/bug Something isn't working labels Jan 14, 2020
Copy link
Member

@sh0rez sh0rez left a comment

Choose a reason for hiding this comment

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

Thanks for picking this up! Did a first pass, commented on everything that caught my eye.

If I were too critical somewhere, just add a comment below and we can discuss 😄

Most of this is related to refactoring mixed in a feature PR, which I am not a big fan of because it makes it harder to review. In general, we always try to touch as few lines as possible when implementing a new feature, without refactoring anything if it works without.

Once merged, we can always come back and make the code look as clean and cool as possible :D

pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile_test.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
The branch so far contains many good refactorings, however they
conflate with the purpose of the branch. Most of the changes will be
included in a further changeset to clean up the implementation.
The comment for ``walkJSON`` was incomplete. A more complete comment
has been added
The previous logic resulted in an inability to pass objects as they
were typed as ``[]interface{}``, although they were
``[]map[string]interface{}``-able.

This fixes that, and takes in all values as ``[]interface{}``, then
asserts the type, throwing ``ErrorPrimitiveReached`` if the type is
not a ``[]map[string]interface{}``.
It's required to have branches for each possible type here. Without
both ``[]interface{}`` and ``[]map[string]interface{}`` it fails to
render YAML and pass tests.
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
pkg/kubernetes/reconcile.go Outdated Show resolved Hide resolved
@sh0rez
Copy link
Member

sh0rez commented Jan 17, 2020

Thanks @lawrencejones @dyson @jackatbancast for your efforts! This already looks really good, found some nits (tests, etc). After these are addressed nothing prevents this anymore to be merged :D (finally)

jackatbancast and others added 4 commits January 18, 2020 14:22
By removing ``walkList``, and handling the recursion in-place in
``walkJSON``, we now handle multiple list types, rather than just
checking for ``[]map[string]interface{}``.
This change brings in the ``reflect`` module, which appears to be
needed to supplement the lack of generics, or advanced handling in
Go's type switches.

This is unfortunate as the functionality inside of
the ``reflect`` modules often slows down the runtime considerably,
however a similar technique is used inside of ``kubectl``, among other
CLIs. It should probably be avoided if attempting to serve lots of
clients in an available web API, however the slowdown should be
relatively minor in typical usage of tanka.

Great effort has been taken to keep the changes in the code to as low
a level as possible, whilst retaining as familiar structure as
possible.
@jackatbancast
Copy link
Contributor

@sh0rez I've added support for an indeterminate depth of arrays as suggested, as well as creating a duplicate of testDataArray with an additional level of depth at the beginning.

As mentioned in my commit messages, I've brought in the reflect module, to supplement the type switch. Unfortunately it seems that this is needed to support the indeterminate depth, but I've tried to make the implementation as clean as possible.

We should still get around to refactoring some of the code a la @dyson's changes, but for now I think this may be the smallest set of changes I could think of to add the functionality we wanted, with a suitable test.

Let me know if there are any more changes, happy to work on getting this in ASAP.

@sh0rez
Copy link
Member

sh0rez commented Jan 19, 2020

Thanks so much, this looks very good! I'd suggest a slight refactoring of tryCoerceList and walkJSON to keep their concerns separate:

From 045999cb169af3e3e4a690efa04b039e5fad44fc Mon Sep 17 00:00:00 2001
From: sh0rez <[email protected]>
Date: Sun, 19 Jan 2020 12:18:13 +0100
Subject: [PATCH] refactor(kubernetes): separate concerns of walkJSON and
 tryCoerceSlice

---
 pkg/kubernetes/reconcile.go | 51 ++++++++++++++++++++-----------------
 1 file changed, 27 insertions(+), 24 deletions(-)

diff --git a/pkg/kubernetes/reconcile.go b/pkg/kubernetes/reconcile.go
index 729fca9..fee55bc 100644
--- a/pkg/kubernetes/reconcile.go
+++ b/pkg/kubernetes/reconcile.go
@@ -71,21 +71,13 @@ func extract(deep interface{}) (map[string]manifest.Manifest, error) {
 	return extracted, nil
 }
 
-// tryCoerceSlice will test that an input is an array or a slice,
-// and then construct a slice of empty interface.
-//
-// This is useful to handle recursion in walkJSON.
-// Types not specified exactly in a type switch (such as highly nested arrays),
-// will not be matched.
-func tryCoerceSlice(input interface{}, path trace) ([]interface{}, error) {
+// tryCoerceSlice attempts to construct a []interface{} from any other kind of
+// array/slice.
+// If the argument is not a list at all, ok will be false
+func tryCoerceSlice(input interface{}, path trace) (list []interface{}, ok bool) {
 	v := reflect.ValueOf(input)
-	v.Kind()
 	if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
-		return nil, ErrorPrimitiveReached{
-			path:      path.Base(),
-			key:       path.Name(),
-			primitive: input,
-		}
+		return nil, false
 	}
 
 	l := v.Len()
@@ -95,7 +87,7 @@ func tryCoerceSlice(input interface{}, path trace) ([]interface{}, error) {
 		s[i] = v.Index(i).Interface()
 	}
 
-	return s, nil
+	return s, true
 }
 
 // walkJSON recurses into either a map or list, returning a list of all objects that look
@@ -106,24 +98,35 @@ func tryCoerceSlice(input interface{}, path trace) ([]interface{}, error) {
 // walkJSON, and then walkObj/walkList to handle the two different types of collection we
 // support.
 func walkJSON(ptr interface{}, extracted map[string]manifest.Manifest, path trace) error {
+	// check for known types
 	switch v := ptr.(type) {
 	case map[string]interface{}:
 		return walkObj(v, extracted, path)
-	case []interface{}: // Duplicated to handle multiple list types
-		for idx, value := range v {
-			err := walkJSON(value, extracted, append(path, fmt.Sprintf("[%d]", idx)))
-			if err != nil {
-				return err
-			}
+	case []interface{}:
+		return walkList(v, extracted, path)
+	}
+
+	// Lists other than []interface{} need to be handled separately
+	s, ok := tryCoerceSlice(ptr, path)
+	if !ok {
+		// object and list tried, so not a collection.
+		return ErrorPrimitiveReached{
+			path:      path.Base(),
+			key:       path.Name(),
+			primitive: ptr,
 		}
-		return nil
-	default:
-		s, err := tryCoerceSlice(ptr, path)
+	}
+	return walkJSON(s, extracted, path)
+}
+
+func walkList(list []interface{}, extracted map[string]manifest.Manifest, path trace) error {
+	for idx, value := range list {
+		err := walkJSON(value, extracted, append(path, fmt.Sprintf("[%d]", idx)))
 		if err != nil {
 			return err
 		}
-		return walkJSON(s, extracted, path)
 	}
+	return nil
 }
 
 func walkObj(obj objx.Map, extracted map[string]manifest.Manifest, path trace) error {
-- 
2.24.1

@sh0rez
Copy link
Member

sh0rez commented Jan 19, 2020

Also, could tryCoerceList be moved below walkJSON? That way it resembles the order of use in the file, so it can be read from top to bottom

This returns the error to ``walkJSON``, avoiding conflation with
``tryCoerceSlice``.
``tryCoerceSlice`` is now also below ``walkObj`` and ``walkList``, so
that the functions are defined in the order in which they are used.

Thanks for the patch @sh0rez
Copy link
Member

@sh0rez sh0rez left a comment

Choose a reason for hiding this comment

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

Wow, this turned out awesome 🚀

@sh0rez sh0rez merged commit 3903b9a into grafana:master Jan 19, 2020
@jackatbancast jackatbancast deleted the dyson-support-lists branch January 19, 2020 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component/kubernetes Working with Kubernetes clusters kind/bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants