Skip to content

Commit

Permalink
feat(kubernetes): make reconcile support arrays (#112)
Browse files Browse the repository at this point in the history
Allows users to output an array of objects from Jsonnet. Handy to use if you want to override sth on all objects, etc.
  • Loading branch information
sh0rez authored Nov 15, 2019
1 parent dcca3de commit eb64779
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 4 deletions.
104 changes: 104 additions & 0 deletions docs/jsonnet/expected-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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 `apiVersion` set.

!!! warning
This behaviour is going to change in the future, `metadata.name` will
also become required.

## 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 an
`apiVersion`.

??? Example
```json
{
"prometheus": {
"service": { // Service nested one level
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "promSvc"
}
},
"deployment": {
"apiVersion": "apps/v1", // apiVersion ..
"kind": "Deployment", // .. and kind are required
// to identify an object.
"metadata": {
"name": "prom"
}
}
},
"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
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 {
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

0 comments on commit eb64779

Please sign in to comment.