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

feat(kubernetes): make reconcile support arrays #112

Merged
merged 4 commits into from
Nov 15, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/jsonnet/expected-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Expected structure

Tanka evaluates the `main.jsonnet` file of your [Environment](/environments) and
filters the output (either Object or Array) for valid Kubernetes objects.
An object is considered valid if it has both, a `kind` and a `metadata.name` set.
Copy link
Contributor

@lawrencejones lawrencejones Nov 15, 2019

Choose a reason for hiding this comment

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

It's usually the case that Kubernetes deals in GVK (Group-Version-Kind) + Name tuples when uniquely identifying resources. It's probably better for tanka to output any leaf node that exhibits the structure:

// Assuming github.com/stretchr/objx
obj.Get("apiVersion").IsStr() &&
  obj.Get("kind").IsStr() &&
  obj.Get("metadata.name").IsStr()

I believe anything other than this format will fail validation anyway.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @lawrencejones but also this remark seems inaccurate. reconcile.go seems to only be logging for kind and apiVersion right now. We should be checking for all three and document them here.


## Deeply nested object (Recommended)
The most commonly used structure is a single big object that includes all of
your configs to be applied to the cluster nested under keys.
How deeply encapsulated the actual object is does not matter, Tanka will
traverse down until it finds something that has both, a `kind` and a
`metadata.name`.

??? Example
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
```json
{
"prometheus": {
"service": { // Service nested one level
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "promSvc"
}
},
"deployment": {
"apiVersion": "apps/v1",
"kind": "Deployment", // kind ..
"metadata": {
"name": "prom" // .. and metadata.name are required
// to indentify a valid object.
}
}
},
"web": {
"nginx": {
"deployment": { // Deployment nested two levels
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "nginx"
}
}
}
}
}
```

Using this technique has the big benefit that it is self-documentary, as the
nesting of keys can be used to logically group related manifests, for example by
application.

!!! info
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
It is also valid to use an encapsulation level of zero, which means
just a regular object like it could be obtained from `kubectl show -o json`:
```json
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "foo"
}
}
```


## Array
Using an array of objects is also fine:
```json
[
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "promSvc"
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "prom"
}
}
]
```

### `List` type
Users of `kubectl` might have had contact with a type called `List`. It is not
part of the official Kubernetes API but rather a pseudo-type introduced by
`kubectl` for dealing with multiple objects at once. Thus, Tanka does not
support it out of the box.

To take full advantage of Tankas features, you can manually flatten it:

```jsonnet
local list = import "list.libsonnet";

# expose the `items` array on the top level:
list.items
```
12 changes: 12 additions & 0 deletions pkg/kubernetes/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,15 @@ func testDataDeep() testData {
},
}
}

// testDataArray is an array of (deeply nested) dicts that should be fully
// flattened
func testDataArray() testData {
return testData{
deep: append([]map[string]interface{}{
testDataDeep().deep.(map[string]interface{}),
}, testDataFlat().deep.(map[string]interface{})),

flat: append(testDataDeep().flat.([]map[string]interface{}), testDataFlat().flat.([]map[string]interface{})...),
}
}
29 changes: 26 additions & 3 deletions pkg/kubernetes/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,37 @@ func (e ErrorPrimitiveReached) Error() string {
}

// walkJSON traverses deeply nested kubernetes manifest and extracts them into a flat []dict.
func walkJSON(deep map[string]interface{}, path string) ([]map[string]interface{}, error) {
func walkJSON(rawDeep interface{}, path string) ([]map[string]interface{}, error) {
flat := make([]map[string]interface{}, 0)

// array: walkJSON for each
if d, ok := rawDeep.([]map[string]interface{}); ok {
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
for i, j := range d {
out, err := walkJSON(j, fmt.Sprintf("%s[%v]", path, i))
if err != nil {
return nil, err
}
flat = append(flat, out...)
}
return flat, nil
}

// assert for map[string]interface{} (also aliased objx.Map)
if m, ok := rawDeep.(objx.Map); ok {
rawDeep = map[string]interface{}(m)
}
deep, ok := rawDeep.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("deep has unexpected type %T @ %s", deep, path)
}

// already flat?
r := objx.New(deep)
if r.Has("apiVersion") && r.Has("kind") {
return []map[string]interface{}{deep}, nil
}

flat := make([]map[string]interface{}, 0)

// walk it
for n, d := range deep {
if n == "__ksonnet" {
continue
Expand Down
6 changes: 5 additions & 1 deletion pkg/kubernetes/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ func TestWalkJSON(t *testing.T) {
name: "deep",
data: testDataDeep(),
},
{
name: "array",
data: testDataArray(),
},
}

for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
got, err := walkJSON(c.data.deep.(map[string]interface{}), "")
got, err := walkJSON(c.data.deep, "")
require.Equal(t, c.err, err)
assert.ElementsMatch(t, c.data.flat, got)
})
Expand Down