1 of 68

Knot8 (Notate)

Tune K8s manifests with lenses

mkm@influxdata.com

2 of 68

Simplicity

kubectl apply -f https://github.com/ac/me/releases/download/v1/app.yaml

3 of 68

4 of 68

Current "Package managers"

Often conflate how to:

  • discover
  • obtain
  • deploy
  • adapt
  • update (detect and apply)

5 of 68

Current "Package managers"

Often conflate how to:

  • discover
  • obtain
  • deploy
  • adapt
  • update (detect and apply)

6 of 68

The future is already here — it's just not very evenly distributed.

- William Gibson

7 of 68

Good old templating

#@ load("@ytt:data", "data")

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: #@ data.values.foo

8 of 68

Good old templating

$ cat values.yaml

foo: meow

$ ytt -f .

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: meow

9 of 68

Templating is viral

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: {{ template "wordpress.image" . }}

selector:

matchLabels:

app: demo

10 of 68

Hidden complexity

{{- define "wordpress.image" -}}

{{- $registryName := .Values.image.registry -}}

{{- $repositoryName := .Values.image.repository -}}

{{- $tag := .Values.image.tag | toString -}}

{{- if .Values.global }}

{{- if .Values.global.imageRegistry }}

{{- printf "%s/%s:%s" .Values.global.imageRegistry $repositoryName $tag -}}

{{- else -}}

{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}

{{- end -}}

{{- else -}}

{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}

{{- end -}}

{{- end -}}

11 of 68

Templating woes

  • my template engine != your template engine
  • you need a tool even if you just use defaults
  • ...

12 of 68

13 of 68

Fields

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: meow # See ^^^ field.knot8.io/foo

14 of 68

Fields

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: meow # See ^^^ field.knot8.io/foo

15 of 68

knot8 set <app.yaml foo=woof

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: woof # See ^^^ field.knot8.io/foo

16 of 68

Find the right item

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/appImage

selector:

matchLabels:

app: demo

17 of 68

Find the right item

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/image

selector:

matchLabels:

app: demo

18 of 68

knot8 set <app.yaml appImage=myrepo/debian:10

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: myrepo/debian:10 # See ^^^ field.knot8.io/Image

selector:

matchLabels:

app: demo

19 of 68

Inner formats

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

data:

foo: |

some:

app:

config:

bar: 42

20 of 68

Inner formats

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

data:

foo: |

some:

app:

config:

bar: 42

?

?

?

21 of 68

Inner formats

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

data:

foo: |

some:

app:

config:

bar: 42

?

?

?

!!

22 of 68

Inner formats: through the looking glass

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar

data:

foo: |

some:

app:

config:

bar: 42

23 of 68

Inner formats: through the looking glass

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar

data:

foo: |

some:

app:

config:

bar: 42

24 of 68

Inner formats: Through the looking glass lens

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar

data:

foo: |

some:

app:

config:

bar: 42

25 of 68

Through the lens

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar

data:

foo: |

some:

app:

config:

bar: 42

26 of 68

Through the lens: same thing, just a level deeper

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar

data:

foo: |

some:

app:

config:

bar: 42

27 of 68

It's turtles trees all the way down

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo/~(yaml)/some/app/config/bar/~(toml)/section/subsection/key

data:

foo: |

some:

app:

config:

bar: |

foo = bar

[section.subsection]

key = 42

28 of 68

Real world example

apiVersion: v1

kind: ConfigMap

metadata:

name: demo

annotations:

field.knot8.io/cluster: /data/cfg/~(yaml)/relabel_configs/~{"target_label":"cluster"}/replacement

data:

cfg: |

relabel_configs:

- source_labels: [

"__meta_kubernetes_namespace",

"__meta_kubernetes_service_name",

"__meta_kubernetes_endpoint_port_name",

]

action: keep

regex: default;kubernetes;https

- target_label: cluster

replacement: main

29 of 68

Image references as inner formats

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image

field.knot8.io/appVersion: image/~(oci)/tag

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/image

selector:

matchLabels:

app: demo

30 of 68

Image references as inner formats

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image

field.knot8.io/appVersion: appImage/~(oci)/tag

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/appImage

selector:

matchLabels:

app: demo

31 of 68

Lenses

32 of 68

Non-goals (When is knot8 not applicable)

  • when you want resources to appear or disappear on a condition
    • e.g. optional ingress resource
  • it doesn't help you craft manifests
    • e.g. with repetitive blocks etc

33 of 68

Workflows

  • GitOps
  • Fire & forget ...

34 of 68

Workflows

  • GitOps
  • Fire & forget ...
  • ... & repent
    • kubectl get deploy/foo -oyaml | knot8 set -v bar=123 | kubectl apply -f -

35 of 68

GitOps

  • Plug&play
    • DEV:
    • vim prod/values.yaml && git commit ...
    • CI:
    • cat app.yaml | knot8 set --from prod/values.yaml | othertools | kubectl ...
  • In-place
    • DEV:
    • knot8 set -f app.yaml && git commit
    • CI:
    • cat app.yaml | othertools | kubectl ...

36 of 68

Wait, what? why would you? that's wrong

37 of 68

How I learned stop worrying and love the files

38 of 68

3-way merge

  • knot8 pull -f app.yaml https://acme.com/releases/v1.2.0/app.yaml
  • knot8 pull -f app.yaml oci://acme/app:1.2.0

39 of 68

3-way merge

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/original: |

foo: meow

bar: "1"

data:

foo: meow

bar: "1"

40 of 68

knot8 set -f app.yaml foo=woof

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/original: |

foo: meow

bar: "1"

data:

foo: woof

bar: "1"

41 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/original: |

foo: meow

bar: "1"

data:

foo: woof

bar: "1"

apiVersion: v1

kind: ConfigMap

metadata:

name: bettername

annotations:

field.knot8.io/foo: /data/cfg/~(toml)/foo

field.knot8.io/bar: /data/cfg/~(toml)/bar

knot8.io/original: |

foo: miau

bar: "42"

data:

cfg: |

bar = 42

foo = miau

apiVersion: v1

kind: ConfigMap

metadata:

name: bettername

annotations:

field.knot8.io/foo: /data/cfg/~(toml)/foo

field.knot8.io/bar: /data/cfg/~(toml)/bar

knot8.io/original: |

foo: miau

bar: "42"

data:

cfg: |

bar = 42

foo = woof

Current

New upstream

Merged

42 of 68

knot8 set -f app.yaml appImage/repo=myrepo/foapp

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage: fooapp:1.0

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:1.0

43 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImageName: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/name

field.knot8.io/appImageTag: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/tag

knot8.io/original: |

appImageName: fooapp

appImageTag: "1.0"

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:1.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImageName: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/name

field.knot8.io/appImageTag: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/tag

knot8.io/original: |

appImageName: fooapp

appImageTag: "2.0"

spec:

template:

spec:

containers:

- name: app

image: fooapp:2.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImageName: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/name

field.knot8.io/appImageTag: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)/tag

knot8.io/original: |

appImageName: fooapp

appImageTag: "2.0"

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:2.0

Current

New upstream

Merged

44 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage: fooapp:1.0

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:1.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage: fooapp:2.0

spec:

template:

spec:

containers:

- name: app

image: fooapp:2.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage: fooapp:2.0

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:2.0

Current

New upstream

Merged

45 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage/*: fooapp:1.0

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:1.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage: fooapp:2.0

spec:

template:

spec:

containers:

- name: app

image: fooapp:2.0

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

knot8.io/original: |

appImage/*: fooapp:2.0

spec:

template:

spec:

containers:

- name: app

image: myrepo/fooapp:1.0

Current

New upstream

Merged

46 of 68

47 of 68

Prior art

  • Lenses:
    • Tons of research in "bidirectional parsing" or "unparsers" in the FP ivory tower
    • Boomerang (project harmony)
    • Augeas
  • In-place editing workflow:

48 of 68

POC?

  • https://github.com/mkmik/knot8

49 of 68

Questions?

50 of 68

Fanout

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

data:

foo: meow # See ^^^ field.knot8.io/foo

---

apiVersion: v1

kind: Secret

metadata:

name: demozzz

annotations:

field.knot8.io/foo: /data/bar/~(base64)

data:

bar: bWVvdw==

51 of 68

Outputs

  • In-place modification
  • Overlays that play well with other tools: (kustomize, ytt, kubecfg, ...)
  • Templates to be used as base for other tools (ytt, kubecfg, ...)

52 of 68

Templates output: jsonnet

$ knot8 convert -f app.yaml -ojsonnet

{

fields:: {

foo: "meow",

},

apiVersion: "v1",

kind: "ConfigMap",

metadata: {

name: "demo2",

annotations: {

"field.knot8.io/foo": "/data/foo",

},

},

data: {

foo: $.fields.foo,

},

}

53 of 68

Templates output: ytt

$ knot8 convert -f app.yaml -oytt=config.yaml,values.yaml

$ cat config.yaml

#@ load("@ytt:data", "data")

apiVersion: v1

kind: ConfigMap

metadata:

name: demo

annotations:

field.knot8.io/foo: /data/foo

data:

foo: #@ data.values.fields.foo

$ cat values.yaml

#@data/values

---

fields:

foo: bar

$ ytt -f .

54 of 68

Templates output: helm

left as an exercise for the reader

55 of 68

Outputs overlays

$ knot8 set -f app.yaml -o overlay appImage=acme:1.2

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

spec:

template:

spec:

containers:

- image: acme:1.2

56 of 68

Outputs overlays

$ knot8 set -f app.yaml -o jsonnet appImage=acme:1.2

{

spec: {

template: {

spec: {

containers: [

{

image: 'acme:1.2',

},

],

},

},

},

}

57 of 68

Why not overlays?

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

spec:

template:

spec:

containers:

- image: debian:10

name: app

58 of 68

Documentation: for humans and ...

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/meta:

foo:

doc: Foo controls a key frobnication parameter.

bar:

doc: Bar strongly affects frobnication efficiency.

data:

foo: meow

bar: "1"

59 of 68

... for machines

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/meta:

foo:

doc: Foo controls a key frobnication parameter.

bar:

doc: Bar strongly affects frobnication efficiency.

type: int

data:

foo: meow

bar: "1"

60 of 68

"Typed" fields

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/image

selector:

matchLabels:

app: demo

61 of 68

knot8 set -f app.yaml appImage/version=9

apiVersion: apps/v1

kind: Deployment

metadata:

name: demo

annotations:

field.knot8.io/appImage: /spec/template/spec/containers/~{"name":"app"}/image/~(oci)

spec:

template:

metadata:

labels:

app: demo

spec:

containers:

- name: app

image: debian:10 # See ^^^ field.knot8.io/image

selector:

matchLabels:

app: demo

62 of 68

Schema

$ knot8 info -f app.yaml

appImage

foo

$ knot8 info -f app.yaml --targets

appImage

/items/~{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"demo"}}

/spec/template/spec/containers/~{"name":"app"}/image

foo

/items/~{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"demo2"}}

/data/foo/~(yaml)/some/app/config/bar

/items/~{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"demo"}}

/spec/template/spec/containers/~{"name":"app"}/env/~{"name":"FOO"}/value

$ knot8 info -f app.yaml --targets --flat

appImage /items/~{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"demo"}}/spec/template/spec/containers/~{"name":"app"}/…

foo /items/~{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"demo2"}}/data/foo/~(yaml)/some/app/config/bar

foo /items/~{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"demo"}}/spec/template/spec/containers/~{"name":"app"}/env/

$ knot8 info -f app.yaml --lenses

yaml

toml

oci

...

$ knot8 set -f app.yaml appImage/tag=9

63 of 68

Schema cont'd

$ knot8 info -f app.yaml --lenses --openapi

oci

#/definitions/io.k8s.api.core.v1.Container/properties/image

#/definitions/io.k8s.api.core.v1.EphemeralContainer/properties/image

base64

#/definitions/io.k8s.api.core.v1.Secret/properties/data

#/definitions/io.k8s.api.certificates.v1beta1.CertificateSigningRequestSpec/properties/request

AppImage points to a field covered by the openapi schema property "image" declared in the io.k8s.api.core.v1.Container def

and thus we know that by default it can be viewed through the oci lens

$ knot8 set -f app.yaml appImage/tag=9

$ knot8 set -f secret.yaml /data/foo/~(base64)=bar

64 of 68

Schema cont'd 2

$ knot8 info -f app.yaml --lenses --annotations

nginxConfig

/items/~{}

/metadata/annotations/nginx.ingress.kubernetes.io~1configuration-snippet

Well-known annotations can be operated on without explicit fields:

$ knot8 set -f app.yaml /metadata/annotations/nginx.ingress.kubernetes.io~1configuration-snippet/error_page/404=my404.html

65 of 68

Operations on trees

  • Objects: set fields, remove fields
  • Arrays: insert, remove, replace
  • Strings: regexp replace

66 of 68

Lens examples

$ knot8 set -f app.yaml /spec/template/spec/containers/0/args/~(cmdline:optparse)/foo=wow

$ git diff deployment.yaml

args: [

"-v",

"--baz=quz"

- "--foo=bar",

+ "--foo=wow",

"blah.txt"

]

$ git diff deployment.yaml

args: [

"-v",

"--baz=quz"

"--foo"

- "bar",

+ "wow",

"blah.txt"

]

67 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/original: |

foo: meow

bar: "1"

data:

foo: woof

bar: "1"

apiVersion: v1

kind: ConfigMap

metadata:

name: bettername

annotations:

field.knot8.io/foo: /data/cfg/~(toml)/foo

knot8.io/original: |

foo: miau

data:

cfg: |

foo = miau

apiVersion: v1

kind: ConfigMap

metadata:

name: bettername

annotations:

field.knot8.io/foo: /data/cfg/~(toml)/foo

field.knot8.io/bar: /data/cfg/~(toml)/bar

knot8.io/original: |

foo: miau

data:

cfg: |

foo = woof

Current

New upstream

Merged

68 of 68

knot8 pull -f app.yaml https://…/v2/app.yaml

apiVersion: v1

kind: ConfigMap

metadata:

name: demo2

annotations:

field.knot8.io/foo: /data/foo

field.knot8.io/bar: /data/bar

knot8.io/original: |

foo: meow

bar: "1"

data:

foo: woof

bar: "2"

apiVersion: v1

kind: ConfigMap

metadata:

name: bettername

annotations:

field.knot8.io/foo: /data/cfg/~(toml)/foo

knot8.io/original: |

foo: miau

data:

cfg: |

foo = miau

error (warning?), unknown field bar

Current

New upstream

Merged