├── Makefile ├── README.md ├── TODO.md ├── _gen ├── about.md ├── index.md ├── lesson1.md ├── lesson2.md ├── lesson3.md ├── lesson4.md ├── lesson5.md └── lesson6.md ├── coursonnet.libsonnet ├── docker ├── Dockerfile ├── README.md └── run.sh ├── docs ├── .nojekyll ├── about.html ├── assets │ ├── anchorlinks.js │ ├── prettyprint.js │ ├── style.css │ └── toc.js ├── index.html ├── lesson1.html ├── lesson2.html ├── lesson3.html ├── lesson4.html ├── lesson5.html └── lesson6.html ├── html ├── assets │ ├── anchorlinks.js │ ├── prettyprint.js │ ├── style.css │ └── toc.js ├── html.jsonnet ├── run.sh └── template.html ├── lessons ├── about.jsonnet ├── about.md ├── index.jsonnet ├── index.md ├── lesson1 │ ├── example1.jsonnet │ ├── example2.jsonnet │ ├── example3.jsonnet │ ├── example4.jsonnet │ ├── example5.jsonnet │ ├── example6.jsonnet │ ├── example7.jsonnet │ ├── examples.jsonnet │ ├── lesson.md │ ├── main.jsonnet │ ├── pitfall1.jsonnet │ ├── pitfall2.jsonnet │ ├── pitfall3.jsonnet │ └── pitfall4.jsonnet ├── lesson2 │ ├── example1 │ │ └── jsonnetfile.json │ ├── example2 │ │ ├── jsonnetfile.json │ │ ├── jsonnetfile.lock.json │ │ └── vendor │ │ │ ├── github.com │ │ │ └── jsonnet-libs │ │ │ │ └── xtd │ │ │ │ ├── .gitignore │ │ │ │ ├── LICENSE │ │ │ │ ├── Makefile │ │ │ │ ├── README.md │ │ │ │ ├── ascii.libsonnet │ │ │ │ ├── camelcase.libsonnet │ │ │ │ ├── docs │ │ │ │ ├── .gitignore │ │ │ │ ├── Gemfile │ │ │ │ ├── README.md │ │ │ │ ├── _config.yml │ │ │ │ ├── ascii.md │ │ │ │ ├── camelcase.md │ │ │ │ ├── inspect.md │ │ │ │ └── url.md │ │ │ │ ├── inspect.libsonnet │ │ │ │ ├── main.libsonnet │ │ │ │ ├── test.jsonnet │ │ │ │ └── url.libsonnet │ │ │ └── xtd │ ├── example3 │ │ ├── .gitignore │ │ └── jsonnetfile.json │ ├── example4 │ │ ├── .gitignore │ │ └── jsonnetfile.json │ ├── example5 │ │ ├── .gitignore │ │ ├── jsonnetfile.json │ │ ├── lib │ │ │ └── istiolib.libsonnet │ │ ├── usage1.jsonnet │ │ ├── usage2.jsonnet │ │ ├── usage3.jsonnet │ │ └── usage4.jsonnet │ ├── examples.jsonnet │ ├── lesson.md │ └── main.jsonnet ├── lesson3 │ ├── example1.jsonnet │ ├── example2 │ │ ├── .gitignore │ │ ├── example1.jsonnet │ │ ├── example2.jsonnet │ │ ├── example3.jsonnet │ │ ├── example4.jsonnet │ │ ├── example5.jsonnet │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ ├── k.libsonnet │ │ │ └── webserver │ │ │ └── main.libsonnet │ ├── examples.jsonnet │ ├── lesson.md │ └── main.jsonnet ├── lesson4 │ ├── example1 │ │ ├── .gitignore │ │ ├── example1.jsonnet │ │ ├── example2.jsonnet │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ ├── k.libsonnet │ │ │ └── privatebin │ │ │ └── main.libsonnet │ ├── example2 │ │ ├── .gitignore │ │ ├── example1.jsonnet │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ ├── k.libsonnet │ │ │ └── privatebin │ │ │ └── main.libsonnet │ ├── example3 │ │ ├── .gitignore │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ └── webserver │ ├── examples.jsonnet │ ├── lesson.md │ ├── main.jsonnet │ └── usecase-pentagon │ │ ├── .gitignore │ │ ├── example1.jsonnet │ │ ├── example2.jsonnet │ │ ├── jsonnetfile.json │ │ └── lib │ │ ├── k.libsonnet │ │ └── pentagon │ │ ├── example1.libsonnet │ │ └── example2.libsonnet ├── lesson5 │ ├── example1 │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── docs │ │ │ └── README.md │ │ ├── example1.jsonnet │ │ ├── example2.jsonnet │ │ ├── example3.jsonnet │ │ ├── example4.jsonnet │ │ ├── example5.jsonnet │ │ ├── example7.jsonnet │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ └── k.libsonnet │ ├── examples.jsonnet │ ├── lesson.md │ └── main.jsonnet ├── lesson6 │ ├── example1 │ │ ├── .gitignore │ │ ├── base.json │ │ ├── example0.jsonnet │ │ ├── example1.jsonnet │ │ ├── example1.jsonnet.output │ │ ├── example1.output │ │ ├── example2.jsonnet │ │ ├── example2.jsonnet.output │ │ ├── example3.jsonnet │ │ ├── example3.jsonnet.output │ │ ├── example4.jsonnet │ │ ├── example4.jsonnet.output │ │ ├── example5.jsonnet │ │ ├── example5.jsonnet.output │ │ ├── example6.jsonnet │ │ ├── example6.jsonnet.output │ │ ├── example7.jsonnet │ │ ├── example7.jsonnet.output │ │ ├── jsonnetfile.json │ │ ├── lib │ │ │ ├── k.libsonnet │ │ │ └── webserver │ │ │ │ ├── correct.libsonnet │ │ │ │ ├── main.libsonnet │ │ │ │ ├── wrong1.libsonnet │ │ │ │ ├── wrong2.libsonnet │ │ │ │ └── wrong3.libsonnet │ │ ├── pitfall1.jsonnet │ │ ├── pitfall1.jsonnet.output │ │ ├── pitfall2.jsonnet │ │ ├── pitfall2.jsonnet.output │ │ ├── pitfall3.jsonnet │ │ └── pitfall3.jsonnet.output │ ├── example2 │ │ ├── .gitignore │ │ ├── jsonnetfile.json │ │ └── lib │ │ │ ├── k.libsonnet │ │ │ └── webserver │ │ │ ├── 2 │ │ │ ├── Makefile │ │ │ ├── main.libsonnet │ │ │ ├── make_test.output │ │ │ └── test │ │ │ ├── .gitignore │ │ │ ├── base.json │ │ │ ├── jsonnetfile.json │ │ │ ├── lib │ │ │ └── k.libsonnet │ │ │ └── main.libsonnet │ ├── examples.jsonnet │ ├── lesson.md │ └── main.jsonnet └── lessons.jsonnet └── main.jsonnet /Makefile: -------------------------------------------------------------------------------- 1 | default: generate html 2 | 3 | generate: 4 | @rm -rf _gen && mkdir -p _gen && \ 5 | jsonnet -J . -m _gen -S main.jsonnet 6 | 7 | html: 8 | @./html/run.sh docs 9 | 10 | generate: lessons/lesson1/examples.jsonnet 11 | lessons/lesson1/examples.jsonnet: 12 | @echo "Generating lessons/lesson1/examples.jsonnet..." 13 | @cd lessons/lesson1 && \ 14 | echo "local example = (import 'coursonnet.libsonnet').example;" > \ 15 | examples.jsonnet && \ 16 | echo "[" >> examples.jsonnet && \ 17 | find . -type f -name \*.jsonnet | \ 18 | grep -v main.jsonnet | \ 19 | grep -v examples.jsonnet | \ 20 | sort | \ 21 | xargs --replace echo " example.new('{}'[2:], importstr '{}', import '{}')+example.withLink()," >> \ 22 | examples.jsonnet && \ 23 | echo "]" >> examples.jsonnet 24 | 25 | generate: lessons/lesson5/examples.jsonnet 26 | lessons/lesson5/examples.jsonnet: 27 | @echo "Generating lessons/lesson5/examples.jsonnet..." 28 | @cd lessons/lesson5 && \ 29 | echo "local example = (import 'coursonnet.libsonnet').example;" > \ 30 | examples.jsonnet && \ 31 | echo "[" >> examples.jsonnet && \ 32 | find ./example1 -type f -name example\*.jsonnet | \ 33 | sort | \ 34 | xargs --replace echo " example.new('{}'[2:], importstr '{}', import '{}')," >> \ 35 | examples.jsonnet && \ 36 | echo "]" >> examples.jsonnet 37 | 38 | generate: lessons/lesson5/example1/docs/README.md 39 | lessons/lesson5/example1/docs/README.md: 40 | @cd lessons/lesson5/example1 && \ 41 | make docs 42 | 43 | generate: lessons/lesson6/example1/example*.jsonnet.output 44 | generate: lessons/lesson6/example1/pitfall*.jsonnet.output 45 | lessons/lesson6/example1/%.jsonnet.output: 46 | @echo "Generating lessons/lesson6/example1/$*.jsonnet.output..." 47 | @cd lessons/lesson6/example1 && \ 48 | echo "# jsonnet -J lib -J vendor $*.jsonnet" > $*.jsonnet.output && \ 49 | jsonnet -J lib -J vendor $*.jsonnet 1>&2 &>> $*.jsonnet.output || true 50 | 51 | generate: lessons/lesson6/example2/lib/webserver/make_test.output 52 | lessons/lesson6/example2/lib/webserver/make_test.output: 53 | @cd lessons/lesson6/example2/lib/webserver && \ 54 | echo "# make test" > make_test.output && \ 55 | make --no-print-directory test 1>&2 &>> make_test.output 56 | 57 | generate: lessons/lesson6/examples.jsonnet 58 | lessons/lesson6/examples.jsonnet: 59 | @echo "Generating lessons/lesson6/examples.jsonnet..." 60 | @cd lessons/lesson6 && \ 61 | echo "local example = (import 'coursonnet.libsonnet').example;" > \ 62 | examples.jsonnet && \ 63 | echo "[" >> examples.jsonnet && \ 64 | ls ./example1/*.jsonnet | grep '\(example\|pitfall\)*.jsonnet' | \ 65 | sort | \ 66 | xargs --replace echo " example.new('{}'[2:], importstr '{}', import '{}')," >> \ 67 | examples.jsonnet && \ 68 | ls ./example1/lib/webserver/*.libsonnet | \ 69 | sort | \ 70 | xargs --replace echo " example.new('{}'[2:], importstr '{}', import '{}')," >> \ 71 | examples.jsonnet && \ 72 | ls ./example1/base.json | \ 73 | sort | \ 74 | xargs --replace echo " example.new('{}'[2:], importstr '{}', import '{}')," >> \ 75 | examples.jsonnet && \ 76 | ls ./example1/*.jsonnet.output | grep '\(example\|pitfall\)*.jsonnet.output' | \ 77 | sort | \ 78 | xargs --replace echo " example.new('{}'[2:], importstr '{}', {filename:'{}'})," >> \ 79 | examples.jsonnet && \ 80 | find ./example2/lib/webserver -name 'vendor' -prune -o -type f -print | \ 81 | sort | \ 82 | xargs --replace echo " example.new('{}'[2:], importstr '{}', {filename:'{}'})," >> \ 83 | examples.jsonnet && \ 84 | echo "]" >> examples.jsonnet 85 | 86 | test: lessons/lesson1/examples.jsonnet 87 | @jsonnet -J . lessons/lesson1/examples.jsonnet > /dev/null && \ 88 | echo "Success!" 89 | 90 | generate: lessons/lessons.jsonnet 91 | lessons/lessons.jsonnet: 92 | @echo "Generating lessons/lessons.jsonnet..." 93 | @cd lessons && \ 94 | echo "[" > lessons.jsonnet && \ 95 | find ./*/ -type f -name main.jsonnet | \ 96 | sort | \ 97 | xargs --replace echo " (import '{}')," >> \ 98 | lessons.jsonnet && \ 99 | echo "]" >> lessons.jsonnet 100 | 101 | .PHONY: generate html test 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jsonnet Training Course 2 | 3 | [HTML version](https://jsonnet-libs.github.io/jsonnet-training-course/) 4 | 5 | [Markdown version](_gen/index.md) 6 | 7 | ## Building 8 | 9 | Build requirements: 10 | 11 | - jsonnet 12 | - md2html (from [md4c](https://github.com/mity/md4c)) 13 | - bash 14 | 15 | To build both the markdown as well as the html version: 16 | 17 | ``` 18 | make -B 19 | ``` 20 | 21 | [docker/](docker/) has an image to do this in docker. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO notes 2 | 3 | Answer questions tl;dr, make reference to longer descriptions 4 | 5 | Why? 6 | 7 | - Why jsonnet? What benefit do we gain from it? 8 | - Why is it worth the inconvenience of people having to learn a new language/paradigm? 9 | - Why we use jsonnet (vs other solutions/tools)? 10 | - Consider creating an extensive retrospective design doc/blog post 11 | 12 | How? (kind of covered in index page and subsequent lessons) 13 | 14 | - What are the lessons we’ve learned after using jsonnet for years? 15 | - What are some GrafanaLabs jsonnet idioms that we try to use? 16 | - Tools/tricks when writing/debugging jsonnet? 17 | 18 | Ideas: 19 | - Consider Jsonnet introduction lesson 20 | - [IDE configuration](https://docs.google.com/spreadsheets/d/10pTqNvOC-0pDhgP3dYwjM6ywWVEyO0wnfl__ewfQa2Y/edit), guest lessons/breakouts (vim, emacs, neovim, VSCode, IntelliJ) 21 | - Consider Terminology list 22 | - Provide good-read list 23 | - CD process (kube-manifests, jsonnet-libs/k8s github workflow) 24 | 25 | Lessons: 26 | 27 | - Write a reusable library 28 | - Properly use keywords such as `self`, `$`, `local`, `super`, `null` 29 | - Write for extensibility with `::` and objects rather than arrays 30 | - Write object-oriented with 'mixin' functions 31 | - Apply YAGNI often 32 | - Know how to avoid common pitfalls: 33 | - Builder pattern 34 | - "private" variables 35 | 36 | - Don't write libraries 37 | - Find existing libraries 38 | - Vendor libraries with `jsonnet-bundler` 39 | - Use a vendored library with `JSONNET_PATH` 40 | 41 | - Exercise: rewrite reusable library with `k8s-libsonnet` 42 | - Vendoring `k8s-libsonnet` with `jsonnet-bundler` 43 | - Understand `k.libsonnet` convention 44 | - Use generated documentation 45 | 46 | - Developing libraries 47 | - Wrapping libraries 48 | - Pentagon usecase 49 | - Grafonnet usecase 50 | - Developing with upstream libraries 51 | 52 | - Documentation & testing 53 | - Use of `docsonnet` 54 | - Test with the use of `error`, `null` and `prune` 55 | - Investigate testing framework (`jsonnetunit` seems dead, fork?) 56 | 57 | - Jsonnet use cases 58 | - Roll out in 'waves' (dev/stag/prod) 59 | - Come up with a few examples (Grafonnet, GitHub Actions) 60 | - Write YAML/JSON files (Grafonnet, GitHub Actions) 61 | - Generate new libraries from specifications (again) 62 | 63 | - Jsonnet features/pitfalls 64 | - `prune` is recursive 65 | - Provide deprecation notices with `std.trace` 66 | - Handle dynamic inputs with top-level arguments and `/dev/stdin` 67 | - Write arbitrary files (jsonnet -S -m) 68 | 69 | Already covered by tanka.dev: 70 | 71 | - Using Tanka 72 | - Use of inline environments (hint: `tanka-util`) 73 | - Keep environments clean 74 | - Locally inspect with `tk eval` 75 | - Continuous Delivery with `tk export` 76 | - Know how to avoid common pitfalls: 77 | - Global variables 78 | - Smashing libraries together 79 | 80 | - Exercise: Use Helm chart in Jsonnet 81 | - Vendor Helm charts with `tk tool charts` 82 | - Load chart with `tanka-util` 83 | - Modify chart beyond `values.yaml` 84 | - Patch arrays 85 | -------------------------------------------------------------------------------- /_gen/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ## Who is this? 4 | 5 | I'm [Jeroen Op 't Eynde](http://simplistic.be), a software engineer at [Grafana 6 | Labs](https://grafana.com) and a maintainer on [Tanka](https://tanka.dev) and the 7 | [jsonnet-libs/k8s](https://github.com/jsonnet-libs/k8s) project. 8 | 9 | This course is the result countless hours of researching and debating with [Malcolm 10 | Holmes](https://github.com/malcolmholmes), [Tom Braack](https://shorez.de/) and many 11 | colleagues and community members on how to maintain and write more effective Jsonnet. 12 | 13 | ## What are the lessons we've learned after using Jsonnet for years? 14 | 15 | Grafana Labs uses a mixture of Terraform (HCL) and Jsonnet to configure the Cloud 16 | infrastructure and applications on Kubernetes. While new hires are often familiar with 17 | Terraform, they might never have heard of Jsonnet. This made that question the top voted 18 | topic for training sessions by new hires at Grafana Labs. 19 | 20 | While a training session can cover the basics or highlights in ~30min, there is much more 21 | to it. We usually point out to the excellent tutorials on 22 | [jsonnet.org](https://jsonnet.org/learning/tutorial.html) to get started, these explain 23 | very well how Jsonnet works but not necessarily how to work with Jsonnet. This course 24 | attempts to cover the idioms we've been adopting over years of discovering. 25 | 26 | ## Why did we pick Jsonnet? 27 | 28 | With the acquisition of Kausal by Grafana Labs early 2018, we also adopted Ksonnet, 29 | laying down the base for configuration management with Jsonnet. In addition to the 30 | [language comparison](https://jsonnet.org/articles/comparisons.html) on jsonnet.org, 31 | there are a few other advantages. 32 | 33 | The most common tool to manage Kubernetes manifests today is Helm, so why not use that? By 34 | templating the YAML, Helm only allows for one level of abstraction (values.yaml), anything 35 | beyond that requires a change request upstream or more commonly a fork. This causes an 36 | asymmetry between authoring and using Helm charts. 37 | 38 | Jsonnet on the other hand allows for an infinite number of abstractions, the initial 39 | author only needs to worry about their use case, so libraries can be kept quite concise. 40 | If a user wants to do something slightly different, they can simply concatenate the 41 | change to the library. 42 | 43 | > **Helm support in Tanka** 44 | > 45 | > Tanka has built-in [support for Helm charts](https://tanka.dev/helm#helm-support), 46 | > giving the Jsonnet community access to the biggest ecosystem of application definitions 47 | > for Kubernetes. 48 | 49 | ## What benefit do we gain from it? 50 | 51 | Jsonnet is a language about data. By managing configuration with Jsonnet, it essentially 52 | turns into a massive programmable database. The infinite number of abstractions allows it 53 | to create layers. A Deployment is part of an application, which can be included in a cell 54 | (collection of loosely-coupled applications) that gets deployed to a cluster. By extending 55 | the cluster list, new cells can be created and applications automatically become available 56 | without the need to configure and deploy each application individually. 57 | 58 | Each of these concepts has their own configuration attributes that could be managed by 59 | a different team. This means that, with a clean, well defined API between layers of 60 | abstraction, a developer new to Jsonnet needn't learn every layer, they only need to 61 | learn the layer they are interested in, enabling them to become productive much faster 62 | than if they had to code every layer. 63 | 64 | -------------------------------------------------------------------------------- /_gen/index.md: -------------------------------------------------------------------------------- 1 | # Jsonnet Training Course 2 | 3 | Excited as you might be after following the excellent tutorials on 4 | [jsonnet.org](https://jsonnet.org/learning/tutorial.html), it can still be daunting to 5 | actually use Jsonnet in the real world. This hands-on course attempts to cover common 6 | Jsonnet idioms that have been battle tested over several years. 7 | 8 | The examples and use cases in this course are from real world usage instead of working 9 | with arbitrary examples like cocktails or your favorite pets. The step by step examples 10 | show how to use Jsonnet effectively, at the same time explaining the why, covering 11 | pitfalls and other hurdles we might come across. 12 | 13 | This course is a work in progress, the plan is to dive deeper into the Jsonnet ecosystem 14 | with more lessons, exercises and use cases. If you notice a mistake or want to share your 15 | experience, reach out to us on 16 | [Github](https://github.com/jsonnet-libs/jsonnet-training-course). 17 | 18 | ## Getting started 19 | 20 | Jsonnet has two implementations (C++ and Go), the examples should work with either version 21 | above v0.18.0. If you don't know what to choose then [install the Go 22 | implementation](https://github.com/google/go-jsonnet#installation-instructions). 23 | 24 | For package management we'll use jsonnet-bundler, please 25 | [install](https://github.com/jsonnet-bundler/jsonnet-bundler#install) this too. 26 | 27 | ## Lessons 28 | 29 | > Note: A lot of the examples will be around Kubernetes objects, but no worries if you 30 | > don't know how Kubernetes works, this isn't a requirement for understanding the Jsonnet 31 | > examples. 32 | 33 | 1. [Write an extensible library](lesson1.md) 34 | 1. [Understanding Package management](lesson2.md) 35 | 1. [Exercise: rewrite a library with `k8s-libsonnet`](lesson3.md) 36 | 1. [Further developing libraries](lesson4.md) 37 | 1. [Providing documentation with Docsonnet](lesson5.md) 38 | 1. [Unit testing](lesson6.md) 39 | 40 | 41 | -------------------------------------------------------------------------------- /_gen/lesson3.md: -------------------------------------------------------------------------------- 1 | # Exercise: rewrite a library with `k8s-libsonnet` 2 | 3 | In the first lesson we've written a extensible library and in the second lesson we've 4 | covered package management with jsonnet-bundler. In this lesson we'll combine what 5 | we've learned and rewrite that library. 6 | 7 | 8 | ## Objectives 9 | 10 | - Rewrite a library 11 | - Vendor and use `k8s-libsonnet` 12 | - Understand the `lib/k.libsonnet` convention 13 | 14 | ## Lesson 15 | 16 | In [Write an extensible library](lesson1.md), we created this library: 17 | 18 | ~~~jsonnet 19 | local webserver = { 20 | new(name, replicas=1): { 21 | local base = self, 22 | 23 | container:: { 24 | name: 'httpd', 25 | image: 'httpd:2.4', 26 | }, 27 | 28 | deployment: { 29 | apiVersion: 'apps/v1', 30 | kind: 'Deployment', 31 | metadata: { 32 | name: name, 33 | }, 34 | spec: { 35 | replicas: replicas, 36 | template: { 37 | spec: { 38 | containers: [ 39 | base.container, 40 | ], 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | 47 | withImage(image): { 48 | container+: { image: image }, 49 | }, 50 | }; 51 | 52 | webserver.new('wonderful-webserver') 53 | + webserver.withImage('httpd:2.5') 54 | 55 | // example1.jsonnet 56 | ~~~ 57 | 58 | 59 | This library is quite verbose as the author has to provide the `apiVersion`, `kind` and 60 | other attributes. 61 | 62 | To simplify this, the community has created a Kubernetes client library for Jsonnet called 63 | [`k8s-libsonnet`](https://github.com/jsonnet-libs/k8s-libsonnet). By leveraging this 64 | client library, the author can provide an abstraction that can work across most Kubernetes 65 | versions. 66 | 67 | Now go ahead with the `k8s-libsonnet` library and work out on your own with the resources 68 | in these lessons: 69 | 70 | 1. [Write an extensible library](lesson1.md) 71 | 1. [Understanding Package management](lesson2.md) 72 | 73 | Find the steps to a solution below. 74 | 75 | ### Solution 76 | 77 | > Examples below expect to have an environment with `export JSONNET_PATH="lib/:vendor/"` 78 | 79 | Let's install `k8s-libsonnet` with jsonnet-bundler and import it: 80 | 81 | `$ jb install https://github.com/jsonnet-libs/k8s-libsonnet/1.23@main` 82 | 83 | Note the alternative naming pattern ending on `1.23`, referencing the Kubernetes version 84 | this was generated for. 85 | 86 | ~~~jsonnet 87 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 88 | 89 | // example2/lib/k.libsonnet 90 | ~~~ 91 | 92 | 93 | The most common convention to work with this is to provide `lib/k.libsonnet` as 94 | a shortcut. 95 | 96 | --- 97 | 98 | ~~~jsonnet 99 | local k = import 'k.libsonnet'; 100 | 101 | k.core.v1.container.new('container-name', 'container-image') 102 | 103 | // example2/example1.jsonnet 104 | ~~~ 105 | 106 | 107 | Many libraries have a line `local k = import 'k.libsonnet'` to refer to this 108 | library. 109 | 110 | --- 111 | 112 | Let's rewrite the container following the 113 | [documentation](https://jsonnet-libs.github.io/k8s-libsonnet/1.23/core/v1/container/): 114 | 115 | ~~~jsonnet 116 | local k = import 'k.libsonnet'; 117 | 118 | local webserver = { 119 | new(name, replicas=1): { 120 | local base = self, 121 | 122 | container:: 123 | k.core.v1.container.new('httpd', 'httpd:2.4'), 124 | 125 | deployment: { 126 | apiVersion: 'apps/v1', 127 | kind: 'Deployment', 128 | metadata: { 129 | name: name, 130 | }, 131 | spec: { 132 | replicas: replicas, 133 | template: { 134 | spec: { 135 | containers: [ 136 | base.container, 137 | ], 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | 144 | withImage(image): { 145 | container+: 146 | k.core.v1.container.withImage(image), 147 | }, 148 | }; 149 | 150 | webserver.new('wonderful-webserver') 151 | + webserver.withImage('httpd:2.5') 152 | 153 | // example2/example2.jsonnet 154 | ~~~ 155 | 156 | 157 | The library has grouped a number of functions under `k.core.v1.container`, we'll use the 158 | `new(name, image)` function here, this makes it concise. Additionally the `withImage()` 159 | function uses the function with the same name in the library. 160 | 161 | --- 162 | 163 | And now for the 164 | [deployment](https://jsonnet-libs.github.io/k8s-libsonnet/1.23/apps/v1/deployment/): 165 | 166 | ~~~jsonnet 167 | local k = import 'k.libsonnet'; 168 | 169 | local webserver = { 170 | new(name, replicas=1): { 171 | container:: 172 | k.core.v1.container.new('httpd', 'httpd:2.4'), 173 | 174 | deployment: 175 | k.apps.v1.deployment.new( 176 | name, 177 | replicas, 178 | [self.container] 179 | ), 180 | }, 181 | 182 | withImage(image): { 183 | container+: 184 | k.core.v1.container.withImage(image), 185 | }, 186 | }; 187 | 188 | webserver.new('wonderful-webserver') 189 | + webserver.withImage('httpd:2.5') 190 | 191 | // example2/example3.jsonnet 192 | ~~~ 193 | 194 | 195 | The `new(name, replicas, images)` function makes things even more concise. The `new()` 196 | function is actually a custom shortcut with the most common parameters for a deployment. 197 | 198 | Note that we've removed `local base = self,`, this was not longer needed as the reference 199 | to `self.container` can now be made inside the same object. 200 | 201 | --- 202 | 203 | Having the library and execution together is not so useful, let's move it into a separate 204 | library and import it again. 205 | 206 | ~~~jsonnet 207 | local k = import 'k.libsonnet'; 208 | 209 | { 210 | new(name, replicas=1): { 211 | container:: 212 | k.core.v1.container.new('httpd', 'httpd:2.4'), 213 | 214 | deployment: 215 | k.apps.v1.deployment.new( 216 | name, 217 | replicas, 218 | [self.container] 219 | ), 220 | }, 221 | 222 | withImage(image): { 223 | container+: 224 | k.core.v1.container.withImage(image), 225 | }, 226 | } 227 | 228 | // example2/lib/webserver/main.libsonnet 229 | ~~~ 230 | 231 | 232 | This removes the `local webserver` and moves the contents to the root of the file. 233 | 234 | --- 235 | 236 | ~~~jsonnet 237 | local webserver = import 'webserver/main.libsonnet'; 238 | 239 | webserver.new('wonderful-webserver') 240 | + webserver.withImage('httpd:2.5') 241 | 242 | // example2/example4.jsonnet 243 | ~~~ 244 | 245 | 246 | If we now `import` the library, we can access its functions just like before. 247 | 248 | --- 249 | 250 | ~~~jsonnet 251 | local webserver = import 'webserver/main.libsonnet'; 252 | 253 | { 254 | webserver1: 255 | webserver.new('wonderful-webserver') 256 | + webserver.withImage('httpd:2.3'), 257 | 258 | webserver2: 259 | webserver.new('marvellous-webserver'), 260 | 261 | webserver3: 262 | webserver.new('incredible-webserver', 2), 263 | } 264 | 265 | // example2/example5.jsonnet 266 | ~~~ 267 | 268 | 269 | Or, if we want more instances, we can simply do so. 270 | 271 | 272 | ## Conclusion 273 | 274 | This exercise showed how to make a library more succinct and readable. By using the 275 | `k.libsonnet` abstract, the user has the option to use an alternative version of the 276 | `k8s-libsonnet` library. 277 | 278 | 279 | -------------------------------------------------------------------------------- /coursonnet.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | local root = self, 3 | 4 | page: { 5 | new(filename, title, content): { 6 | local this = self, 7 | 8 | filename: filename, 9 | title: title, 10 | content: content, 11 | 12 | render: { 13 | [filename]: ||| 14 | # %(title)s 15 | 16 | %(content)s 17 | ||| % this, 18 | }, 19 | }, 20 | }, 21 | 22 | lesson: { 23 | new( 24 | slug, 25 | title, 26 | summary, 27 | objectives, 28 | lesson, 29 | conclusion, 30 | ): { 31 | content: ||| 32 | %(summary)s 33 | 34 | ## Objectives 35 | 36 | %(objectives)s 37 | 38 | ## Lesson 39 | 40 | %(lesson)s 41 | 42 | ## Conclusion 43 | 44 | %(conclusion)s 45 | ||| % { 46 | title: title, 47 | summary: summary, 48 | objectives: 49 | std.join('\n', [ 50 | '- %s' % objective 51 | for objective in objectives 52 | ]), 53 | lesson: lesson, 54 | conclusion: conclusion, 55 | }, 56 | 57 | slug: slug, 58 | title: title, 59 | filename: self.page.filename, 60 | render: self.page.render[self.filename], 61 | 62 | page: root.page.new( 63 | slug + '.md', 64 | title, 65 | self.content, 66 | ), 67 | }, 68 | }, 69 | 70 | example: { 71 | new(filename, string, jsonnet={}, type='jsonnet'): { 72 | local this = self, 73 | 74 | filename: filename, 75 | string: string, 76 | jsonnet: jsonnet, 77 | type: type, 78 | base64: std.base64(self.string), 79 | playground: 'https://jsonnet-libs.github.io/playground/?code=%s' % self.base64, 80 | 81 | code: 82 | ||| 83 | ~~~%(type)s 84 | %(string)s 85 | // %(filename)s 86 | ~~~ 87 | ||| % self, 88 | 89 | render: self.code, 90 | }, 91 | 92 | withLink():: { 93 | render+: 94 | '[Try `%(filename)s` in Jsonnet Playground](%(playground)s)' % self, 95 | }, 96 | 97 | withFoldedIframe():: { 98 | iframe: '' % self.playground, 99 | 100 | render+: 101 | ||| 102 |
103 | Try in Jsonnet Playground 104 | %(iframe)s 105 |
106 | ||| % self, 107 | }, 108 | 109 | }, 110 | } 111 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS md2html 2 | 3 | RUN apk --no-cache add cmake clang clang-dev make gcc g++ libc-dev linux-headers git tree 4 | 5 | RUN git clone https://github.com/mity/md4c.git 6 | 7 | RUN cd md4c && \ 8 | mkdir build && \ 9 | cd build && \ 10 | cmake -DBUILD_SHARED_LIBS=OFF .. && \ 11 | make && \ 12 | make install 13 | 14 | FROM golang:alpine AS jsonnet 15 | 16 | RUN go install github.com/google/go-jsonnet/cmd/jsonnet@latest 17 | RUN which jsonnet 18 | 19 | FROM alpine 20 | 21 | RUN apk --no-cache add make bash 22 | 23 | COPY --from=jsonnet /go/bin/jsonnet /usr/local/bin/jsonnet 24 | COPY --from=md2html /usr/local/bin/md2html /usr/local/bin/md2html 25 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Build workflow in docker 2 | 3 | Simple build image to render the markdown and html with Docker. 4 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | DIRNAME="$(dirname "$0")" 6 | 7 | PARENT="$(readlink -e "$DIRNAME/..")" 8 | 9 | docker build . -t jsonnet-md2html 10 | 11 | docker run \ 12 | -u "$(id -u "${USER}"):$(id -g "${USER}")" \ 13 | --rm \ 14 | -v "$PARENT":/training \ 15 | -w /training \ 16 | jsonnet-md2html make 17 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonnet-libs/jsonnet-training-course/9ec5eec738214a3dd11acec12afd9d1126d5ee21/docs/.nojekyll -------------------------------------------------------------------------------- /docs/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About - Jsonnet Training Course 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

index

18 |

About

19 |

Who is this?

20 |

I'm Jeroen Op 't Eynde, a software engineer at Grafana 21 | Labs and a maintainer on Tanka and the 22 | jsonnet-libs/k8s project.

23 |

This course is the result countless hours of researching and debating with Malcolm 24 | Holmes, Tom Braack and many 25 | colleagues and community members on how to maintain and write more effective Jsonnet.

26 |

What are the lessons we've learned after using Jsonnet for years?

27 |

Grafana Labs uses a mixture of Terraform (HCL) and Jsonnet to configure the Cloud 28 | infrastructure and applications on Kubernetes. While new hires are often familiar with 29 | Terraform, they might never have heard of Jsonnet. This made that question the top voted 30 | topic for training sessions by new hires at Grafana Labs.

31 |

While a training session can cover the basics or highlights in ~30min, there is much more 32 | to it. We usually point out to the excellent tutorials on 33 | jsonnet.org to get started, these explain 34 | very well how Jsonnet works but not necessarily how to work with Jsonnet. This course 35 | attempts to cover the idioms we've been adopting over years of discovering.

36 |

Why did we pick Jsonnet?

37 |

With the acquisition of Kausal by Grafana Labs early 2018, we also adopted Ksonnet, 38 | laying down the base for configuration management with Jsonnet. In addition to the 39 | language comparison on jsonnet.org, 40 | there are a few other advantages.

41 |

The most common tool to manage Kubernetes manifests today is Helm, so why not use that? By 42 | templating the YAML, Helm only allows for one level of abstraction (values.yaml), anything 43 | beyond that requires a change request upstream or more commonly a fork. This causes an 44 | asymmetry between authoring and using Helm charts.

45 |

Jsonnet on the other hand allows for an infinite number of abstractions, the initial 46 | author only needs to worry about their use case, so libraries can be kept quite concise. 47 | If a user wants to do something slightly different, they can simply concatenate the 48 | change to the library.

49 |
50 |

Helm support in Tanka

51 |

Tanka has built-in support for Helm charts, 52 | giving the Jsonnet community access to the biggest ecosystem of application definitions 53 | for Kubernetes.

54 |
55 |

What benefit do we gain from it?

56 |

Jsonnet is a language about data. By managing configuration with Jsonnet, it essentially 57 | turns into a massive programmable database. The infinite number of abstractions allows it 58 | to create layers. A Deployment is part of an application, which can be included in a cell 59 | (collection of loosely-coupled applications) that gets deployed to a cluster. By extending 60 | the cluster list, new cells can be created and applications automatically become available 61 | without the need to configure and deploy each application individually.

62 |

Each of these concepts has their own configuration attributes that could be managed by 63 | a different team. This means that, with a clean, well defined API between layers of 64 | abstraction, a developer new to Jsonnet needn't learn every layer, they only need to 65 | learn the layer they are interested in, enabling them to become productive much faster 66 | than if they had to code every layer.

67 |

index

68 |
69 | 70 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/assets/anchorlinks.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | // Source: https://attacomsian.com/blog/deep-anchor-links-javascript 3 | document.querySelectorAll('h2, h3, h4, h5, h6').forEach($heading => { 4 | //create id from heading text 5 | var id = $heading.getAttribute("id") || $heading.innerText.toLowerCase().replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '').replace(/ +/g, '-'); 6 | 7 | //add id to heading 8 | $heading.setAttribute('id', id); 9 | 10 | //append parent class to heading 11 | $heading.classList.add('anchor-heading'); 12 | 13 | //create anchor 14 | var $anchor; 15 | $anchor = document.createElement('a'); 16 | $anchor.className = 'anchor-link'; 17 | $anchor.href = '#' + id; 18 | $anchor.innerHTML = '☍'; 19 | 20 | //prepend anchor before heading text 21 | var text = $heading.innerText; 22 | $heading.innerText = ""; 23 | $heading.appendChild($anchor); 24 | $heading.append(text); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /docs/assets/prettyprint.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | document.querySelectorAll('pre').forEach($pre => { 3 | $pre.className='prettyprint linenums'; 4 | }); 5 | PR.prettyPrint(); 6 | }); 7 | -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 100%; 5 | /*background: cornflowerblue;*/ 6 | /* Pattern/color from jsonnet.org 7 | background-image: url(https://jsonnet.org/img/isopattern.svg); 8 | background-size: 128px; 9 | background-color: #0064bd;*/ 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 1.2em; 15 | min-height: 100%; 16 | margin-left: 15%; 17 | margin-right: 15%; 18 | background: white; 19 | /*border: thin solid slategrey;*/ 20 | border-top: 0; 21 | /* sans-serif fonts are generally better readable for people with dyslexia */ 22 | font-family: sans-serif; 23 | font-size: 1em; 24 | } 25 | 26 | blockquote { 27 | background: lightyellow; 28 | padding: .1em; 29 | padding-left: 1em; 30 | margin-left: 0; 31 | margin-right: 0; 32 | display: flow-root; 33 | font-size: 1rem; 34 | border-left: 1em solid gold; 35 | } 36 | 37 | p { 38 | display: flow-root; 39 | line-height: 1.5em; 40 | } 41 | 42 | h1 a, h2 a, h3 a, 43 | h4 a, h5 a, h6 a { 44 | text-decoration: none; 45 | color: black; 46 | } 47 | 48 | a.anchor-link { 49 | font-size: 0.9rem; 50 | vertical-align: middle; 51 | line-height: 1; 52 | margin: 0.5em; 53 | margin-left: -1.5em; 54 | color: slategrey; 55 | } 56 | 57 | h1, h2, h3, h4, h5, h6, hr { 58 | clear: both; 59 | } 60 | 61 | hr { 62 | visibility: hidden; 63 | } 64 | 65 | ol, ul { 66 | display: flow-root; 67 | } 68 | 69 | ol li, 70 | ul li { 71 | line-height: 1.5em; 72 | } 73 | 74 | ul li code, p code { 75 | background: ghostwhite; 76 | padding: 0.2em; 77 | } 78 | 79 | pre ol { 80 | display: block; 81 | } 82 | 83 | pre { 84 | float: left; 85 | margin-top: 0; 86 | margin-right: 1em; 87 | width: min(50%, 70ch); 88 | overflow-x: scroll; 89 | border: 1px solid slategrey !important; 90 | clear: left; 91 | } 92 | 93 | nav#toc { 94 | background: white; 95 | border: 1px solid slategrey; 96 | padding-right: 1em; 97 | } 98 | 99 | span.nav a { 100 | display: inline-block; 101 | width: 5rem; 102 | height: 1.15rem; 103 | padding: 0.2em; 104 | text-decoration: none; 105 | border: 1px solid slategrey; 106 | line-height: 1.15rem; 107 | color: slategrey; 108 | text-align: center; 109 | font-size: 0.9rem; 110 | } 111 | 112 | span.nav.previous a { 113 | border-radius: 1em 0 0 1em; 114 | } 115 | 116 | span.nav.next a { 117 | border-radius: 0 1em 1em 0; 118 | color: white; 119 | background: slategrey; 120 | } 121 | 122 | footer, footer a{ 123 | color: slategrey; 124 | text-align: center; 125 | } 126 | 127 | @media only screen and (max-width: 720px) { 128 | body { 129 | width: auto; 130 | margin-left: 0; 131 | margin-right: 0; 132 | border: 0; 133 | } 134 | pre { 135 | clear: both; 136 | float: none; 137 | width: min(95%, 70ch); 138 | } 139 | } 140 | 141 | li.L1, li.L3, li.L5, li.L7, li.L9 { 142 | background: ghostwhite !important; 143 | } 144 | 145 | ol.linenums { 146 | list-style: none; 147 | counter-reset: li; 148 | } 149 | 150 | ol.linenums li::before { 151 | content: counter(li); 152 | color: slategrey; 153 | display: inline-block; 154 | width: 1em; 155 | margin-left: -2em; 156 | padding-right: 0.5em; 157 | text-align: right; 158 | } 159 | 160 | ol.linenums li { 161 | counter-increment: li; 162 | line-height: normal; 163 | } 164 | -------------------------------------------------------------------------------- /docs/assets/toc.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | var heading = document.querySelector('h1'); 3 | 4 | //create anchor 5 | var anchor = document.createElement('a'); 6 | anchor.className = 'anchor-link'; 7 | anchor.innerHTML = '☰'; 8 | anchor.style.cursor = 'pointer'; 9 | 10 | var toc = document.createElement('nav'); 11 | toc.id = 'toc'; 12 | toc.style.display = 'none'; 13 | 14 | toc.addEventListener('mouseleave', e => { 15 | toc.style.display = 'none'; 16 | }) 17 | anchor.addEventListener('click', e => { 18 | toc.style.display = 'block'; 19 | }) 20 | 21 | var lastLevel = 1; 22 | var lastId = 'null'; 23 | var li = toc; 24 | var ul; 25 | document.querySelectorAll('h2, h3, h4, h5, h6').forEach($heading => { 26 | var level = $heading.nodeName.charAt(1); 27 | 28 | if (level > lastLevel) { 29 | ul = li.querySelector('ul.'+lastId) 30 | if (ul == null) { 31 | ul = document.createElement('ul'); 32 | ul.classList.add($heading.id); 33 | li.appendChild(ul); 34 | } 35 | } 36 | 37 | if (level < lastLevel) { 38 | for (i=lastLevel; i>level; i--) { 39 | // ul li ul 40 | ul = li.parentNode.parentNode.parentNode; 41 | if (ul == null) { 42 | ul = toc.querySelector('ul:last-child') 43 | break 44 | } 45 | // li ul li 46 | li = ul.querySelector('li:last-child').parentNode.parentNode; 47 | } 48 | } 49 | 50 | li = document.createElement('li'); 51 | ul.appendChild(li); 52 | 53 | var headerId = $heading.id; 54 | var a = document.createElement('a'); 55 | a.href = '#' + headerId; 56 | var h = $heading; 57 | a.innerHTML = $heading.innerHTML; 58 | a.querySelectorAll('a').forEach($child => { 59 | a.removeChild($child); 60 | }); 61 | li.appendChild(a); 62 | lastLevel = level; 63 | lastId = headerId; 64 | }) 65 | 66 | if (document.querySelectorAll('h2, h3, h4, h5, h6').length > 0) { 67 | //prepend anchor before heading text 68 | var text = heading.innerText; 69 | heading.innerText = ""; 70 | heading.appendChild(anchor); 71 | heading.append(text); 72 | 73 | toc.style.position = 'absolute'; 74 | toc.style.top = anchor.offsetTop + 'px'; 75 | toc.style.left = anchor.offsetLeft + 'px'; 76 | 77 | document.querySelector('body').appendChild(toc); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jsonnet Training Course 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Jsonnet Training Course

18 |

Excited as you might be after following the excellent tutorials on 19 | jsonnet.org, it can still be daunting to 20 | actually use Jsonnet in the real world. This hands-on course attempts to cover common 21 | Jsonnet idioms that have been battle tested over several years.

22 |

The examples and use cases in this course are from real world usage instead of working 23 | with arbitrary examples like cocktails or your favorite pets. The step by step examples 24 | show how to use Jsonnet effectively, at the same time explaining the why, covering 25 | pitfalls and other hurdles we might come across.

26 |

This course is a work in progress, the plan is to dive deeper into the Jsonnet ecosystem 27 | with more lessons, exercises and use cases. If you notice a mistake or want to share your 28 | experience, reach out to us on 29 | Github.

30 |

Getting started

31 |

Jsonnet has two implementations (C++ and Go), the examples should work with either version 32 | above v0.18.0. If you don't know what to choose then install the Go 33 | implementation.

34 |

For package management we'll use jsonnet-bundler, please 35 | install this too.

36 |

Lessons

37 |
38 |

Note: A lot of the examples will be around Kubernetes objects, but no worries if you 39 | don't know how Kubernetes works, this isn't a requirement for understanding the Jsonnet 40 | examples.

41 |
42 |
    43 |
  1. Write an extensible library
  2. 44 |
  3. Understanding Package management
  4. 45 |
  5. Exercise: rewrite a library with k8s-libsonnet
  6. 46 |
  7. Further developing libraries
  8. 47 |
  9. Providing documentation with Docsonnet
  10. 48 |
  11. Unit testing
  12. 49 |
50 |
51 | 52 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/lesson3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exercise: rewrite a library with `k8s-libsonnet` - Jsonnet Training Course 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

« previous index next »

18 |

Exercise: rewrite a library with k8s-libsonnet

19 |

In the first lesson we've written a extensible library and in the second lesson we've 20 | covered package management with jsonnet-bundler. In this lesson we'll combine what 21 | we've learned and rewrite that library.

22 |

Objectives

23 | 28 |

Lesson

29 |

In Write an extensible library, we created this library:

30 |
local webserver = {
 31 |   new(name, replicas=1): {
 32 |     local base = self,
 33 | 
 34 |     container:: {
 35 |       name: 'httpd',
 36 |       image: 'httpd:2.4',
 37 |     },
 38 | 
 39 |     deployment: {
 40 |       apiVersion: 'apps/v1',
 41 |       kind: 'Deployment',
 42 |       metadata: {
 43 |         name: name,
 44 |       },
 45 |       spec: {
 46 |         replicas: replicas,
 47 |         template: {
 48 |           spec: {
 49 |             containers: [
 50 |               base.container,
 51 |             ],
 52 |           },
 53 |         },
 54 |       },
 55 |     },
 56 |   },
 57 | 
 58 |   withImage(image): {
 59 |     container+: { image: image },
 60 |   },
 61 | };
 62 | 
 63 | webserver.new('wonderful-webserver')
 64 | + webserver.withImage('httpd:2.5')
 65 | 
 66 | // example1.jsonnet
 67 | 
68 |

This library is quite verbose as the author has to provide the apiVersion, kind and 69 | other attributes.

70 |

To simplify this, the community has created a Kubernetes client library for Jsonnet called 71 | k8s-libsonnet. By leveraging this 72 | client library, the author can provide an abstraction that can work across most Kubernetes 73 | versions.

74 |

Now go ahead with the k8s-libsonnet library and work out on your own with the resources 75 | in these lessons:

76 |
    77 |
  1. Write an extensible library
  2. 78 |
  3. Understanding Package management
  4. 79 |
80 |

Find the steps to a solution below.

81 |

Solution

82 |
83 |

Examples below expect to have an environment with export JSONNET_PATH="lib/:vendor/"

84 |
85 |

Let's install k8s-libsonnet with jsonnet-bundler and import it:

86 |

$ jb install https://github.com/jsonnet-libs/k8s-libsonnet/1.23@main

87 |

Note the alternative naming pattern ending on 1.23, referencing the Kubernetes version 88 | this was generated for.

89 |
(import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet')
 90 | 
 91 | // example2/lib/k.libsonnet
 92 | 
93 |

The most common convention to work with this is to provide lib/k.libsonnet as 94 | a shortcut.

95 |
96 |
local k = import 'k.libsonnet';
 97 | 
 98 | k.core.v1.container.new('container-name', 'container-image')
 99 | 
100 | // example2/example1.jsonnet
101 | 
102 |

Many libraries have a line local k = import 'k.libsonnet' to refer to this 103 | library.

104 |
105 |

Let's rewrite the container following the 106 | documentation:

107 |
local k = import 'k.libsonnet';
108 | 
109 | local webserver = {
110 |   new(name, replicas=1): {
111 |     local base = self,
112 | 
113 |     container::
114 |       k.core.v1.container.new('httpd', 'httpd:2.4'),
115 | 
116 |     deployment: {
117 |       apiVersion: 'apps/v1',
118 |       kind: 'Deployment',
119 |       metadata: {
120 |         name: name,
121 |       },
122 |       spec: {
123 |         replicas: replicas,
124 |         template: {
125 |           spec: {
126 |             containers: [
127 |               base.container,
128 |             ],
129 |           },
130 |         },
131 |       },
132 |     },
133 |   },
134 | 
135 |   withImage(image): {
136 |     container+:
137 |       k.core.v1.container.withImage(image),
138 |   },
139 | };
140 | 
141 | webserver.new('wonderful-webserver')
142 | + webserver.withImage('httpd:2.5')
143 | 
144 | // example2/example2.jsonnet
145 | 
146 |

The library has grouped a number of functions under k.core.v1.container, we'll use the 147 | new(name, image) function here, this makes it concise. Additionally the withImage() 148 | function uses the function with the same name in the library.

149 |
150 |

And now for the 151 | deployment:

152 |
local k = import 'k.libsonnet';
153 | 
154 | local webserver = {
155 |   new(name, replicas=1): {
156 |     container::
157 |       k.core.v1.container.new('httpd', 'httpd:2.4'),
158 | 
159 |     deployment:
160 |       k.apps.v1.deployment.new(
161 |         name,
162 |         replicas,
163 |         [self.container]
164 |       ),
165 |   },
166 | 
167 |   withImage(image): {
168 |     container+:
169 |       k.core.v1.container.withImage(image),
170 |   },
171 | };
172 | 
173 | webserver.new('wonderful-webserver')
174 | + webserver.withImage('httpd:2.5')
175 | 
176 | // example2/example3.jsonnet
177 | 
178 |

The new(name, replicas, images) function makes things even more concise. The new() 179 | function is actually a custom shortcut with the most common parameters for a deployment.

180 |

Note that we've removed local base = self,, this was not longer needed as the reference 181 | to self.container can now be made inside the same object.

182 |
183 |

Having the library and execution together is not so useful, let's move it into a separate 184 | library and import it again.

185 |
local k = import 'k.libsonnet';
186 | 
187 | {
188 |   new(name, replicas=1): {
189 |     container::
190 |       k.core.v1.container.new('httpd', 'httpd:2.4'),
191 | 
192 |     deployment:
193 |       k.apps.v1.deployment.new(
194 |         name,
195 |         replicas,
196 |         [self.container]
197 |       ),
198 |   },
199 | 
200 |   withImage(image): {
201 |     container+:
202 |       k.core.v1.container.withImage(image),
203 |   },
204 | }
205 | 
206 | // example2/lib/webserver/main.libsonnet
207 | 
208 |

This removes the local webserver and moves the contents to the root of the file.

209 |
210 |
local webserver = import 'webserver/main.libsonnet';
211 | 
212 | webserver.new('wonderful-webserver')
213 | + webserver.withImage('httpd:2.5')
214 | 
215 | // example2/example4.jsonnet
216 | 
217 |

If we now import the library, we can access its functions just like before.

218 |
219 |
local webserver = import 'webserver/main.libsonnet';
220 | 
221 | {
222 |   webserver1:
223 |     webserver.new('wonderful-webserver')
224 |     + webserver.withImage('httpd:2.3'),
225 | 
226 |   webserver2:
227 |     webserver.new('marvellous-webserver'),
228 | 
229 |   webserver3:
230 |     webserver.new('incredible-webserver', 2),
231 | }
232 | 
233 | // example2/example5.jsonnet
234 | 
235 |

Or, if we want more instances, we can simply do so.

236 |

Conclusion

237 |

This exercise showed how to make a library more succinct and readable. By using the 238 | k.libsonnet abstract, the user has the option to use an alternative version of the 239 | k8s-libsonnet library.

240 |

« previous index next »

241 |
242 | 243 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /html/assets/anchorlinks.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | // Source: https://attacomsian.com/blog/deep-anchor-links-javascript 3 | document.querySelectorAll('h2, h3, h4, h5, h6').forEach($heading => { 4 | //create id from heading text 5 | var id = $heading.getAttribute("id") || $heading.innerText.toLowerCase().replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '').replace(/ +/g, '-'); 6 | 7 | //add id to heading 8 | $heading.setAttribute('id', id); 9 | 10 | //append parent class to heading 11 | $heading.classList.add('anchor-heading'); 12 | 13 | //create anchor 14 | var $anchor; 15 | $anchor = document.createElement('a'); 16 | $anchor.className = 'anchor-link'; 17 | $anchor.href = '#' + id; 18 | $anchor.innerHTML = '☍'; 19 | 20 | //prepend anchor before heading text 21 | var text = $heading.innerText; 22 | $heading.innerText = ""; 23 | $heading.appendChild($anchor); 24 | $heading.append(text); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /html/assets/prettyprint.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | document.querySelectorAll('pre').forEach($pre => { 3 | $pre.className='prettyprint linenums'; 4 | }); 5 | PR.prettyPrint(); 6 | }); 7 | -------------------------------------------------------------------------------- /html/assets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | min-height: 100%; 5 | /*background: cornflowerblue;*/ 6 | /* Pattern/color from jsonnet.org 7 | background-image: url(https://jsonnet.org/img/isopattern.svg); 8 | background-size: 128px; 9 | background-color: #0064bd;*/ 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 1.2em; 15 | min-height: 100%; 16 | margin-left: 15%; 17 | margin-right: 15%; 18 | background: white; 19 | /*border: thin solid slategrey;*/ 20 | border-top: 0; 21 | /* sans-serif fonts are generally better readable for people with dyslexia */ 22 | font-family: sans-serif; 23 | font-size: 1em; 24 | } 25 | 26 | blockquote { 27 | background: lightyellow; 28 | padding: .1em; 29 | padding-left: 1em; 30 | margin-left: 0; 31 | margin-right: 0; 32 | display: flow-root; 33 | font-size: 1rem; 34 | border-left: 1em solid gold; 35 | } 36 | 37 | p { 38 | display: flow-root; 39 | line-height: 1.5em; 40 | } 41 | 42 | h1 a, h2 a, h3 a, 43 | h4 a, h5 a, h6 a { 44 | text-decoration: none; 45 | color: black; 46 | } 47 | 48 | a.anchor-link { 49 | font-size: 0.9rem; 50 | vertical-align: middle; 51 | line-height: 1; 52 | margin: 0.5em; 53 | margin-left: -1.5em; 54 | color: slategrey; 55 | } 56 | 57 | h1, h2, h3, h4, h5, h6, hr { 58 | clear: both; 59 | } 60 | 61 | hr { 62 | visibility: hidden; 63 | } 64 | 65 | ol, ul { 66 | display: flow-root; 67 | } 68 | 69 | ol li, 70 | ul li { 71 | line-height: 1.5em; 72 | } 73 | 74 | ul li code, p code { 75 | background: ghostwhite; 76 | padding: 0.2em; 77 | } 78 | 79 | pre ol { 80 | display: block; 81 | } 82 | 83 | pre { 84 | float: left; 85 | margin-top: 0; 86 | margin-right: 1em; 87 | width: min(50%, 70ch); 88 | overflow-x: scroll; 89 | border: 1px solid slategrey !important; 90 | clear: left; 91 | } 92 | 93 | nav#toc { 94 | background: white; 95 | border: 1px solid slategrey; 96 | padding-right: 1em; 97 | } 98 | 99 | span.nav a { 100 | display: inline-block; 101 | width: 5rem; 102 | height: 1.15rem; 103 | padding: 0.2em; 104 | text-decoration: none; 105 | border: 1px solid slategrey; 106 | line-height: 1.15rem; 107 | color: slategrey; 108 | text-align: center; 109 | font-size: 0.9rem; 110 | } 111 | 112 | span.nav.previous a { 113 | border-radius: 1em 0 0 1em; 114 | } 115 | 116 | span.nav.next a { 117 | border-radius: 0 1em 1em 0; 118 | color: white; 119 | background: slategrey; 120 | } 121 | 122 | footer, footer a{ 123 | color: slategrey; 124 | text-align: center; 125 | } 126 | 127 | @media only screen and (max-width: 720px) { 128 | body { 129 | width: auto; 130 | margin-left: 0; 131 | margin-right: 0; 132 | border: 0; 133 | } 134 | pre { 135 | clear: both; 136 | float: none; 137 | width: min(95%, 70ch); 138 | } 139 | } 140 | 141 | li.L1, li.L3, li.L5, li.L7, li.L9 { 142 | background: ghostwhite !important; 143 | } 144 | 145 | ol.linenums { 146 | list-style: none; 147 | counter-reset: li; 148 | } 149 | 150 | ol.linenums li::before { 151 | content: counter(li); 152 | color: slategrey; 153 | display: inline-block; 154 | width: 1em; 155 | margin-left: -2em; 156 | padding-right: 0.5em; 157 | text-align: right; 158 | } 159 | 160 | ol.linenums li { 161 | counter-increment: li; 162 | line-height: normal; 163 | } 164 | -------------------------------------------------------------------------------- /html/assets/toc.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', e => { 2 | var heading = document.querySelector('h1'); 3 | 4 | //create anchor 5 | var anchor = document.createElement('a'); 6 | anchor.className = 'anchor-link'; 7 | anchor.innerHTML = '☰'; 8 | anchor.style.cursor = 'pointer'; 9 | 10 | var toc = document.createElement('nav'); 11 | toc.id = 'toc'; 12 | toc.style.display = 'none'; 13 | 14 | toc.addEventListener('mouseleave', e => { 15 | toc.style.display = 'none'; 16 | }) 17 | anchor.addEventListener('click', e => { 18 | toc.style.display = 'block'; 19 | }) 20 | 21 | var lastLevel = 1; 22 | var lastId = 'null'; 23 | var li = toc; 24 | var ul; 25 | document.querySelectorAll('h2, h3, h4, h5, h6').forEach($heading => { 26 | var level = $heading.nodeName.charAt(1); 27 | 28 | if (level > lastLevel) { 29 | ul = li.querySelector('ul.'+lastId) 30 | if (ul == null) { 31 | ul = document.createElement('ul'); 32 | ul.classList.add($heading.id); 33 | li.appendChild(ul); 34 | } 35 | } 36 | 37 | if (level < lastLevel) { 38 | for (i=lastLevel; i>level; i--) { 39 | // ul li ul 40 | ul = li.parentNode.parentNode.parentNode; 41 | if (ul == null) { 42 | ul = toc.querySelector('ul:last-child') 43 | break 44 | } 45 | // li ul li 46 | li = ul.querySelector('li:last-child').parentNode.parentNode; 47 | } 48 | } 49 | 50 | li = document.createElement('li'); 51 | ul.appendChild(li); 52 | 53 | var headerId = $heading.id; 54 | var a = document.createElement('a'); 55 | a.href = '#' + headerId; 56 | var h = $heading; 57 | a.innerHTML = $heading.innerHTML; 58 | a.querySelectorAll('a').forEach($child => { 59 | a.removeChild($child); 60 | }); 61 | li.appendChild(a); 62 | lastLevel = level; 63 | lastId = headerId; 64 | }) 65 | 66 | if (document.querySelectorAll('h2, h3, h4, h5, h6').length > 0) { 67 | //prepend anchor before heading text 68 | var text = heading.innerText; 69 | heading.innerText = ""; 70 | heading.appendChild(anchor); 71 | heading.append(text); 72 | 73 | toc.style.position = 'absolute'; 74 | toc.style.top = anchor.offsetTop + 'px'; 75 | toc.style.left = anchor.offsetLeft + 'px'; 76 | 77 | document.querySelector('body').appendChild(toc); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /html/html.jsonnet: -------------------------------------------------------------------------------- 1 | local template = importstr 'template.html'; 2 | 3 | function(title, body) 4 | template % { title: title, body: body } 5 | -------------------------------------------------------------------------------- /html/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | [[ -n $(which md2html) ]] || { 6 | echo You need md2html from https://github.com/mity/md4c 7 | exit 1 8 | } 9 | 10 | OUTPUT="$1" 11 | 12 | DIRNAME="$(dirname "$0")" 13 | 14 | TEMPDIR=$(mktemp -d) 15 | trap "rm -rf $TEMPDIR" EXIT 16 | 17 | jsonnet --tla-code nav=true -J . -m "$TEMPDIR" -S main.jsonnet 18 | 19 | FILES=$(find "$TEMPDIR" -type f -name \*.md) 20 | 21 | rm -rf "$OUTPUT" 22 | mkdir -p "$OUTPUT" 23 | touch "$OUTPUT"/.nojekyll 24 | cp -r "$DIRNAME"/assets "$OUTPUT"/assets 25 | 26 | for f in ${FILES[@]}; do 27 | FILENAME=$(basename "$f") 28 | TITLE=$(grep '^# ' "$f"| head -1 | sed 's;^# ;;g') 29 | if [[ "$TITLE" != "Jsonnet Training Course" ]]; then 30 | TITLE="$TITLE - Jsonnet Training Course" 31 | fi 32 | BODY=$(md2html --github "$f") 33 | jsonnet -S -A "title=$TITLE" -A "body=$BODY" "$DIRNAME"/html.jsonnet > "$OUTPUT/${FILENAME%.md}.html" 34 | done 35 | 36 | echo 37 | 38 | for f in ${FILES[@]}; do 39 | FILENAME=$(basename "$f") 40 | find "$OUTPUT" -type f -exec sed -i "s/${FILENAME}/${FILENAME%.md}.html/" {} \; 41 | done 42 | -------------------------------------------------------------------------------- /html/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %(title)s 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | %(body)s 18 |
19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lessons/about.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local page = c.page; 3 | 4 | page.new( 5 | 'about.md', 6 | 'About', 7 | (importstr './about.md'), 8 | ) 9 | -------------------------------------------------------------------------------- /lessons/about.md: -------------------------------------------------------------------------------- 1 | ## Who is this? 2 | 3 | I'm [Jeroen Op 't Eynde](http://simplistic.be), a software engineer at [Grafana 4 | Labs](https://grafana.com) and a maintainer on [Tanka](https://tanka.dev) and the 5 | [jsonnet-libs/k8s](https://github.com/jsonnet-libs/k8s) project. 6 | 7 | This course is the result countless hours of researching and debating with [Malcolm 8 | Holmes](https://github.com/malcolmholmes), [Tom Braack](https://shorez.de/) and many 9 | colleagues and community members on how to maintain and write more effective Jsonnet. 10 | 11 | ## What are the lessons we've learned after using Jsonnet for years? 12 | 13 | Grafana Labs uses a mixture of Terraform (HCL) and Jsonnet to configure the Cloud 14 | infrastructure and applications on Kubernetes. While new hires are often familiar with 15 | Terraform, they might never have heard of Jsonnet. This made that question the top voted 16 | topic for training sessions by new hires at Grafana Labs. 17 | 18 | While a training session can cover the basics or highlights in ~30min, there is much more 19 | to it. We usually point out to the excellent tutorials on 20 | [jsonnet.org](https://jsonnet.org/learning/tutorial.html) to get started, these explain 21 | very well how Jsonnet works but not necessarily how to work with Jsonnet. This course 22 | attempts to cover the idioms we've been adopting over years of discovering. 23 | 24 | ## Why did we pick Jsonnet? 25 | 26 | With the acquisition of Kausal by Grafana Labs early 2018, we also adopted Ksonnet, 27 | laying down the base for configuration management with Jsonnet. In addition to the 28 | [language comparison](https://jsonnet.org/articles/comparisons.html) on jsonnet.org, 29 | there are a few other advantages. 30 | 31 | The most common tool to manage Kubernetes manifests today is Helm, so why not use that? By 32 | templating the YAML, Helm only allows for one level of abstraction (values.yaml), anything 33 | beyond that requires a change request upstream or more commonly a fork. This causes an 34 | asymmetry between authoring and using Helm charts. 35 | 36 | Jsonnet on the other hand allows for an infinite number of abstractions, the initial 37 | author only needs to worry about their use case, so libraries can be kept quite concise. 38 | If a user wants to do something slightly different, they can simply concatenate the 39 | change to the library. 40 | 41 | > **Helm support in Tanka** 42 | > 43 | > Tanka has built-in [support for Helm charts](https://tanka.dev/helm#helm-support), 44 | > giving the Jsonnet community access to the biggest ecosystem of application definitions 45 | > for Kubernetes. 46 | 47 | ## What benefit do we gain from it? 48 | 49 | Jsonnet is a language about data. By managing configuration with Jsonnet, it essentially 50 | turns into a massive programmable database. The infinite number of abstractions allows it 51 | to create layers. A Deployment is part of an application, which can be included in a cell 52 | (collection of loosely-coupled applications) that gets deployed to a cluster. By extending 53 | the cluster list, new cells can be created and applications automatically become available 54 | without the need to configure and deploy each application individually. 55 | 56 | Each of these concepts has their own configuration attributes that could be managed by 57 | a different team. This means that, with a clean, well defined API between layers of 58 | abstraction, a developer new to Jsonnet needn't learn every layer, they only need to 59 | learn the layer they are interested in, enabling them to become productive much faster 60 | than if they had to code every layer. 61 | -------------------------------------------------------------------------------- /lessons/index.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local page = c.page; 3 | 4 | page.new( 5 | 'index.md', 6 | 'Jsonnet Training Course', 7 | (importstr './index.md') % { 8 | lessons: std.foldl( 9 | function(acc, l) 10 | acc + 11 | '1. [%(title)s](%(filename)s)\n' % l.page 12 | , 13 | import './lessons.jsonnet', 14 | '', 15 | ), 16 | } 17 | , 18 | ) 19 | -------------------------------------------------------------------------------- /lessons/index.md: -------------------------------------------------------------------------------- 1 | Excited as you might be after following the excellent tutorials on 2 | [jsonnet.org](https://jsonnet.org/learning/tutorial.html), it can still be daunting to 3 | actually use Jsonnet in the real world. This hands-on course attempts to cover common 4 | Jsonnet idioms that have been battle tested over several years. 5 | 6 | The examples and use cases in this course are from real world usage instead of working 7 | with arbitrary examples like cocktails or your favorite pets. The step by step examples 8 | show how to use Jsonnet effectively, at the same time explaining the why, covering 9 | pitfalls and other hurdles we might come across. 10 | 11 | This course is a work in progress, the plan is to dive deeper into the Jsonnet ecosystem 12 | with more lessons, exercises and use cases. If you notice a mistake or want to share your 13 | experience, reach out to us on 14 | [Github](https://github.com/jsonnet-libs/jsonnet-training-course). 15 | 16 | ## Getting started 17 | 18 | Jsonnet has two implementations (C++ and Go), the examples should work with either version 19 | above v0.18.0. If you don't know what to choose then [install the Go 20 | implementation](https://github.com/google/go-jsonnet#installation-instructions). 21 | 22 | For package management we'll use jsonnet-bundler, please 23 | [install](https://github.com/jsonnet-bundler/jsonnet-bundler#install) this too. 24 | 25 | ## Lessons 26 | 27 | > Note: A lot of the examples will be around Kubernetes objects, but no worries if you 28 | > don't know how Kubernetes works, this isn't a requirement for understanding the Jsonnet 29 | > examples. 30 | 31 | %(lessons)s 32 | -------------------------------------------------------------------------------- /lessons/lesson1/example1.jsonnet: -------------------------------------------------------------------------------- 1 | { 2 | apiVersion: 'apps/v1', 3 | kind: 'Deployment', 4 | metadata: { 5 | name: 'webserver', 6 | }, 7 | spec: { 8 | replicas: 1, 9 | template: { 10 | spec: { 11 | containers: [ 12 | { 13 | name: 'httpd', 14 | image: 'httpd:2.4', 15 | }, 16 | ], 17 | }, 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /lessons/lesson1/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | apiVersion: 'apps/v1', 4 | kind: 'Deployment', 5 | metadata: { 6 | name: name, 7 | }, 8 | spec: { 9 | replicas: replicas, 10 | template: { 11 | spec: { 12 | containers: [ 13 | { 14 | name: 'httpd', 15 | image: 'httpd:2.4', 16 | }, 17 | ], 18 | }, 19 | }, 20 | }, 21 | }, 22 | }; 23 | 24 | webserver.new('wonderful-webserver') 25 | -------------------------------------------------------------------------------- /lessons/lesson1/example3.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | apiVersion: 'apps/v1', 4 | kind: 'Deployment', 5 | metadata: { 6 | name: name, 7 | }, 8 | spec: { 9 | replicas: replicas, 10 | template: { 11 | spec: { 12 | containers: [ 13 | { 14 | name: 'httpd', 15 | image: 'httpd:2.4', 16 | }, 17 | ], 18 | }, 19 | }, 20 | }, 21 | }, 22 | 23 | withImage(image): { 24 | local containers = super.spec.template.spec.containers, 25 | spec+: { 26 | template+: { 27 | spec+: { 28 | containers: [ 29 | if container.name == 'httpd' 30 | then container { image: image } 31 | else container 32 | for container in containers 33 | ], 34 | }, 35 | }, 36 | }, 37 | }, 38 | }; 39 | 40 | webserver.new('wonderful-webserver') 41 | + webserver.withImage('httpd:2.5') 42 | -------------------------------------------------------------------------------- /lessons/lesson1/example4.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | }, 9 | 10 | deployment: { 11 | apiVersion: 'apps/v1', 12 | kind: 'Deployment', 13 | metadata: { 14 | name: name, 15 | }, 16 | spec: { 17 | replicas: replicas, 18 | template: { 19 | spec: { 20 | containers: [ 21 | base.container, 22 | ], 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | 29 | withImage(image): { 30 | container+: { image: image }, 31 | }, 32 | }; 33 | 34 | webserver.new('wonderful-webserver') 35 | + webserver.withImage('httpd:2.5') 36 | -------------------------------------------------------------------------------- /lessons/lesson1/example5.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | ports: [{ 9 | containerPort: 80, 10 | }], 11 | }, 12 | 13 | deployment: { 14 | apiVersion: 'apps/v1', 15 | kind: 'Deployment', 16 | metadata: { 17 | name: name, 18 | }, 19 | spec: { 20 | replicas: replicas, 21 | template: { 22 | spec: { 23 | containers: [ 24 | base.container, 25 | ], 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | 32 | withImage(image): { 33 | container+: { image: image }, 34 | }, 35 | }; 36 | 37 | webserver.new('wonderful-webserver') 38 | + webserver.withImage('httpd:2.5') 39 | -------------------------------------------------------------------------------- /lessons/lesson1/example6.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | ports: [{ 9 | containerPort: 80, 10 | }], 11 | }, 12 | 13 | deployment: { 14 | apiVersion: 'apps/v1', 15 | kind: 'Deployment', 16 | metadata: { 17 | name: name, 18 | }, 19 | spec: { 20 | replicas: replicas, 21 | template: { 22 | spec: { 23 | containers: [ 24 | base.container, 25 | ], 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | 32 | withImage(image): { 33 | container+: { image: image }, 34 | }, 35 | }; 36 | 37 | webserver.new('wonderful-webserver') 38 | + webserver.withImage('httpd:2.5') 39 | + { 40 | container+: { 41 | ports: [{ 42 | containerPort: 8080, 43 | }], 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /lessons/lesson1/example7.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | ports: [{ 9 | containerPort: 80, 10 | }], 11 | }, 12 | 13 | deployment: { 14 | apiVersion: 'apps/v1', 15 | kind: 'Deployment', 16 | metadata: { 17 | name: name, 18 | }, 19 | spec: { 20 | replicas: replicas, 21 | template: { 22 | spec: { 23 | containers: [ 24 | base.container, 25 | ], 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | 32 | withImage(image): { 33 | container+: { image: image }, 34 | }, 35 | 36 | withImagePullPolicy(policy='Always'): { 37 | container+: { imagePullPolicy: policy }, 38 | }, 39 | }; 40 | 41 | webserver.new('wonderful-webserver') 42 | + webserver.withImage('httpd:2.5') 43 | + webserver.withImagePullPolicy() 44 | -------------------------------------------------------------------------------- /lessons/lesson1/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | [ 3 | example.new('./example1.jsonnet'[2:], importstr './example1.jsonnet', import './example1.jsonnet')+example.withLink(), 4 | example.new('./example2.jsonnet'[2:], importstr './example2.jsonnet', import './example2.jsonnet')+example.withLink(), 5 | example.new('./example3.jsonnet'[2:], importstr './example3.jsonnet', import './example3.jsonnet')+example.withLink(), 6 | example.new('./example4.jsonnet'[2:], importstr './example4.jsonnet', import './example4.jsonnet')+example.withLink(), 7 | example.new('./example5.jsonnet'[2:], importstr './example5.jsonnet', import './example5.jsonnet')+example.withLink(), 8 | example.new('./example6.jsonnet'[2:], importstr './example6.jsonnet', import './example6.jsonnet')+example.withLink(), 9 | example.new('./example7.jsonnet'[2:], importstr './example7.jsonnet', import './example7.jsonnet')+example.withLink(), 10 | example.new('./pitfall1.jsonnet'[2:], importstr './pitfall1.jsonnet', import './pitfall1.jsonnet')+example.withLink(), 11 | example.new('./pitfall2.jsonnet'[2:], importstr './pitfall2.jsonnet', import './pitfall2.jsonnet')+example.withLink(), 12 | example.new('./pitfall3.jsonnet'[2:], importstr './pitfall3.jsonnet', import './pitfall3.jsonnet')+example.withLink(), 13 | example.new('./pitfall4.jsonnet'[2:], importstr './pitfall4.jsonnet', import './pitfall4.jsonnet')+example.withLink(), 14 | ] 15 | -------------------------------------------------------------------------------- /lessons/lesson1/lesson.md: -------------------------------------------------------------------------------- 1 | ### Creating an extensible library 2 | 3 | Let's start with a simple `Deployment` of a webserver: 4 | 5 | %(example1.jsonnet)s 6 | 7 | A `Deployment` needs a number of configuration options, most importantly a unique `name` 8 | and an array of `containers` 9 | 10 | The `name` attribute exists on both the `metadata` and the first container. To refer to 11 | these without ambiguity we can use a dot-notation, so referring can become more explicit 12 | with `metadata.name` and `spec.template.spec.containers[0].name`. 13 | 14 | --- 15 | 16 | Let's wrap this into a small `webserver` library and parameterize the name because 17 | 'webserver' may be a bit too generic: 18 | 19 | %(example2.jsonnet)s 20 | 21 | The `local` keyword makes this part of the code only available within this file, it is 22 | often used for importing libraries from other files, for example `local myapp = import 23 | 'myapp.libsonnet';`. 24 | 25 | The Deployment is wrapped into a `new()` function with a `name` and an optional 26 | `replicas` arguments, this configures `metadata.name` and `spec.replicas` 27 | respectively. 28 | 29 | --- 30 | 31 | Let's add another function to modify the image of the httpd container: 32 | 33 | %(example3.jsonnet)s 34 | 35 | `withImage` is an optional 'mixin' function to modify the `Deployment`, notice how the 36 | `new()` function did not have to change to make this possible. The function is intended to 37 | be concatenated to the `Deployment` object created by `new()`, it uses the `super` keyword 38 | to access the `container` attribute. 39 | 40 | As the `container` attribute is an array, it is not intuitive to modify an single entry. 41 | We have to loop over the array, find the matching container and apply a patch. This is 42 | quite verbose and hard to read. 43 | 44 | 45 | --- 46 | 47 | Let's make the container a bit more accessible by moving it out of the `Deployment`: 48 | 49 | %(example4.jsonnet)s 50 | 51 | This makes the code a lot more succinct, no more loops and conditionals needed. The code 52 | now reads more like a declarative document. 53 | 54 | This introduces the `::` syntax, it hides an attribute from the final output but allows 55 | for future changes to be applied to them. The `withImage` function uses `+:`, this 56 | concatenates the image patch to the `container` attribute, using a single colon it 57 | maintains the same hidden visibility as the `Deployment` object has defined. 58 | 59 | The local `base` variable refers to the `self` keyword which returns the current object 60 | (first curly brackets it encounters). The `deployment` then refers to `self.container`, 61 | as `self` is late-bound any changes to `container` will be reflected in `deployment`. 62 | 63 | --- 64 | 65 | To expose the webserver, a port is configured below. Now imagine that you are not the 66 | author of this library and want to change the `ports` attribute. 67 | 68 | %(example6.jsonnet)s 69 | 70 | The author has not provided a function for that however, unlike Helm charts, it is not 71 | necessary to fork the library to make this change. Jsonnet allows the user to change any 72 | attribute after the fact by concatenating a 'patch'. The `container+:` will maintain the 73 | visibility of the `container` attribute while `ports:` will change the value of 74 | `container.ports`. 75 | 76 | This trait of Jsonnet keeps a balance between library authors providing a useful library 77 | and users to extend it easily. Authors don't need to think about every use case out 78 | there, they can apply [YAGNI](https://www.martinfowler.com/bliki/Yagni.html) and keep the 79 | library code terse and maintainable without sacrificing extensibility. 80 | 81 | --- 82 | 83 | ### Common pitfalls when creating libraries 84 | 85 | #### Builder pattern 86 | 87 | Avoid the 'builder' pattern: 88 | 89 | %(pitfall1.jsonnet)s 90 | 91 | Notice the odd `withImage():: self + {}` structure within `new()`. 92 | 93 | This practice nests functions in the newly created object, allowing the user to 'chain' 94 | functions to modify `self`. However this comes at a performance impact in the Jsonnet 95 | interpreter and should be avoided. 96 | 97 | #### `_config` and `_images` pattern 98 | 99 | A common pattern involves libraries that use the `_config` and `_images` keys. This 100 | supposedly attempts to differentiate between 'public' and 'private' APIs on libraries. 101 | However the underscore prefix has no real meaning in Jsonnet, at best it is a convention 102 | with implied meaning. 103 | 104 | Applying the convention to above library would make it look like this: 105 | 106 | %(pitfall2.jsonnet)s 107 | 108 | This convention attempts to provide a 'stable' API through the `_config` and `_images` 109 | parameters, implying that patching other attributes will not be supported. However the 110 | 'public' attributes (indicated by the `_` prefix) are not more public or private than the 111 | 'private' attributes as they exists the same space. To make the `name` parameter 112 | a required argument, an `error` is returned if it is not set in `_config`. 113 | 114 | It is comparable to the `values.yaml` in Helm charts, however Jsonnet does not face the 115 | same limitations and as mentioned before users can modify the final output after the fact 116 | either way. 117 | 118 | --- 119 | 120 | This pattern also has an impact on extensibility. When introducing a new attribute, the 121 | author needs to take into account that users might not want the same default. 122 | 123 | %(pitfall3.jsonnet)s 124 | 125 | This can be accomplished with imperative statements, however these pile up over time and 126 | make the library brittle and hard to read. In this example the default for 127 | `imagePullPolicy` is `null`, the author avoids adding an additional boolean parameter 128 | (`_config.imagePullPolicyEnabled` for example) with the drawback that no default value can 129 | be provided. 130 | 131 | --- 132 | 133 | In the object-oriented library this can be done with a new function: 134 | 135 | %(example7.jsonnet)s 136 | 137 | The `withImagePullPolicy()` function provides a more declarative approach to configure 138 | this new option. In contrast to the approach above this new feature does not have to 139 | modify the existing code, keeping a strong separation of concerns and reduces the risk of 140 | introducing bugs. 141 | 142 | At the same time functions provide a clean API for the end user and the author alike, 143 | replacing the implied convention with declarative statements with required and optional 144 | arguments. Calling the function implies that the user wants to set a value, the optional 145 | arguments provides a default value `Always` to get the user going. 146 | 147 | #### Use of `$` 148 | 149 | As you might have noticed, the `$` keyword is not used in any of these examples. In many 150 | libraries it is used to refer to variables that still need to be set. 151 | 152 | %(pitfall4.jsonnet)s 153 | 154 | This pattern makes it hard to determine which library is consuming which attribute. On top 155 | of that libraries can influence each other unintentionally. 156 | 157 | In this example: 158 | - `_config.httpd_replicas` is only consumed by `webserver2` while it seems to apply to 159 | both. 160 | - `_image.httpd` is set on both libraries, however `webserver2` overrides the image of 161 | `webserver1` as it was concatenated later. 162 | 163 | This practice comes from an anti-pattern to merge several libraries on top of each other 164 | and refer to attributes that need to be set elsewhere. Or in other words, `$` promotes the 165 | concept known as 'globals' in other programming libraries. It is best to avoid this as it 166 | leads to spaghetti code. 167 | -------------------------------------------------------------------------------- /lessons/lesson1/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = import './examples.jsonnet'; 5 | 6 | lesson.new( 7 | 'lesson1', 8 | 'Write an extensible library', 9 | ||| 10 | Jsonnet gives us a lot of freedom to organize our libraries, there is no right or 11 | wrong, however a well-organized library can get you a long way. While applying common 12 | software development best-practices, we'll come up with an extensible library to 13 | deploy a webserver on Kubernetes. 14 | |||, 15 | [ 16 | "Write object-oriented with 'mixin' functions", 17 | 'Develop for extensibility with `::`, `+:` and objects rather than arrays', 18 | 'Properly use keywords such as `local`, `super`, `self`, `null` and `$`', 19 | 'Know how to avoid common pitfalls', 20 | ], 21 | (importstr './lesson.md') % 22 | std.foldr( 23 | function(e, acc) 24 | acc { [e.filename]: e.render }, 25 | examples, 26 | {} 27 | ), 28 | ||| 29 | By following an object-oriented approach, it is possible to build extensible jsonnet 30 | libraries. They can be extended infinitely and in such a way that it doesn't impact 31 | existing uses, providing backwards compatibility. 32 | 33 | The pitfalls show a few patterns that exist in the wild but should be avoided and 34 | refactored as they become unsustainable in the long term. 35 | |||, 36 | ) 37 | -------------------------------------------------------------------------------- /lessons/lesson1/pitfall1.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | }, 9 | 10 | deployment: { 11 | apiVersion: 'apps/v1', 12 | kind: 'Deployment', 13 | metadata: { 14 | name: name, 15 | }, 16 | spec: { 17 | replicas: replicas, 18 | template: { 19 | spec: { 20 | containers: [ 21 | base.container, 22 | ], 23 | }, 24 | }, 25 | }, 26 | }, 27 | 28 | withImage(image):: self + { 29 | container+: { image: image }, 30 | }, 31 | }, 32 | }; 33 | 34 | webserver.new('wonderful-webserver').withImage('httpd:2.5') 35 | -------------------------------------------------------------------------------- /lessons/lesson1/pitfall2.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | local base = self, 3 | 4 | _config:: { 5 | name: error 'provide name', 6 | replicas: 1, 7 | }, 8 | 9 | _images:: { 10 | httpd: 'httpd:2.4', 11 | }, 12 | 13 | container:: { 14 | name: 'httpd', 15 | image: base._images.httpd, 16 | }, 17 | 18 | deployment: { 19 | apiVersion: 'apps/v1', 20 | kind: 'Deployment', 21 | metadata: { 22 | name: base._config.name, 23 | }, 24 | spec: { 25 | replicas: base._config.replicas, 26 | template: { 27 | spec: { 28 | containers: [ 29 | base.container, 30 | ], 31 | }, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | webserver { 38 | _config+: { 39 | name: 'wonderful-webserver', 40 | }, 41 | _images+: { 42 | httpd: 'httpd:2.5', 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /lessons/lesson1/pitfall3.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | local base = self, 3 | 4 | _config:: { 5 | name: error 'provide name', 6 | replicas: 1, 7 | imagePullPolicy: null, 8 | }, 9 | 10 | _images:: { 11 | httpd: 'httpd:2.4', 12 | }, 13 | 14 | container:: { 15 | name: 'httpd', 16 | image: base._images.httpd, 17 | } + ( 18 | if base._config.imagePullPolicy != null 19 | then { imagePullPolicy: base._config.imagePullPolicy } 20 | else {} 21 | ), 22 | 23 | deployment: { 24 | apiVersion: 'apps/v1', 25 | kind: 'Deployment', 26 | metadata: { 27 | name: base._config.name, 28 | }, 29 | spec: { 30 | replicas: base._config.replicas, 31 | template: { 32 | spec: { 33 | containers: [ 34 | base.container, 35 | ], 36 | }, 37 | }, 38 | }, 39 | }, 40 | }; 41 | 42 | webserver { 43 | _config+: { 44 | name: 'wonderful-webserver', 45 | imagePullPolicy: 'Always', 46 | }, 47 | _images+: { 48 | httpd: 'httpd:2.5', 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /lessons/lesson1/pitfall4.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver1 = { 2 | _images:: { 3 | httpd: 'httpd:2.4', 4 | }, 5 | webserver1: { 6 | apiVersion: 'apps/v1', 7 | kind: 'Deployment', 8 | metadata: { 9 | name: 'webserver1', 10 | }, 11 | spec: { 12 | replicas: 1, 13 | template: { 14 | spec: { 15 | containers: [{ 16 | name: 'httpd', 17 | image: $._images.httpd, 18 | }], 19 | }, 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | local webserver2 = { 26 | _images:: { 27 | httpd: 'httpd:2.5', 28 | }, 29 | webserver2: { 30 | apiVersion: 'apps/v1', 31 | kind: 'Deployment', 32 | metadata: { 33 | name: 'webserver2', 34 | }, 35 | spec: { 36 | replicas: $._config.httpd_replicas, 37 | template: { 38 | spec: { 39 | containers: [{ 40 | name: 'httpd', 41 | image: $._images.httpd, 42 | }], 43 | }, 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | webserver1 + webserver2 + { 50 | _config:: { 51 | httpd_replicas: 1, 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /lessons/lesson2/example1/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [], 4 | "legacyImports": true 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/xtd.git", 8 | "subdir": "" 9 | } 10 | }, 11 | "version": "master" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/xtd.git", 8 | "subdir": "" 9 | } 10 | }, 11 | "version": "803739029925cf31b0e3c6db2f4aae09b0378a6e", 12 | "sum": "d/c+3om56mfddeYWrsxOwsrlH008BmX/5NoquXMj0+g=" 13 | } 14 | ], 15 | "legacyImports": false 16 | } 17 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/.gitignore: -------------------------------------------------------------------------------- 1 | .jekyll-cache 2 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 grafana, sh0rez 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | jsonnet test.jsonnet 4 | 5 | .PHONY: docs 6 | docs: 7 | docsonnet main.libsonnet 8 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/README.md: -------------------------------------------------------------------------------- 1 | # `xtd` 2 | 3 | `xtd` aims to collect useful functions not included in the Jsonnet standard library (`std`). 4 | 5 | ## Install 6 | 7 | ```console 8 | jb install github.com/jsonnet-libs/xtd 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```jsonnet 14 | local xtd = import "github.com/jsonnet-libs/xtd/main.libsonnet" 15 | ``` 16 | 17 | ## Docs 18 | 19 | [docs](docs/README.md) 20 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/ascii.libsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'doc-util/main.libsonnet'; 2 | 3 | { 4 | '#': d.pkg( 5 | name='ascii', 6 | url='github.com/jsonnet-libs/xtd/ascii.libsonnet', 7 | help='`ascii` implements helper functions for ascii characters', 8 | ), 9 | 10 | local cp(c) = std.codepoint(c), 11 | 12 | '#isLower':: d.fn( 13 | '`isLower` reports whether ASCII character `c` is a lower case letter', 14 | [d.arg('c', d.T.string)] 15 | ), 16 | isLower(c): 17 | if cp(c) >= 97 && cp(c) < 123 18 | then true 19 | else false, 20 | 21 | '#isUpper':: d.fn( 22 | '`isUpper` reports whether ASCII character `c` is a upper case letter', 23 | [d.arg('c', d.T.string)] 24 | ), 25 | isUpper(c): 26 | if cp(c) >= 65 && cp(c) < 91 27 | then true 28 | else false, 29 | 30 | '#isNumber':: d.fn( 31 | '`isNumber` reports whether character `c` is a number.', 32 | [d.arg('c', d.T.string)] 33 | ), 34 | isNumber(c): 35 | if std.isNumber(c) || (cp(c) >= 48 && cp(c) < 58) 36 | then true 37 | else false, 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/camelcase.libsonnet: -------------------------------------------------------------------------------- 1 | local xtd = import './main.libsonnet'; 2 | local d = import 'doc-util/main.libsonnet'; 3 | 4 | { 5 | '#': d.pkg( 6 | name='camelcase', 7 | url='github.com/jsonnet-libs/xtd/camelcase.libsonnet', 8 | help='`camelcase` can split camelCase words into an array of words.', 9 | ), 10 | 11 | '#split':: d.fn( 12 | ||| 13 | `split` splits a camelcase word and returns an array of words. It also supports 14 | digits. Both lower camel case and upper camel case are supported. It only supports 15 | ASCII characters. 16 | For more info please check: http://en.wikipedia.org/wiki/CamelCase 17 | Based on https://github.com/fatih/camelcase/ 18 | |||, 19 | [d.arg('src', d.T.string)] 20 | ), 21 | split(src): 22 | if src == '' 23 | then [''] 24 | else 25 | local runes = std.foldl( 26 | function(acc, r) 27 | acc { 28 | local class = 29 | if xtd.ascii.isNumber(r) 30 | then 1 31 | else if xtd.ascii.isLower(r) 32 | then 2 33 | else if xtd.ascii.isUpper(r) 34 | then 3 35 | else 4, 36 | 37 | lastClass:: class, 38 | 39 | runes: 40 | if class == super.lastClass 41 | then super.runes[:std.length(super.runes) - 1] 42 | + [super.runes[std.length(super.runes) - 1] + r] 43 | else super.runes + [r], 44 | }, 45 | [src[i] for i in std.range(0, std.length(src) - 1)], 46 | { lastClass:: 0, runes: [] } 47 | ).runes; 48 | 49 | local fixRunes = 50 | std.foldl( 51 | function(runes, i) 52 | if xtd.ascii.isUpper(runes[i][0]) 53 | && xtd.ascii.isLower(runes[i + 1][0]) 54 | && !xtd.ascii.isNumber(runes[i + 1][0]) 55 | && runes[i][0] != ' ' 56 | && runes[i + 1][0] != ' ' 57 | then 58 | std.mapWithIndex( 59 | function(index, r) 60 | if index == i + 1 61 | then runes[i][std.length(runes[i]) - 1:] + r 62 | else 63 | if index == i 64 | then r[:std.length(r) - 1] 65 | else r 66 | , runes 67 | ) 68 | else runes 69 | , 70 | [i for i in std.range(0, std.length(runes) - 2)], 71 | runes 72 | ); 73 | 74 | [ 75 | r 76 | for r in fixRunes 77 | if r != '' 78 | ], 79 | 80 | } 81 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | _site 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "github-pages", group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: / 3 | --- 4 | 5 | # package xtd 6 | 7 | ```jsonnet 8 | local xtd = import "github.com/jsonnet-libs/xtd/main.libsonnet" 9 | ``` 10 | 11 | `xtd` aims to collect useful functions not included in the Jsonnet standard library (`std`). 12 | 13 | This package serves as a test field for functions intended to be contributed to `std` 14 | in the future, but also provides a place for less general, yet useful utilities. 15 | 16 | 17 | ## Subpackages 18 | 19 | * [ascii](ascii.md) 20 | * [camelcase](camelcase.md) 21 | * [inspect](inspect.md) 22 | * [url](url.md) -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | baseurl: /xtd 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/ascii.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /ascii/ 3 | --- 4 | 5 | # package ascii 6 | 7 | ```jsonnet 8 | local ascii = import "github.com/jsonnet-libs/xtd/ascii.libsonnet" 9 | ``` 10 | 11 | `ascii` implements helper functions for ascii characters 12 | 13 | ## Index 14 | 15 | * [`fn isLower(c)`](#fn-islower) 16 | * [`fn isNumber(c)`](#fn-isnumber) 17 | * [`fn isUpper(c)`](#fn-isupper) 18 | 19 | ## Fields 20 | 21 | ### fn isLower 22 | 23 | ```ts 24 | isLower(c) 25 | ``` 26 | 27 | `isLower` reports whether ASCII character `c` is a lower case letter 28 | 29 | ### fn isNumber 30 | 31 | ```ts 32 | isNumber(c) 33 | ``` 34 | 35 | `isNumber` reports whether character `c` is a number. 36 | 37 | ### fn isUpper 38 | 39 | ```ts 40 | isUpper(c) 41 | ``` 42 | 43 | `isUpper` reports whether ASCII character `c` is a upper case letter -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/camelcase.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /camelcase/ 3 | --- 4 | 5 | # package camelcase 6 | 7 | ```jsonnet 8 | local camelcase = import "github.com/jsonnet-libs/xtd/camelcase.libsonnet" 9 | ``` 10 | 11 | `camelcase` can split camelCase words into an array of words. 12 | 13 | ## Index 14 | 15 | * [`fn split(src)`](#fn-split) 16 | 17 | ## Fields 18 | 19 | ### fn split 20 | 21 | ```ts 22 | split(src) 23 | ``` 24 | 25 | `split` splits a camelcase word and returns an array of words. It also supports 26 | digits. Both lower camel case and upper camel case are supported. It only supports 27 | ASCII characters. 28 | For more info please check: http://en.wikipedia.org/wiki/CamelCase 29 | Based on https://github.com/fatih/camelcase/ 30 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/inspect.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /inspect/ 3 | --- 4 | 5 | # package inspect 6 | 7 | ```jsonnet 8 | local inspect = import "github.com/jsonnet-libs/xtd/inspect.libsonnet" 9 | ``` 10 | 11 | `inspect` implements helper functions for inspecting Jsonnet 12 | 13 | ## Index 14 | 15 | * [`fn inspect(object, maxDepth)`](#fn-inspect) 16 | 17 | ## Fields 18 | 19 | ### fn inspect 20 | 21 | ```ts 22 | inspect(object, maxDepth) 23 | ``` 24 | 25 | `inspect` reports the structure of a Jsonnet object with a recursion depth of 26 | `maxDepth` (default maxDepth=10). 27 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/docs/url.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /url/ 3 | --- 4 | 5 | # package url 6 | 7 | ```jsonnet 8 | local url = import "github.com/jsonnet-libs/xtd/url.libsonnet" 9 | ``` 10 | 11 | `url` implements URL escaping and query building 12 | 13 | ## Index 14 | 15 | * [`fn encodeQuery(params)`](#fn-encodequery) 16 | * [`fn escapeString(str, excludedChars)`](#fn-escapestring) 17 | 18 | ## Fields 19 | 20 | ### fn encodeQuery 21 | 22 | ```ts 23 | encodeQuery(params) 24 | ``` 25 | 26 | `encodeQuery` takes an object of query parameters and returns them as an escaped `key=value` string 27 | 28 | ### fn escapeString 29 | 30 | ```ts 31 | escapeString(str, excludedChars) 32 | ``` 33 | 34 | `escapeString` escapes the given string so it can be safely placed inside an URL, replacing special characters with `%XX` sequences -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/inspect.libsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'doc-util/main.libsonnet'; 2 | 3 | { 4 | '#': d.pkg( 5 | name='inspect', 6 | url='github.com/jsonnet-libs/xtd/inspect.libsonnet', 7 | help='`inspect` implements helper functions for inspecting Jsonnet', 8 | ), 9 | 10 | 11 | '#inspect':: d.fn( 12 | ||| 13 | `inspect` reports the structure of a Jsonnet object with a recursion depth of 14 | `maxDepth` (default maxDepth=10). 15 | |||, 16 | [ 17 | d.arg('object', d.T.object), 18 | d.arg('maxDepth', d.T.number), 19 | //d.arg('depth', d.T.number), // used for recursion, not exposing in docs 20 | ] 21 | ), 22 | 23 | local this = self, 24 | inspect(object, maxDepth=10, depth=0): 25 | std.foldl( 26 | function(acc, p) 27 | acc + ( 28 | if std.isObject(object[p]) 29 | && depth != maxDepth 30 | then { [p]+: 31 | this.inspect( 32 | object[p], 33 | maxDepth, 34 | depth + 1 35 | ) } 36 | else { 37 | [ 38 | (if !std.objectHas(object, p) 39 | then 'hidden_' 40 | else '') 41 | + (if std.isFunction(object[p]) 42 | then 'functions' 43 | else 'fields') 44 | ]+: [p], 45 | } 46 | ), 47 | std.objectFieldsAll(object), 48 | {} 49 | ), 50 | } 51 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/main.libsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'doc-util/main.libsonnet'; 2 | 3 | { 4 | '#': d.pkg( 5 | name='xtd', 6 | url='github.com/jsonnet-libs/xtd/main.libsonnet', 7 | help=||| 8 | `xtd` aims to collect useful functions not included in the Jsonnet standard library (`std`). 9 | 10 | This package serves as a test field for functions intended to be contributed to `std` 11 | in the future, but also provides a place for less general, yet useful utilities. 12 | |||, 13 | ), 14 | 15 | ascii: (import './ascii.libsonnet'), 16 | camelcase: (import './camelcase.libsonnet'), 17 | inspect: (import './inspect.libsonnet'), 18 | url: (import './url.libsonnet'), 19 | } 20 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/test.jsonnet: -------------------------------------------------------------------------------- 1 | local xtd = import './main.libsonnet'; 2 | 3 | local trace(v) = std.trace(v, v); 4 | 5 | // TestEscapeString 6 | local TestEscapeString = 7 | local name(case) = 'TestEscapeString:%s failed' % case; 8 | 9 | assert xtd.url.escapeString('') 10 | == '' 11 | : name('empty'); 12 | 13 | assert xtd.url.escapeString('abc') 14 | == 'abc' 15 | : name('abc'); 16 | 17 | assert xtd.url.escapeString('one two') 18 | == 'one%20two' 19 | : name('space'); 20 | 21 | assert xtd.url.escapeString('10%') 22 | == '10%25' 23 | : name('percent'); 24 | 25 | assert xtd.url.escapeString(" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;") 26 | == '%20%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B' 27 | : name('complex'); 28 | 29 | assert xtd.url.escapeString('hello, world', [',']) 30 | == 'hello,%20world' 31 | : name('exclusions'); 32 | 33 | assert xtd.url.escapeString('hello, world,&', [',', '&']) 34 | == 'hello,%20world,&' 35 | : name('multiple exclusions'); 36 | true; 37 | 38 | local TestEncodeQuery = 39 | local name(case) = 'TestEncodeQuery:%s failed' % case; 40 | 41 | assert xtd.url.encodeQuery({}) == '' : name('empty'); 42 | 43 | assert xtd.url.encodeQuery({ q: 'puppies', oe: 'utf8' }) 44 | == 'oe=utf8&q=puppies' 45 | : name('simple'); 46 | true; 47 | 48 | local TestCamelCaseSplit = 49 | local name(case) = 'TestCamelCaseSplit:%s failed' % case; 50 | assert xtd.camelcase.split('') 51 | == [''] 52 | : name('nostring'); 53 | assert xtd.camelcase.split('lowercase') 54 | == ['lowercase'] 55 | : name('lowercase'); 56 | assert xtd.camelcase.split('Class') 57 | == ['Class'] 58 | : name('Class'); 59 | assert xtd.camelcase.split('MyClass') 60 | == ['My', 'Class'] 61 | : name('MyClass'); 62 | assert xtd.camelcase.split('MyC') 63 | == ['My', 'C'] 64 | : name('MyC'); 65 | assert xtd.camelcase.split('HTML') 66 | == ['HTML'] 67 | : name('HTML'); 68 | assert xtd.camelcase.split('PDFLoader') 69 | == ['PDF', 'Loader'] 70 | : name('PDFLoader'); 71 | assert xtd.camelcase.split('AString') 72 | == ['A', 'String'] 73 | : name('AString'); 74 | assert xtd.camelcase.split('SimpleXMLParser') 75 | == ['Simple', 'XML', 'Parser'] 76 | : name('SimpleXMLParser'); 77 | assert xtd.camelcase.split('vimRPCPlugin') 78 | == ['vim', 'RPC', 'Plugin'] 79 | : name('vimRPCPlugin'); 80 | assert xtd.camelcase.split('GL11Version') 81 | == ['GL', '11', 'Version'] 82 | : name('GL11Version'); 83 | assert xtd.camelcase.split('99Bottles') 84 | == ['99', 'Bottles'] 85 | : name('99Bottles'); 86 | assert xtd.camelcase.split('May5') 87 | == ['May', '5'] 88 | : name('May5'); 89 | assert xtd.camelcase.split('BFG9000') 90 | == ['BFG', '9000'] 91 | : name('BFG9000'); 92 | assert xtd.camelcase.split('Two spaces') 93 | == ['Two', ' ', 'spaces'] 94 | : name('Two spaces'); 95 | assert xtd.camelcase.split('Multiple Random spaces') 96 | == ['Multiple', ' ', 'Random', ' ', 'spaces'] 97 | : name('Multiple Random spaces'); 98 | 99 | // TODO: find or create is(Upper|Lower) for non-ascii characters 100 | // Something like this for Jsonnet: 101 | // https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/unicode/tables.go 102 | //assert xtd.camelcase.split('BöseÜberraschung') 103 | // == ['Böse', 'Überraschung'] 104 | // : name('BöseÜberraschung'); 105 | 106 | // This doesn't even render in Jsonnet 107 | //assert xtd.camelcase.split("BadUTF8\xe2\xe2\xa1") 108 | // == ["BadUTF8\xe2\xe2\xa1"] 109 | // : name("BadUTF8\xe2\xe2\xa1"); 110 | true; 111 | 112 | local TestInspect = 113 | local name(case) = 'TestInspect:%s failed' % case; 114 | 115 | assert xtd.inspect.inspect({}) 116 | == {} 117 | : name('emptyobject'); 118 | 119 | assert xtd.inspect.inspect({ 120 | key: 'value', 121 | hidden_key:: 'value', 122 | func(value): value, 123 | hidden_func(value):: value, 124 | }) 125 | == { 126 | fields: ['key'], 127 | hidden_fields: ['hidden_key'], 128 | functions: ['func'], 129 | hidden_functions: ['hidden_func'], 130 | } 131 | : name('flatObject'); 132 | 133 | assert xtd.inspect.inspect({ 134 | nested: { 135 | key: 'value', 136 | hidden_key:: 'value', 137 | func(value): value, 138 | hidden_func(value):: value, 139 | }, 140 | key: 'value', 141 | hidden_func(value):: value, 142 | }) 143 | == { 144 | nested: { 145 | fields: ['key'], 146 | hidden_fields: ['hidden_key'], 147 | functions: ['func'], 148 | hidden_functions: ['hidden_func'], 149 | }, 150 | fields: ['key'], 151 | hidden_functions: ['hidden_func'], 152 | } 153 | : name('nestedObject'); 154 | 155 | assert xtd.inspect.inspect({ 156 | key: 'value', 157 | nested: { 158 | key: 'value', 159 | nested: { 160 | key: 'value', 161 | }, 162 | }, 163 | }, maxDepth=1) 164 | == { 165 | fields: ['key'], 166 | nested: { 167 | fields: ['key', 'nested'], 168 | }, 169 | } 170 | : name('maxRecursionDepth'); 171 | true; 172 | 173 | 174 | true 175 | && TestEscapeString 176 | && TestEncodeQuery 177 | && TestCamelCaseSplit 178 | && TestInspect 179 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/github.com/jsonnet-libs/xtd/url.libsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'doc-util/main.libsonnet'; 2 | 3 | { 4 | '#': d.pkg( 5 | name='url', 6 | url='github.com/jsonnet-libs/xtd/url.libsonnet', 7 | help='`url` implements URL escaping and query building', 8 | ), 9 | 10 | '#escapeString': d.fn('`escapeString` escapes the given string so it can be safely placed inside an URL, replacing special characters with `%XX` sequences', [d.arg('str', d.T.string), d.arg('excludedChars', d.T.array)]), 11 | escapeString(str, excludedChars=[]):: 12 | local allowedChars = '0123456789abcdefghijklmnopqrstuvwqxyzABCDEFGHIJKLMNOPQRSTUVWQXYZ'; 13 | local utf8(char) = std.foldl(function(a, b) a + '%%%02X' % b, std.encodeUTF8(char), ''); 14 | local escapeChar(char) = if std.member(excludedChars, char) || std.member(allowedChars, char) then char else utf8(char); 15 | std.join('', std.map(escapeChar, std.stringChars(str))), 16 | 17 | '#encodeQuery': d.fn('`encodeQuery` takes an object of query parameters and returns them as an escaped `key=value` string', [d.arg('params', d.T.object)]), 18 | encodeQuery(params):: 19 | local fmtParam(p) = '%s=%s' % [self.escapeString(p), self.escapeString(params[p])]; 20 | std.join('&', std.map(fmtParam, std.objectFields(params))), 21 | } 22 | -------------------------------------------------------------------------------- /lessons/lesson2/example2/vendor/xtd: -------------------------------------------------------------------------------- 1 | github.com/jsonnet-libs/xtd -------------------------------------------------------------------------------- /lessons/lesson2/example3/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example3/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/xtd.git", 8 | "subdir": "" 9 | } 10 | }, 11 | "version": "803739029925cf31b0e3c6db2f4aae09b0378a6e" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /lessons/lesson2/example4/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example4/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/istio-libsonnet.git", 8 | "subdir": "1.13" 9 | } 10 | }, 11 | "version": "main", 12 | "name": "istio-lib" 13 | } 14 | ], 15 | "legacyImports": true 16 | } 17 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/istio-libsonnet.git", 8 | "subdir": "1.13" 9 | } 10 | }, 11 | "version": "main", 12 | "name": "istio-lib" 13 | }, 14 | { 15 | "source": { 16 | "git": { 17 | "remote": "https://github.com/jsonnet-libs/xtd.git", 18 | "subdir": "" 19 | } 20 | }, 21 | "version": "803739029925cf31b0e3c6db2f4aae09b0378a6e" 22 | } 23 | ], 24 | "legacyImports": true 25 | } 26 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/lib/istiolib.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/istio-libsonnet/1.13/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/usage1.jsonnet: -------------------------------------------------------------------------------- 1 | local xtd = import 'github.com/jsonnet-libs/xtd/main.libsonnet'; 2 | 3 | xtd.ascii.isNumber('2') 4 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/usage2.jsonnet: -------------------------------------------------------------------------------- 1 | local xtd = import 'xtd/main.libsonnet'; 2 | 3 | xtd.ascii.isNumber('2') 4 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/usage3.jsonnet: -------------------------------------------------------------------------------- 1 | local istiolib = import 'istio-lib/main.libsonnet'; 2 | 3 | istiolib.networking.v1beta1.virtualService.new('test') 4 | -------------------------------------------------------------------------------- /lessons/lesson2/example5/usage4.jsonnet: -------------------------------------------------------------------------------- 1 | local istiolib = import 'istiolib.libsonnet'; 2 | 3 | istiolib.networking.v1beta1.virtualService.new('test') 4 | -------------------------------------------------------------------------------- /lessons/lesson2/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | 3 | [ 4 | example.new( 5 | 'example1/jsonnetfile.json', 6 | importstr './example1/jsonnetfile.json', 7 | type='json' 8 | ), 9 | 10 | example.new( 11 | 'example2/jsonnetfile.json', 12 | importstr './example2/jsonnetfile.json', 13 | type='json' 14 | ), 15 | 16 | example.new( 17 | 'example2/jsonnetfile.lock.json', 18 | importstr './example2/jsonnetfile.lock.json', 19 | type='json' 20 | ), 21 | 22 | example.new( 23 | 'example3/jsonnetfile.json', 24 | importstr './example3/jsonnetfile.json', 25 | type='json' 26 | ), 27 | 28 | example.new( 29 | 'example3/.gitignore', 30 | importstr './example3/.gitignore', 31 | type='gitignore' 32 | ), 33 | 34 | example.new( 35 | 'example4/jsonnetfile.json', 36 | importstr './example4/jsonnetfile.json', 37 | type='json' 38 | ), 39 | 40 | example.new( 41 | 'example5/usage1.jsonnet', 42 | importstr './example5/usage1.jsonnet', 43 | ), 44 | 45 | example.new( 46 | 'example5/usage2.jsonnet', 47 | importstr './example5/usage2.jsonnet', 48 | ), 49 | 50 | example.new( 51 | 'example5/usage3.jsonnet', 52 | importstr './example5/usage3.jsonnet', 53 | ), 54 | 55 | example.new( 56 | 'example5/usage4.jsonnet', 57 | importstr './example5/usage4.jsonnet', 58 | ), 59 | 60 | example.new( 61 | 'example5/lib/istiolib.libsonnet', 62 | importstr './example5/lib/istiolib.libsonnet', 63 | ), 64 | ] 65 | -------------------------------------------------------------------------------- /lessons/lesson2/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = import './examples.jsonnet'; 5 | 6 | lesson.new( 7 | 'lesson2', 8 | 'Understanding Package management', 9 | ||| 10 | There are a ton of Jsonnet libraries out there, ranging from big generated libraries 11 | to manually curated for a very specific purpose. Let's have a look at how to find and 12 | vendor them. 13 | |||, 14 | [ 15 | 'Find libraries', 16 | 'Install and update with jsonnet-bundler', 17 | 'Import a library on the `JSONNET_PATH`', 18 | 'Handle common use cases', 19 | ], 20 | (importstr './lesson.md') % 21 | std.foldr( 22 | function(e, acc) 23 | acc { [e.filename]: e.render }, 24 | examples, 25 | {} 26 | ), 27 | ||| 28 | Finding libraries and package managemet can be cumbersome, nonetheless 29 | jsonnet-bundler makes it a bit easier to work with the distributed ecosystem. 30 | Additionally `JSONNET_PATH` offers a level of flexibility to work around the package 31 | management shortcomming. 32 | |||, 33 | ) 34 | -------------------------------------------------------------------------------- /lessons/lesson3/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = { 2 | new(name, replicas=1): { 3 | local base = self, 4 | 5 | container:: { 6 | name: 'httpd', 7 | image: 'httpd:2.4', 8 | }, 9 | 10 | deployment: { 11 | apiVersion: 'apps/v1', 12 | kind: 'Deployment', 13 | metadata: { 14 | name: name, 15 | }, 16 | spec: { 17 | replicas: replicas, 18 | template: { 19 | spec: { 20 | containers: [ 21 | base.container, 22 | ], 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | 29 | withImage(image): { 30 | container+: { image: image }, 31 | }, 32 | }; 33 | 34 | webserver.new('wonderful-webserver') 35 | + webserver.withImage('httpd:2.5') 36 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | k.core.v1.container.new('container-name', 'container-image') 4 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | local webserver = { 4 | new(name, replicas=1): { 5 | local base = self, 6 | 7 | container:: 8 | k.core.v1.container.new('httpd', 'httpd:2.4'), 9 | 10 | deployment: { 11 | apiVersion: 'apps/v1', 12 | kind: 'Deployment', 13 | metadata: { 14 | name: name, 15 | }, 16 | spec: { 17 | replicas: replicas, 18 | template: { 19 | spec: { 20 | containers: [ 21 | base.container, 22 | ], 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | 29 | withImage(image): { 30 | container+: 31 | k.core.v1.container.withImage(image), 32 | }, 33 | }; 34 | 35 | webserver.new('wonderful-webserver') 36 | + webserver.withImage('httpd:2.5') 37 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/example3.jsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | local webserver = { 4 | new(name, replicas=1): { 5 | container:: 6 | k.core.v1.container.new('httpd', 'httpd:2.4'), 7 | 8 | deployment: 9 | k.apps.v1.deployment.new( 10 | name, 11 | replicas, 12 | [self.container] 13 | ), 14 | }, 15 | 16 | withImage(image): { 17 | container+: 18 | k.core.v1.container.withImage(image), 19 | }, 20 | }; 21 | 22 | webserver.new('wonderful-webserver') 23 | + webserver.withImage('httpd:2.5') 24 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/example4.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = import 'webserver/main.libsonnet'; 2 | 3 | webserver.new('wonderful-webserver') 4 | + webserver.withImage('httpd:2.5') 5 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/example5.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = import 'webserver/main.libsonnet'; 2 | 3 | { 4 | webserver1: 5 | webserver.new('wonderful-webserver') 6 | + webserver.withImage('httpd:2.3'), 7 | 8 | webserver2: 9 | webserver.new('marvellous-webserver'), 10 | 11 | webserver3: 12 | webserver.new('incredible-webserver', 2), 13 | } 14 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 8 | "subdir": "1.23" 9 | } 10 | }, 11 | "version": "main" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson3/example2/lib/webserver/main.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | { 4 | new(name, replicas=1): { 5 | container:: 6 | k.core.v1.container.new('httpd', 'httpd:2.4'), 7 | 8 | deployment: 9 | k.apps.v1.deployment.new( 10 | name, 11 | replicas, 12 | [self.container] 13 | ), 14 | }, 15 | 16 | withImage(image): { 17 | container+: 18 | k.core.v1.container.withImage(image), 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /lessons/lesson3/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | [ 3 | example.new( 4 | 'example1.jsonnet', 5 | importstr './example1.jsonnet', 6 | import './example1.jsonnet', 7 | ), 8 | 9 | example.new( 10 | 'example2/lib/k.libsonnet', 11 | importstr './example2/lib/k.libsonnet', 12 | ), 13 | 14 | example.new( 15 | 'example2/example1.jsonnet', 16 | importstr './example2/example1.jsonnet', 17 | ), 18 | 19 | example.new( 20 | 'example2/example2.jsonnet', 21 | importstr './example2/example2.jsonnet', 22 | ), 23 | 24 | example.new( 25 | 'example2/example3.jsonnet', 26 | importstr './example2/example3.jsonnet', 27 | ), 28 | 29 | example.new( 30 | 'example2/example4.jsonnet', 31 | importstr './example2/example4.jsonnet', 32 | ), 33 | 34 | example.new( 35 | 'example2/example5.jsonnet', 36 | importstr './example2/example5.jsonnet', 37 | ), 38 | 39 | example.new( 40 | 'example2/lib/webserver/main.libsonnet', 41 | importstr './example2/lib/webserver/main.libsonnet', 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /lessons/lesson3/lesson.md: -------------------------------------------------------------------------------- 1 | In [Write an extensible library](lesson1.md), we created this library: 2 | 3 | %(example1.jsonnet)s 4 | 5 | This library is quite verbose as the author has to provide the `apiVersion`, `kind` and 6 | other attributes. 7 | 8 | To simplify this, the community has created a Kubernetes client library for Jsonnet called 9 | [`k8s-libsonnet`](https://github.com/jsonnet-libs/k8s-libsonnet). By leveraging this 10 | client library, the author can provide an abstraction that can work across most Kubernetes 11 | versions. 12 | 13 | Now go ahead with the `k8s-libsonnet` library and work out on your own with the resources 14 | in these lessons: 15 | 16 | 1. [Write an extensible library](lesson1.md) 17 | 1. [Understanding Package management](lesson2.md) 18 | 19 | Find the steps to a solution below. 20 | 21 | ### Solution 22 | 23 | > Examples below expect to have an environment with `export JSONNET_PATH="lib/:vendor/"` 24 | 25 | Let's install `k8s-libsonnet` with jsonnet-bundler and import it: 26 | 27 | `$ jb install https://github.com/jsonnet-libs/k8s-libsonnet/1.23@main` 28 | 29 | Note the alternative naming pattern ending on `1.23`, referencing the Kubernetes version 30 | this was generated for. 31 | 32 | %(example2/lib/k.libsonnet)s 33 | 34 | The most common convention to work with this is to provide `lib/k.libsonnet` as 35 | a shortcut. 36 | 37 | --- 38 | 39 | %(example2/example1.jsonnet)s 40 | 41 | Many libraries have a line `local k = import 'k.libsonnet'` to refer to this 42 | library. 43 | 44 | --- 45 | 46 | Let's rewrite the container following the 47 | [documentation](https://jsonnet-libs.github.io/k8s-libsonnet/1.23/core/v1/container/): 48 | 49 | %(example2/example2.jsonnet)s 50 | 51 | The library has grouped a number of functions under `k.core.v1.container`, we'll use the 52 | `new(name, image)` function here, this makes it concise. Additionally the `withImage()` 53 | function uses the function with the same name in the library. 54 | 55 | --- 56 | 57 | And now for the 58 | [deployment](https://jsonnet-libs.github.io/k8s-libsonnet/1.23/apps/v1/deployment/): 59 | 60 | %(example2/example3.jsonnet)s 61 | 62 | The `new(name, replicas, images)` function makes things even more concise. The `new()` 63 | function is actually a custom shortcut with the most common parameters for a deployment. 64 | 65 | Note that we've removed `local base = self,`, this was not longer needed as the reference 66 | to `self.container` can now be made inside the same object. 67 | 68 | --- 69 | 70 | Having the library and execution together is not so useful, let's move it into a separate 71 | library and import it again. 72 | 73 | %(example2/lib/webserver/main.libsonnet)s 74 | 75 | This removes the `local webserver` and moves the contents to the root of the file. 76 | 77 | --- 78 | 79 | %(example2/example4.jsonnet)s 80 | 81 | If we now `import` the library, we can access its functions just like before. 82 | 83 | --- 84 | 85 | %(example2/example5.jsonnet)s 86 | 87 | Or, if we want more instances, we can simply do so. 88 | -------------------------------------------------------------------------------- /lessons/lesson3/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = import './examples.jsonnet'; 5 | 6 | lesson.new( 7 | 'lesson3', 8 | 'Exercise: rewrite a library with `k8s-libsonnet`', 9 | ||| 10 | In the first lesson we've written a extensible library and in the second lesson we've 11 | covered package management with jsonnet-bundler. In this lesson we'll combine what 12 | we've learned and rewrite that library. 13 | |||, 14 | [ 15 | 'Rewrite a library', 16 | 'Vendor and use `k8s-libsonnet`', 17 | 'Understand the `lib/k.libsonnet` convention', 18 | ], 19 | (importstr './lesson.md') % 20 | std.foldr( 21 | function(e, acc) 22 | acc { [e.filename]: e.render }, 23 | examples, 24 | {} 25 | ), 26 | ||| 27 | This exercise showed how to make a library more succinct and readable. By using the 28 | `k.libsonnet` abstract, the user has the option to use an alternative version of the 29 | `k8s-libsonnet` library. 30 | |||, 31 | ) 32 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local privatebin = import 'github.com/Duologic/privatebin-libsonnet/main.libsonnet'; 2 | 3 | { 4 | privatebin: privatebin.new(), 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local privatebin = import 'privatebin/main.libsonnet'; 2 | 3 | { 4 | privatebin: 5 | privatebin.new('backend') 6 | + privatebin.withPort(9000), 7 | } 8 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/Duologic/privatebin-libsonnet.git", 8 | "subdir": "" 9 | } 10 | }, 11 | "version": "master" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 17 | "subdir": "1.23" 18 | } 19 | }, 20 | "version": "main" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet' 2 | -------------------------------------------------------------------------------- /lessons/lesson4/example1/lib/privatebin/main.libsonnet: -------------------------------------------------------------------------------- 1 | local privatebin = import 'github.com/Duologic/privatebin-libsonnet/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | privatebin 5 | { 6 | withPort(port): { 7 | container+: 8 | k.core.v1.container.withPorts([ 9 | k.core.v1.containerPort.newNamed( 10 | name='http', 11 | containerPort=port 12 | ), 13 | ]), 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /lessons/lesson4/example2/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson4/example2/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local privatebin = import 'privatebin/main.libsonnet'; 2 | 3 | { 4 | privatebin: 5 | privatebin.new('backend') 6 | + privatebin.withPort(9000), 7 | } 8 | -------------------------------------------------------------------------------- /lessons/lesson4/example2/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/Duologic/privatebin-libsonnet.git", 8 | "subdir": "" 9 | } 10 | }, 11 | "version": "master" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 17 | "subdir": "1.23" 18 | } 19 | }, 20 | "version": "main" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /lessons/lesson4/example2/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet' 2 | -------------------------------------------------------------------------------- /lessons/lesson4/example2/lib/privatebin/main.libsonnet: -------------------------------------------------------------------------------- 1 | local privatebin = import 'github.com/Duologic/privatebin-libsonnet/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | privatebin 5 | { 6 | new(team): 7 | super.new(team + '-privatebin'), 8 | 9 | withPort(port): { 10 | container+: 11 | k.core.v1.container.withPorts([ 12 | k.core.v1.containerPort.newNamed( 13 | name='http', 14 | containerPort=port 15 | ), 16 | ]), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /lessons/lesson4/example3/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson4/example3/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "local": { 7 | "directory": "../../lesson3/example2/lib/webserver" 8 | } 9 | }, 10 | "version": "" 11 | } 12 | ], 13 | "legacyImports": true 14 | } 15 | -------------------------------------------------------------------------------- /lessons/lesson4/example3/lib/webserver: -------------------------------------------------------------------------------- 1 | ../../../lesson3/example2/lib/webserver -------------------------------------------------------------------------------- /lessons/lesson4/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | 3 | [ 4 | example.new( 5 | 'example1/example1.jsonnet', 6 | importstr './example1/example1.jsonnet', 7 | ), 8 | 9 | example.new( 10 | 'example1/vendor/github.com/Duologic/privatebin-libsonnet/main.libsonnet', 11 | importstr './example1/vendor/github.com/Duologic/privatebin-libsonnet/main.libsonnet', 12 | ), 13 | 14 | example.new( 15 | 'example1/lib/privatebin/main.libsonnet', 16 | importstr './example1/lib/privatebin/main.libsonnet', 17 | ), 18 | 19 | example.new( 20 | 'example1/example2.jsonnet', 21 | importstr './example1/example2.jsonnet', 22 | ), 23 | 24 | example.new( 25 | 'example2/lib/privatebin/main.libsonnet', 26 | importstr './example2/lib/privatebin/main.libsonnet', 27 | ), 28 | 29 | example.new( 30 | 'usecase-pentagon/lib/pentagon/example1.libsonnet', 31 | importstr './usecase-pentagon/lib/pentagon/example1.libsonnet', 32 | ), 33 | 34 | example.new( 35 | 'usecase-pentagon/example1.jsonnet', 36 | importstr './usecase-pentagon/example1.jsonnet', 37 | ), 38 | 39 | example.new( 40 | 'usecase-pentagon/lib/pentagon/example2.libsonnet', 41 | importstr './usecase-pentagon/lib/pentagon/example2.libsonnet', 42 | ), 43 | 44 | example.new( 45 | 'example3/jsonnetfile.json', 46 | importstr './example3/jsonnetfile.json', 47 | type='json', 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /lessons/lesson4/lesson.md: -------------------------------------------------------------------------------- 1 | ### Wrapping libraries 2 | 3 | As shown in the [refactoring exercise](lesson3.md#solution), let's initialize a new 4 | project with `k8s-libsonnet`: 5 | 6 | `$ jb init` 7 | 8 | `$ jb install github.com/jsonnet-libs/k8s-libsonnet/1.23@main` 9 | 10 | `$ mkdir lib` 11 | 12 | `$ echo "(import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet')" > lib/k.libsonnet` 13 | 14 | And install 15 | [`privatebin-libsonnet`](https://github.com/Duologic/privatebin-libsonnet): 16 | 17 | `$ jb install github.com/Duologic/privatebin-libsonnet` 18 | 19 | %(example1/example1.jsonnet)s 20 | 21 | This is relatively simple web application that exposes itself on port `8080`. 22 | 23 | --- 24 | 25 | %(example1/vendor/github.com/Duologic/privatebin-libsonnet/main.libsonnet)s 26 | 27 | `privatebin-libsonnet` is relatively simple, it already exposes the `container` separate 28 | from the `deployment` and we can pick the `name` and `image` when initializing it with 29 | `new()`. 30 | 31 | One hurdle: the library does not expose a function to change the port. We could go about 32 | and create a change request upstream, but considering this is a very trivial change it 33 | might not be worth the time. 34 | 35 | So, let's try to deal with it locally. 36 | 37 | > **`ksonnet-util`** 38 | > 39 | > The [`ksonnet-util`](https://github.com/grafana/jsonnet-libs/tree/master/ksonnet-util) 40 | > library contains a set of util functions developed around the now deprecated 41 | > [`ksonnet-lib`](https://github.com/ksonnet/ksonnet-lib). It also functions as 42 | > a compatibility layer towards `k8s-libsonnet`. Many of the util functions have since 43 | > made it into `k8s-libsonnet` with the aim to make this library obsolete over time. 44 | 45 | --- 46 | 47 | For the purpose of this exercise we want to run this application for different teams and 48 | each team should be able to change the port to their liking. 49 | 50 | 51 | Create a local library in `lib/privatebin/`: 52 | 53 | %(example1/lib/privatebin/main.libsonnet)s 54 | 55 | Here we import the vendored library and extend it. We are extending the privatebin 56 | library here extended with the `withPort()` function that will change the ports of the 57 | `container`. 58 | 59 | Have a look at the 60 | [`container`](https://jsonnet-libs.github.io/k8s-libsonnet/1.18/core/v1/container/#fn-withports) 61 | and 62 | [`containerPort`](https://jsonnet-libs.github.io/k8s-libsonnet/1.18/core/v1/containerPort/#fn-newnamed) 63 | documentation for the details on each. 64 | 65 | --- 66 | 67 | %(example1/example2.jsonnet)s 68 | 69 | To use the new library, we need to change the import to match `privatebin/main.libsonnet`, 70 | `JSONNET_PATH` will expand it to `lib/privatebin/main.libsonnet`. 71 | 72 | This makes the `withPort()` function available and now teams can set their own port. 73 | 74 | In this example, the backend team has named its privatebin `backend`, this will generate 75 | a deployment/service with that name. As the name is quite generic, this may cause 76 | conflicts. 77 | 78 | --- 79 | 80 | Let's add a suffix to prevent the naming conflict: 81 | 82 | %(example2/lib/privatebin/main.libsonnet)s 83 | 84 | Here we call `super.new()`, this means it will use the `new()` function defined in 85 | privatebin. 86 | 87 | The deployment/service will now be suffixed with `-privatebin`, ie. `backend-privatebin`. 88 | 89 | #### Use case: replace Pentagon with External Secrets Operator 90 | 91 | Wrapping libraries is a powerful concept, it can be used to manipulate or even replace 92 | whole systems that are used across a code base. 93 | 94 | %(usecase-pentagon/lib/pentagon/example1.libsonnet)s 95 | 96 | %(usecase-pentagon/example1.jsonnet)s 97 | 98 | For synchronizing secrets between Vault and Kubernetes, Grafana Labs used a fork of 99 | [Pentagon](https://github.com/grafana/pentagon). This process was a deployment per 100 | namespace with each team managing their own deployments. To facilitate a consistent 101 | connection configuration we wrapped the [pentagon 102 | library](https://github.com/grafana/jsonnet-libs/tree/master/pentagon). Teams could create 103 | a Vault->Kubernetes mapping with the shortcut function `pentagonKVmapping` to populate the 104 | `pentagon_mappings` array (which gets turned into a configMap). 105 | 106 | As a deployment per namespace accumulated quite a bit of resources, we opted to replace it 107 | with [External Secrets Operator](https://external-secrets.io/). This means we'd go from 108 | many deployments and configMaps (at least one of each per namespace) to a single operator 109 | deployment per cluster, a secretStore per namespace and many externalSecret objects. 110 | 111 | --- 112 | 113 | %(usecase-pentagon/lib/pentagon/example2.libsonnet)s 114 | 115 | The operator deployment and secretStore objects are managed centrally, only thing left to 116 | do is replace the secret mappings everywhere. With more than 2500 mappings, this would 117 | have been a hell of a refactoring job. Fortunately we had the wrapped library in place and 118 | we could transform the mappings array into externalSecret objects transparently. 119 | 120 | As the wrapped library is aware of the cluster it was being deployed too, we were able 121 | gradually roll this out across the fleet. 122 | 123 | Finally with this in place, we informed each team on how to refactor their code to use 124 | External Secrets directly, allowing them to work on it at their own pace. 125 | 126 | ### Developing on vendored libraries 127 | 128 | As it gives immediate feedback, it often happens that a vendored library is developed 129 | alongside the project that is using it. However any invocation of `jb install` will remove 130 | changes from `vendor/`, which makes it a little bit more challenging. Let's have a look at 131 | the different options. 132 | 133 | #### Simply edit files in `vendor/` 134 | 135 | This is the easiest except the risk of loosing changes is highest. For testing small 136 | changes this is probably safe, it gives immediate feedback whether the changes match 137 | expectations. The small changes should be pushed upstream early on so they don't get lost. 138 | 139 | #### Clone dependency in `vendor/` 140 | 141 | Similar but a bit more elaborate, it is possible to `git clone` the dependency straight 142 | into `vendor/`. It faces the same risk as above but allows for a shorter loop to push 143 | changes upstream. 144 | 145 | #### Use local reference 146 | 147 | `$ jb install ../../lesson3/example2/lib/webserver` 148 | 149 | %(example3/jsonnetfile.json)s 150 | 151 | ``` 152 | . 153 | ├── jsonnetfile.json 154 | ├── jsonnetfile.lock.json 155 | └── vendor 156 | └── webserver -> ../../../lesson3/example2/lib/webserver 157 | ``` 158 | 159 | This installs a local library relative to the project root with a symlink. Changes made in 160 | `vendor/` or on the real location are unaffected by `jb install` however it changes 161 | `jsonnetfile.json` to something that can't be shared. 162 | 163 | #### Symlink in `lib/` 164 | 165 | `$ ln -s ../../../lesson3/example2/lib/webserver lib/` 166 | 167 | ``` 168 | . 169 | └── lib 170 | └── webserver -> ../../../lesson3/example2/lib/webserver 171 | ``` 172 | 173 | By relying on the import order, a symlink in `lib/` could be made. With `lib/` being 174 | matched before `vendor/`, it will be used first. This approach is unaffected by `jb 175 | install` and doesn't change `jsonnetfile.json`. 176 | -------------------------------------------------------------------------------- /lessons/lesson4/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = import './examples.jsonnet'; 5 | 6 | lesson.new( 7 | 'lesson4', 8 | 'Further developing libraries', 9 | ||| 10 | When starting out with Jsonnet, it is very likely that you'll with existing code and 11 | dependencies. Developing on these can seem confusing and tedious, however there are 12 | several techniques that can help us iterate at different velocities. 13 | |||, 14 | [ 15 | 'Wrap and extend a dependency locally', 16 | 'Developing on upstream libraries', 17 | ], 18 | (importstr './lesson.md') % 19 | std.foldr( 20 | function(e, acc) 21 | acc { [e.filename]: e.render }, 22 | examples, 23 | {} 24 | ), 25 | ||| 26 | Wrapping libraries locally by leveraging the extensible nature of Jsonnet can be very 27 | useful. It can alter default behavior that is more suitable for the project. 28 | 29 | Developing directly on vendored libraries on the other hand is quite clumsy, the 30 | techniques described require a bit of pragmatism to be useful. jsonnet-bundler could 31 | benefit from a feature to make this easier. 32 | |||, 33 | ) 34 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local pentagon = import 'pentagon/example1.libsonnet'; 2 | 3 | { 4 | pentagon: pentagon { 5 | _config+:: { 6 | cluster_name: 'dev', 7 | namespace: 'app1', 8 | }, 9 | pentagon_mappings: [ 10 | pentagon.pentagonKVMapping('path/to/secret', 'k8sSecretName'), 11 | ], 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local pentagon = import 'pentagon/example2.libsonnet'; 2 | 3 | { 4 | pentagon: pentagon { 5 | _config+:: { 6 | cluster_name: 'dev', 7 | namespace: 'app1', 8 | }, 9 | pentagon_mappings: [ 10 | pentagon.pentagonKVMapping('path/to/secret', 'k8sSecretName'), 11 | ], 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/grafana/jsonnet-libs.git", 8 | "subdir": "pentagon" 9 | } 10 | }, 11 | "version": "master" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/external-secrets-libsonnet.git", 17 | "subdir": "0.5" 18 | } 19 | }, 20 | "version": "main", 21 | "name": "external-secrets-libsonnet" 22 | }, 23 | { 24 | "source": { 25 | "git": { 26 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 27 | "subdir": "1.23" 28 | } 29 | }, 30 | "version": "main" 31 | } 32 | ], 33 | "legacyImports": true 34 | } 35 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/lib/pentagon/example1.libsonnet: -------------------------------------------------------------------------------- 1 | local pentagon = import 'github.com/grafana/jsonnet-libs/pentagon/pentagon.libsonnet'; 2 | 3 | pentagon 4 | { 5 | _config+:: { 6 | local this = self, 7 | pentagon+: { 8 | vault_address: 9 | if this.cluster_name == 'dev' 10 | then 'vault-dev.example.com' 11 | else 'vault-prod.example.com', 12 | }, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /lessons/lesson4/usecase-pentagon/lib/pentagon/example2.libsonnet: -------------------------------------------------------------------------------- 1 | local externalSecrets = import 'external-secrets-libsonnet/main.libsonnet'; 2 | local pentagon = import 'github.com/grafana/jsonnet-libs/pentagon/pentagon.libsonnet'; 3 | 4 | pentagon 5 | { 6 | local this = self, 7 | 8 | // Remove/hide resources 9 | _config+:: {}, 10 | deployment:: {}, 11 | config_map:: {}, 12 | rbac:: {}, 13 | cluster_role:: {}, 14 | cluster_role_binding:: {}, 15 | 16 | local externalSecret = externalSecrets.nogroup.v1beta1.externalSecret, 17 | externalSecrets: std.sort([ 18 | local mapping = this.pentagon_mappings_map[m]; 19 | 20 | externalSecret.new(mapping.secretName) 21 | + externalSecret.spec.secretStoreRef.withName('vault-backend') 22 | + externalSecret.spec.secretStoreRef.withKind('SecretStore') 23 | + externalSecret.spec.target.withName(mapping.secretName) 24 | + externalSecret.spec.withDataFrom([ 25 | { 26 | extract: { 27 | key: mapping.vaultPath, 28 | }, 29 | }, 30 | ]) 31 | 32 | for m in std.objectFields(this.pentagon_mappings_map) 33 | ], function(e) if std.objectHasAll(e, 'idx') then e.idx else 0), 34 | } 35 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | docs: 3 | @rm -rf docs/ && \ 4 | jsonnet -J ./vendor -S -c -m docs/ example7.jsonnet 5 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/docs/README.md: -------------------------------------------------------------------------------- 1 | # package webserver 2 | 3 | `webserver` provides a basic webserver on Kubernetes 4 | 5 | ## Install 6 | 7 | ``` 8 | jb install github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1@master 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```jsonnet 14 | local webserver = import "github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1/example5.jsonnet" 15 | ``` 16 | 17 | ## Index 18 | 19 | * [`fn new(name, replicas=1)`](#fn-new) 20 | * [`fn withImage(image)`](#fn-withimage) 21 | * [`obj images`](#obj-images) 22 | 23 | ## Fields 24 | 25 | ### fn new 26 | 27 | ```ts 28 | new(name, replicas=1) 29 | ``` 30 | 31 | `new` creates a Deployment object for Kubernetes 32 | 33 | * `name` sets the name for the Deployment object 34 | * `replicas` number of desired pods, defaults to 1 35 | 36 | 37 | ### fn withImage 38 | 39 | ```ts 40 | withImage(image) 41 | ``` 42 | 43 | `withImage` modifies the image used for the httpd container 44 | 45 | ### obj images 46 | 47 | `images` provides images for common webservers 48 | 49 | Usage: 50 | 51 | ``` 52 | webserver.new('my-nginx') 53 | + webserver.withImage(webserver.images.nginx) 54 | ``` 55 | 56 | 57 | * `images.apache` (`string`): `"httpd:2.4"` - Apache HTTP webserver 58 | * `images.nginx` (`string`): `"nginx:1.22"` - Nginx HTTP webserver 59 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | { 4 | new(name, replicas=1): { 5 | container:: 6 | k.core.v1.container.new('httpd', 'httpd:2.4'), 7 | 8 | deployment: 9 | k.apps.v1.deployment.new( 10 | name, 11 | replicas, 12 | [self.container] 13 | ), 14 | }, 15 | 16 | withImage(image): { 17 | container+: 18 | k.core.v1.container.withImage(image), 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | { 5 | '#':: d.pkg( 6 | name='webserver', 7 | url='github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1', 8 | help='`webserver` provides a basic webserver on Kubernetes', 9 | filename=std.thisFile, 10 | ), 11 | 12 | new(name, replicas=1): { 13 | container:: 14 | k.core.v1.container.new('httpd', 'httpd:2.4'), 15 | 16 | deployment: 17 | k.apps.v1.deployment.new( 18 | name, 19 | replicas, 20 | [self.container] 21 | ), 22 | }, 23 | 24 | withImage(image): { 25 | container+: 26 | k.core.v1.container.withImage(image), 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example3.jsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | { 5 | '#':: d.pkg( 6 | name='webserver', 7 | url='github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1', 8 | help='`webserver` provides a basic webserver on Kubernetes', 9 | filename=std.thisFile, 10 | ), 11 | 12 | '#new':: d.fn( 13 | help=||| 14 | `new` creates a Deployment object for Kubernetes 15 | 16 | * `name` sets the name for the Deployment object 17 | * `replicas` number of desired pods, defaults to 1 18 | |||, 19 | args=[ 20 | d.arg('name', d.T.string), 21 | d.arg('replicas', d.T.number, 1), 22 | ] 23 | ), 24 | new(name, replicas=1): { 25 | container:: 26 | k.core.v1.container.new('httpd', 'httpd:2.4'), 27 | 28 | deployment: 29 | k.apps.v1.deployment.new( 30 | name, 31 | replicas, 32 | [self.container] 33 | ), 34 | }, 35 | 36 | '#withImage':: d.fn( 37 | help='`withImage` modifies the image used for the httpd container', 38 | args=[ 39 | d.arg('image', d.T.string), 40 | ] 41 | ), 42 | withImage(image): { 43 | container+: 44 | k.core.v1.container.withImage(image), 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example4.jsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | { 5 | '#':: d.pkg( 6 | name='webserver', 7 | url='github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1', 8 | help='`webserver` provides a basic webserver on Kubernetes', 9 | filename=std.thisFile, 10 | ), 11 | 12 | images: { 13 | apache: 'httpd:2.4', 14 | nginx: 'nginx:1.22', 15 | }, 16 | 17 | '#new':: d.fn( 18 | help=||| 19 | `new` creates a Deployment object for Kubernetes 20 | 21 | * `name` sets the name for the Deployment object 22 | * `replicas` number of desired pods, defaults to 1 23 | |||, 24 | args=[ 25 | d.arg('name', d.T.string), 26 | d.arg('replicas', d.T.number, 1), 27 | ] 28 | ), 29 | new(name, replicas=1): { 30 | container:: 31 | k.core.v1.container.new('httpd', 'httpd:2.4'), 32 | 33 | deployment: 34 | k.apps.v1.deployment.new( 35 | name, 36 | replicas, 37 | [self.container] 38 | ), 39 | }, 40 | 41 | '#withImage':: d.fn( 42 | help='`withImage` modifies the image used for the httpd container', 43 | args=[ 44 | d.arg('image', d.T.string), 45 | ] 46 | ), 47 | withImage(image): { 48 | container+: 49 | k.core.v1.container.withImage(image), 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example5.jsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; 2 | local k = import 'k.libsonnet'; 3 | 4 | { 5 | '#':: d.pkg( 6 | name='webserver', 7 | url='github.com/jsonnet-libs/jsonnet-training-course/lessons/lesson5/example1', 8 | help='`webserver` provides a basic webserver on Kubernetes', 9 | filename=std.thisFile, 10 | ), 11 | 12 | '#images':: d.obj( 13 | help=||| 14 | `images` provides images for common webservers 15 | 16 | Usage: 17 | 18 | ``` 19 | webserver.new('my-nginx') 20 | + webserver.withImage(webserver.images.nginx) 21 | ``` 22 | ||| 23 | ), 24 | images: { 25 | '#apache':: d.val(d.T.string, 'Apache HTTP webserver'), 26 | apache: 'httpd:2.4', 27 | '#nginx':: d.val(d.T.string, 'Nginx HTTP webserver'), 28 | nginx: 'nginx:1.22', 29 | }, 30 | 31 | '#new':: d.fn( 32 | help=||| 33 | `new` creates a Deployment object for Kubernetes 34 | 35 | * `name` sets the name for the Deployment object 36 | * `replicas` number of desired pods, defaults to 1 37 | |||, 38 | args=[ 39 | d.arg('name', d.T.string), 40 | d.arg('replicas', d.T.number, 1), 41 | ] 42 | ), 43 | new(name, replicas=1): { 44 | container:: 45 | k.core.v1.container.new('httpd', 'httpd:2.4'), 46 | 47 | deployment: 48 | k.apps.v1.deployment.new( 49 | name, 50 | replicas, 51 | [self.container] 52 | ), 53 | }, 54 | 55 | '#withImage':: d.fn( 56 | help='`withImage` modifies the image used for the httpd container', 57 | args=[ 58 | d.arg('image', d.T.string), 59 | ] 60 | ), 61 | withImage(image): { 62 | container+: 63 | k.core.v1.container.withImage(image), 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/example7.jsonnet: -------------------------------------------------------------------------------- 1 | local d = import 'github.com/jsonnet-libs/docsonnet/doc-util/main.libsonnet'; 2 | 3 | d.render(import 'example5.jsonnet') 4 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/docsonnet.git", 8 | "subdir": "doc-util" 9 | } 10 | }, 11 | "version": "master" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 17 | "subdir": "1.23" 18 | } 19 | }, 20 | "version": "main" 21 | }, 22 | { 23 | "source": { 24 | "git": { 25 | "remote": "https://github.com/jsonnet-libs/xtd.git", 26 | "subdir": "" 27 | } 28 | }, 29 | "version": "master" 30 | } 31 | ], 32 | "legacyImports": true 33 | } 34 | -------------------------------------------------------------------------------- /lessons/lesson5/example1/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson5/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | [ 3 | example.new('./example1/example1.jsonnet'[2:], importstr './example1/example1.jsonnet', import './example1/example1.jsonnet'), 4 | example.new('./example1/example2.jsonnet'[2:], importstr './example1/example2.jsonnet', import './example1/example2.jsonnet'), 5 | example.new('./example1/example3.jsonnet'[2:], importstr './example1/example3.jsonnet', import './example1/example3.jsonnet'), 6 | example.new('./example1/example4.jsonnet'[2:], importstr './example1/example4.jsonnet', import './example1/example4.jsonnet'), 7 | example.new('./example1/example5.jsonnet'[2:], importstr './example1/example5.jsonnet', import './example1/example5.jsonnet'), 8 | example.new('./example1/example7.jsonnet'[2:], importstr './example1/example7.jsonnet', import './example1/example7.jsonnet'), 9 | ] 10 | -------------------------------------------------------------------------------- /lessons/lesson5/lesson.md: -------------------------------------------------------------------------------- 1 | ### Writing docstrings 2 | 3 | We'll continue with the webserver library from the exercise. 4 | 5 | %(example1/example1.jsonnet)s 6 | 7 | This library provides a number of functions to create a webserver. In docsonnet lingo, 8 | this is called a 'package'. Each function has a few arguments which usually have a fixed 9 | type. As Jsonnet itself is not a typed language, we'll simply try to document the type. 10 | 11 | --- 12 | 13 | To get started, let's grab docsonnet's doc-util library. It provides a few functions to 14 | write consistent docstrings for the different objects in Jsonnet. 15 | 16 | `$ jb install github.com/jsonnet-libs/docsonnet/doc-util` 17 | 18 | Note that the [docs for 19 | doc-util](https://github.com/jsonnet-libs/docsonnet/blob/master/doc-util/README.md) are 20 | also rendered by docsonnet. 21 | 22 | --- 23 | 24 | %(example1/example2.jsonnet)s 25 | 26 | The docsonnet puts a claim on keys that start with a hash `#`. It assumes that keys 27 | starting with a hash symbol is very uncommon. Additionally, it closely relates to how 28 | comments are written. 29 | 30 | The package definition is put in the `#` key without a value. 31 | 32 | The `url` argument is the part that can be used to install this library with 33 | jsonnet-bundler: 34 | 35 | `$ jb install ` 36 | 37 | In combination with the `url`, the `filename` refers to what should be imported, note the 38 | neat `std.thisFile` shortcut so we don't have to remember to change the filename here if 39 | we ever rename the file. 40 | 41 | `import '/'` 42 | 43 | --- 44 | 45 | %(example1/example3.jsonnet)s 46 | 47 | Docstrings for other elements are put in the same key prefixed by a hash `#`, for example 48 | the docstring for `new()` will be in the `#new` key. Functions can be documented with 49 | `d.fn(help, args)`. 50 | 51 | As the `help` text is quite long for this example, we're using multi line strings with 52 | `|||`. This also improves the readability in the code. The `args` can be typed, values 53 | for these are provided by `d.T.` to increase consistency. 54 | 55 | --- 56 | 57 | For the sake of this lesson, let's add a new `images` object with a few attributes: 58 | 59 | %(example1/example4.jsonnet)s 60 | 61 | The `images` key holds an object with additional webserver images. 62 | 63 | It can be used in combination with `withImage`: 64 | 65 | `webserver.withImage(webserver.images.nginx)` 66 | 67 | --- 68 | 69 | %(example1/example5.jsonnet)s 70 | 71 | Just like with functions, the key for the docstring gets prefixed by `#`. Objects can be 72 | documented with `d.obj(help)`. 73 | 74 | The attributes in this object can be documented with `d.val(type, help)`. 75 | 76 | ### Generating markdown docs 77 | 78 | The doc-util library has a built-in rendering: 79 | 80 | %(example1/example7.jsonnet)s 81 | 82 | The `render(obj)` function returns a format to output [multiple 83 | files](https://jsonnet.org/learning/getting_started.html#multi). 84 | 85 | --- 86 | 87 | ``` 88 | { 89 | 'README.md': "...", 90 | 'path/to/example.md': "...", 91 | } 92 | ``` 93 | 94 | --- 95 | 96 | Jsonnet can export those files: 97 | 98 | `$ jsonnet --string --create-output-dirs --multi docs/ example7.jsonnet` 99 | 100 | * `--string` because the Markdown output should be treated as a string instead of JSON. 101 | * `--create-output-dirs` ensure directories for `path/to/example.md` get created. 102 | * `--multi docs/` to set the output directory for multiple files to `docs/`. 103 | 104 | Or in short: 105 | 106 | `$ jsonnet -S -c -m docs/ example7.jsonnet` 107 | 108 | Note that this overwrites but does not remove existing files. 109 | 110 | %(example1/Makefile)s 111 | 112 | A simple Makefile target can be quite useful to contain these incantations. 113 | 114 | --- 115 | 116 | > This can also be done without the additional Jsonnet file by using `jsonnet --exec`: 117 | > 118 | > `$ jsonnet -S -c -m docs/ --exec "(import 'doc-util/main.libsonnet').render(import 'example7.jsonnet')"` 119 | 120 | --- 121 | 122 | The documentation for the webserver library will look like this: 123 | 124 | %(example1/docs/README.md)s 125 | -------------------------------------------------------------------------------- /lessons/lesson5/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = 5 | (import './examples.jsonnet') 6 | + [ 7 | c.example.new('example1/Makefile', importstr './example1/Makefile', {}, 'makefile'), 8 | c.example.new('example1/docs/README.md', importstr './example1/docs/README.md', {}, 'markdown'), 9 | ]; 10 | 11 | lesson.new( 12 | 'lesson5', 13 | 'Providing documentation with Docsonnet', 14 | ||| 15 | [Docsonnet](https://github.com/jsonnet-libs/docsonnet) provides a way to consistently 16 | add docstrings to Jsonnet code. As Docsonnet docstrings are written as Jsonnet 17 | attributes, it can also natively render user-friendly documentation in Markdown. 18 | |||, 19 | [ 20 | 'Write docstrings for Docsonnet', 21 | 'Generate markdown documentation', 22 | ], 23 | (importstr './lesson.md') % 24 | std.foldr( 25 | function(e, acc) 26 | acc { [e.filename]: e.render }, 27 | examples, 28 | {} 29 | ), 30 | ||| 31 | Providing documentation can be very helpful to communicate the library's intended 32 | use. The argument types give an indication about the expected values and the help 33 | text can contain code samples to get meaningful results quickly. 34 | 35 | Rendering documentation can be done directly with Jsonnet without additional 36 | programs, altough the incantations may feel like magic at first. 37 | |||, 38 | ) 39 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": { 3 | "apiVersion": "apps/v1", 4 | "kind": "Deployment", 5 | "metadata": { 6 | "name": "webserver1" 7 | }, 8 | "spec": { 9 | "replicas": 1, 10 | "selector": { 11 | "matchLabels": { 12 | "name": "webserver1" 13 | } 14 | }, 15 | "template": { 16 | "metadata": { 17 | "labels": { 18 | "name": "webserver1" 19 | } 20 | }, 21 | "spec": { 22 | "containers": [ 23 | { 24 | "image": "httpd:2.4", 25 | "name": "httpd" 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example0.jsonnet: -------------------------------------------------------------------------------- 1 | local webserver = import 'webserver/main.libsonnet'; 2 | webserver.new('webserver1') 3 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example1.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/main.libsonnet'; 3 | 4 | test.new(std.thisFile) 5 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example1.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example1.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite example1.jsonnet 3 | { 4 | "verify": "Passed 0 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example1.output: -------------------------------------------------------------------------------- 1 | Opening input file: example1: no such file or directory 2 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example2.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/main.libsonnet'; 3 | 4 | local base = import 'base.json'; 5 | 6 | test.new(std.thisFile) 7 | + test.case.new( 8 | 'Basic', 9 | test.expect.eq( 10 | webserver.new('webserver1'), 11 | base 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example2.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example2.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite example2.jsonnet 3 | { 4 | "verify": "Passed 1 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example3.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/main.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | test.new(std.thisFile) 8 | + test.case.new( 9 | 'Basic', 10 | test.expect.eq( 11 | webserver.new(webserverName), 12 | base 13 | ) 14 | ) 15 | + test.case.new( 16 | 'Change default replicas', 17 | test.expect.eq( 18 | webserver.new(webserverName, 2), 19 | base { deployment+: { spec+: { replicas: 2 } } } 20 | ) 21 | ) 22 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example3.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example3.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite example3.jsonnet 3 | { 4 | "verify": "Passed 2 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example4.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/main.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | local mapContainerWithName(name, obj) = 8 | { 9 | local containers = super.spec.template.spec.containers, 10 | spec+: { template+: { spec+: { containers: [ 11 | if c.name == name 12 | then c + obj 13 | else c 14 | for c in containers 15 | ] } } }, 16 | }; 17 | 18 | test.new(std.thisFile) 19 | + test.case.new( 20 | 'Basic', 21 | test.expect.eq( 22 | webserver.new(webserverName), 23 | base 24 | ) 25 | ) 26 | + test.case.new( 27 | 'Change default replicas', 28 | test.expect.eq( 29 | webserver.new(webserverName, 2), 30 | base { deployment+: { spec+: { replicas: 2 } } } 31 | ) 32 | ) 33 | + test.case.new( 34 | 'Set alternative image', 35 | test.expect.eq( 36 | webserver.new(webserverName) 37 | + webserver.withImage('httpd:2.5'), 38 | base { deployment+: mapContainerWithName('httpd', { image: 'httpd:2.5' }) } 39 | ) 40 | ) 41 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example4.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example4.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite example4.jsonnet 3 | { 4 | "verify": "Passed 3 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example5.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/wrong1.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | local mapContainerWithName(name, obj) = 8 | { 9 | local containers = super.spec.template.spec.containers, 10 | spec+: { template+: { spec+: { containers: [ 11 | if c.name == name 12 | then c + obj 13 | else c 14 | for c in containers 15 | ] } } }, 16 | }; 17 | 18 | 19 | test.new(std.thisFile) 20 | + test.case.new( 21 | 'Basic', 22 | test.expect.eq( 23 | webserver.new(webserverName), 24 | base 25 | ) 26 | ) 27 | + test.case.new( 28 | 'Change default replicas', 29 | test.expect.eq( 30 | webserver.new(webserverName, 2), 31 | base { deployment+: { spec+: { replicas: 2 } } } 32 | ) 33 | ) 34 | + test.case.new( 35 | 'Set alternative image', 36 | test.expect.eq( 37 | webserver.new(webserverName) 38 | + webserver.withImage('httpd:2.5'), 39 | base { deployment+: mapContainerWithName('httpd', { image: 'httpd:2.5' }) } 40 | ) 41 | ) 42 | + test.case.new( 43 | 'Set imagePullPolicy', 44 | test.expect.eq( 45 | webserver.new(webserverName) 46 | + webserver.withImagePullPolicy('Always'), 47 | base { deployment+: mapContainerWithName('httpd', { imagePullPolicy: 'Always' }) } 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example5.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example5.jsonnet 2 | RUNTIME ERROR: Failed 1/4 test cases: 3 | Set imagePullPolicy: Expected {"deployment": {"apiVersion": "apps/v1", "kind": "Deployment", "metadata": {"name": "webserver1"}, "spec": {"replicas": 1, "selector": {"matchLabels": {"name": "webserver1"}}, "template": {"metadata": {"labels": {"name": "webserver1"}}, "spec": {"containers": [{"imagePullPolicy": "Always"}]}}}}} to be {"deployment": {"apiVersion": "apps/v1", "kind": "Deployment", "metadata": {"name": "webserver1"}, "spec": {"replicas": 1, "selector": {"matchLabels": {"name": "webserver1"}}, "template": {"metadata": {"labels": {"name": "webserver1"}}, "spec": {"containers": [{"image": "httpd:2.4", "imagePullPolicy": "Always", "name": "httpd"}]}}}}} 4 | vendor/testonnet/main.libsonnet:(78:11)-(84:13) thunk from > 5 | vendor/testonnet/main.libsonnet:(74:7)-(87:8) object 6 | Field "verify" 7 | During manifestation 8 | 9 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example6.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/wrong1.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | local mapContainerWithName(name, obj) = 8 | { 9 | local containers = super.spec.template.spec.containers, 10 | spec+: { template+: { spec+: { containers: [ 11 | if c.name == name 12 | then c + obj 13 | else c 14 | for c in containers 15 | ] } } }, 16 | }; 17 | 18 | local eqJson = test.expect.new( 19 | function(actual, expected) actual == expected, 20 | function(actual, expected) 21 | 'Actual:\n' 22 | + std.manifestJson(actual) 23 | + '\nExpected:\n' 24 | + std.manifestJson(expected), 25 | ); 26 | 27 | test.new(std.thisFile) 28 | + test.case.new( 29 | 'Basic', 30 | eqJson( 31 | webserver.new(webserverName), 32 | base 33 | ) 34 | ) 35 | + test.case.new( 36 | 'Change default replicas', 37 | eqJson( 38 | webserver.new(webserverName, 2), 39 | base { deployment+: { spec+: { replicas: 2 } } } 40 | ) 41 | ) 42 | + test.case.new( 43 | 'Set alternative image', 44 | eqJson( 45 | webserver.new(webserverName) 46 | + webserver.withImage('httpd:2.5'), 47 | base { deployment+: mapContainerWithName('httpd', { image: 'httpd:2.5' }) } 48 | ) 49 | ) 50 | + test.case.new( 51 | 'Set imagePullPolicy', 52 | eqJson( 53 | webserver.new(webserverName) 54 | + webserver.withImagePullPolicy('Always'), 55 | base { deployment+: mapContainerWithName('httpd', { imagePullPolicy: 'Always' }) } 56 | ) 57 | ) 58 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example6.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example6.jsonnet 2 | RUNTIME ERROR: Failed 1/4 test cases: 3 | Set imagePullPolicy: Actual: 4 | { 5 | "deployment": { 6 | "apiVersion": "apps/v1", 7 | "kind": "Deployment", 8 | "metadata": { 9 | "name": "webserver1" 10 | }, 11 | "spec": { 12 | "replicas": 1, 13 | "selector": { 14 | "matchLabels": { 15 | "name": "webserver1" 16 | } 17 | }, 18 | "template": { 19 | "metadata": { 20 | "labels": { 21 | "name": "webserver1" 22 | } 23 | }, 24 | "spec": { 25 | "containers": [ 26 | { 27 | "imagePullPolicy": "Always" 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | } 34 | } 35 | Expected: 36 | { 37 | "deployment": { 38 | "apiVersion": "apps/v1", 39 | "kind": "Deployment", 40 | "metadata": { 41 | "name": "webserver1" 42 | }, 43 | "spec": { 44 | "replicas": 1, 45 | "selector": { 46 | "matchLabels": { 47 | "name": "webserver1" 48 | } 49 | }, 50 | "template": { 51 | "metadata": { 52 | "labels": { 53 | "name": "webserver1" 54 | } 55 | }, 56 | "spec": { 57 | "containers": [ 58 | { 59 | "image": "httpd:2.4", 60 | "imagePullPolicy": "Always", 61 | "name": "httpd" 62 | } 63 | ] 64 | } 65 | } 66 | } 67 | } 68 | } 69 | vendor/testonnet/main.libsonnet:(78:11)-(84:13) thunk from > 70 | vendor/testonnet/main.libsonnet:(74:7)-(87:8) object 71 | Field "verify" 72 | During manifestation 73 | 74 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example7.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/correct.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | local mapContainerWithName(name, obj) = 8 | { 9 | local containers = super.spec.template.spec.containers, 10 | spec+: { template+: { spec+: { containers: [ 11 | if c.name == name 12 | then c + obj 13 | else c 14 | for c in containers 15 | ] } } }, 16 | }; 17 | 18 | local eqJson = test.expect.new( 19 | function(actual, expected) actual == expected, 20 | function(actual, expected) 21 | 'Actual:\n' 22 | + std.manifestJson(actual) 23 | + '\nExpected:\n' 24 | + std.manifestJson(expected), 25 | ); 26 | 27 | 28 | test.new(std.thisFile) 29 | + test.case.new( 30 | 'Basic', 31 | eqJson( 32 | webserver.new(webserverName), 33 | base 34 | ) 35 | ) 36 | + test.case.new( 37 | 'Change default replicas', 38 | eqJson( 39 | webserver.new(webserverName, 2), 40 | base { deployment+: { spec+: { replicas: 2 } } } 41 | ) 42 | ) 43 | + test.case.new( 44 | 'Set alternative image', 45 | eqJson( 46 | webserver.new(webserverName) 47 | + webserver.withImage('httpd:2.5'), 48 | base { deployment+: mapContainerWithName('httpd', { image: 'httpd:2.5' }) } 49 | ) 50 | ) 51 | + test.case.new( 52 | 'Set imagePullPolicy', 53 | eqJson( 54 | webserver.new(webserverName) 55 | + webserver.withImagePullPolicy('Always'), 56 | base { deployment+: mapContainerWithName('httpd', { imagePullPolicy: 'Always' }) } 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/example7.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor example7.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite example7.jsonnet 3 | { 4 | "verify": "Passed 4 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 8 | "subdir": "1.23" 9 | } 10 | }, 11 | "version": "main" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/testonnet.git", 17 | "subdir": "" 18 | } 19 | }, 20 | "version": "master" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/webserver/correct.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | local main = import 'main.libsonnet'; 3 | 4 | main { 5 | withImagePullPolicy(policy): { 6 | container+: 7 | k.core.v1.container.withImagePullPolicy(policy), 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/webserver/main.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | { 4 | new(name, replicas=1): { 5 | container:: 6 | k.core.v1.container.new('httpd', 'httpd:2.4'), 7 | 8 | deployment: 9 | k.apps.v1.deployment.new( 10 | name, 11 | replicas, 12 | [self.container] 13 | ), 14 | }, 15 | 16 | withImage(image): { 17 | container+: 18 | k.core.v1.container.withImage(image), 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/webserver/wrong1.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | local main = import 'main.libsonnet'; 3 | 4 | main { 5 | withImagePullPolicy(policy): { 6 | container: 7 | k.core.v1.container.withImagePullPolicy(policy), 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/webserver/wrong2.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | local main = import 'main.libsonnet'; 3 | 4 | main { 5 | withImagePullPolicy(policy): { 6 | container+: 7 | k.core.v1.container.withName(super.container.name + policy) 8 | + k.core.v1.container.withImagePullPolicy(policy), 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/lib/webserver/wrong3.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | local main = import 'main.libsonnet'; 3 | 4 | main { 5 | withImagePullPolicy(policy): { 6 | container+::: 7 | k.core.v1.container.withImagePullPolicy(policy), 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall1.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/wrong2.libsonnet'; 3 | 4 | local simple = webserver.new('webserver1'); 5 | local imagePull = 6 | webserver.new('webserver1') 7 | + webserver.withImagePullPolicy('Always'); 8 | 9 | test.new(std.thisFile) 10 | + test.case.new( 11 | 'Validate name', 12 | test.expect.eq( 13 | simple.deployment.metadata.name, 14 | 'webserver1', 15 | ) 16 | ) 17 | + test.case.new( 18 | 'Validate image name', 19 | test.expect.eq( 20 | simple.deployment.spec.template.spec.containers[0].name, 21 | 'httpd', 22 | ) 23 | ) 24 | + test.case.new( 25 | 'Validate imagePullPolicy', 26 | test.expect.eq( 27 | imagePull.deployment.spec.template.spec.containers[0].imagePullPolicy, 28 | 'Always', 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall1.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor pitfall1.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite pitfall1.jsonnet 3 | { 4 | "verify": "Passed 3 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall2.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/wrong2.libsonnet'; 3 | 4 | local simple = webserver.new('webserver1'); 5 | local imagePull = 6 | webserver.new('webserver1') 7 | + webserver.withImagePullPolicy('Always'); 8 | 9 | test.new(std.thisFile) 10 | + test.case.new( 11 | 'Validate name', 12 | test.expect.eq( 13 | simple.deployment.metadata.name, 14 | 'webserver1', 15 | ) 16 | ) 17 | + test.case.new( 18 | 'Validate image name', 19 | test.expect.eq( 20 | simple.deployment.spec.template.spec.containers[0].name, 21 | 'httpd', 22 | ) 23 | ) 24 | + test.case.new( 25 | 'Validate imagePullPolicy', 26 | test.expect.eq( 27 | imagePull.deployment.spec.template.spec.containers[0].imagePullPolicy, 28 | 'Always', 29 | ) 30 | ) 31 | + test.case.new( 32 | 'Validate name', 33 | test.expect.eq( 34 | imagePull.deployment.metadata.name, 35 | 'webserver1', 36 | ) 37 | ) 38 | + test.case.new( 39 | 'Validate image name', 40 | test.expect.eq( 41 | imagePull.deployment.spec.template.spec.containers[0].name, 42 | 'httpd', 43 | ) 44 | ) 45 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall2.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor pitfall2.jsonnet 2 | RUNTIME ERROR: Failed 1/5 test cases: 3 | Validate image name: Expected httpdAlways to be httpd 4 | vendor/testonnet/main.libsonnet:(78:11)-(84:13) thunk from > 5 | vendor/testonnet/main.libsonnet:(74:7)-(87:8) object 6 | Field "verify" 7 | During manifestation 8 | 9 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall3.jsonnet: -------------------------------------------------------------------------------- 1 | local test = import 'testonnet/main.libsonnet'; 2 | local webserver = import 'webserver/wrong3.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | test.new(std.thisFile) 8 | + test.case.new( 9 | 'Basic', 10 | test.expect.eq( 11 | webserver.new(webserverName), 12 | base 13 | ) 14 | ) 15 | + test.case.new( 16 | 'Set alternative image', 17 | test.expect.eq( 18 | (webserver.new(webserverName) 19 | + webserver.withImagePullPolicy('Always')).container, 20 | { 21 | name: 'httpd', 22 | image: 'httpd:2.4', 23 | imagePullPolicy: 'Always', 24 | } 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /lessons/lesson6/example1/pitfall3.jsonnet.output: -------------------------------------------------------------------------------- 1 | # jsonnet -J lib -J vendor pitfall3.jsonnet 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite pitfall3.jsonnet 3 | { 4 | "verify": "Passed 2 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 8 | "subdir": "1.23" 9 | } 10 | }, 11 | "version": "main" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/testonnet.git", 17 | "subdir": "" 18 | } 19 | }, 20 | "version": "master" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonnet-libs/jsonnet-training-course/9ec5eec738214a3dd11acec12afd9d1126d5ee21/lessons/lesson6/example2/lib/webserver/2 -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | @cd test/ && \ 4 | jb install && \ 5 | jsonnet -J vendor -J lib main.libsonnet 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/main.libsonnet: -------------------------------------------------------------------------------- 1 | local k = import 'k.libsonnet'; 2 | 3 | { 4 | new(name, replicas=1): { 5 | container:: 6 | k.core.v1.container.new('httpd', 'httpd:2.4'), 7 | 8 | deployment: 9 | k.apps.v1.deployment.new( 10 | name, 11 | replicas, 12 | [self.container] 13 | ), 14 | }, 15 | 16 | withImage(image): { 17 | container+: 18 | k.core.v1.container.withImage(image), 19 | }, 20 | 21 | withImagePullPolicy(policy): { 22 | container+: 23 | k.core.v1.container.withImagePullPolicy(policy), 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/make_test.output: -------------------------------------------------------------------------------- 1 | # make test 2 | TRACE: vendor/testonnet/main.libsonnet:74 Testing suite main.libsonnet 3 | { 4 | "verify": "Passed 4 test cases" 5 | } 6 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/test/.gitignore: -------------------------------------------------------------------------------- 1 | jsonnetfile.lock.json 2 | vendor/ 3 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/test/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": { 3 | "apiVersion": "apps/v1", 4 | "kind": "Deployment", 5 | "metadata": { 6 | "name": "webserver1" 7 | }, 8 | "spec": { 9 | "replicas": 1, 10 | "selector": { 11 | "matchLabels": { 12 | "name": "webserver1" 13 | } 14 | }, 15 | "template": { 16 | "metadata": { 17 | "labels": { 18 | "name": "webserver1" 19 | } 20 | }, 21 | "spec": { 22 | "containers": [ 23 | { 24 | "image": "httpd:2.4", 25 | "name": "httpd" 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/test/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/jsonnet-libs/k8s-libsonnet.git", 8 | "subdir": "1.23" 9 | } 10 | }, 11 | "version": "main" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/jsonnet-libs/testonnet.git", 17 | "subdir": "" 18 | } 19 | }, 20 | "version": "master" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/test/lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'github.com/jsonnet-libs/k8s-libsonnet/1.23/main.libsonnet') 2 | -------------------------------------------------------------------------------- /lessons/lesson6/example2/lib/webserver/test/main.libsonnet: -------------------------------------------------------------------------------- 1 | local webserver = import '../main.libsonnet'; 2 | local test = import 'testonnet/main.libsonnet'; 3 | 4 | local webserverName = 'webserver1'; 5 | local base = import 'base.json'; 6 | 7 | local mapContainerWithName(name, obj) = 8 | { 9 | local containers = super.spec.template.spec.containers, 10 | spec+: { template+: { spec+: { containers: [ 11 | if c.name == name 12 | then c + obj 13 | else c 14 | for c in containers 15 | ] } } }, 16 | }; 17 | 18 | local eqJson = test.expect.new( 19 | function(actual, expected) actual == expected, 20 | function(actual, expected) 21 | 'Actual:\n' 22 | + std.manifestJson(actual) 23 | + '\nExpected:\n' 24 | + std.manifestJson(expected), 25 | ); 26 | 27 | 28 | test.new(std.thisFile) 29 | + test.case.new( 30 | 'Basic', 31 | eqJson( 32 | webserver.new(webserverName), 33 | base 34 | ) 35 | ) 36 | + test.case.new( 37 | 'Change default replicas', 38 | eqJson( 39 | webserver.new(webserverName, 2), 40 | base { deployment+: { spec+: { replicas: 2 } } } 41 | ) 42 | ) 43 | + test.case.new( 44 | 'Set alternative image', 45 | eqJson( 46 | webserver.new(webserverName) 47 | + webserver.withImage('httpd:2.5'), 48 | base { deployment+: mapContainerWithName('httpd', { image: 'httpd:2.5' }) } 49 | ) 50 | ) 51 | + test.case.new( 52 | 'Set imagePullPolicy', 53 | eqJson( 54 | webserver.new(webserverName) 55 | + webserver.withImagePullPolicy('Always'), 56 | base { deployment+: mapContainerWithName('httpd', { imagePullPolicy: 'Always' }) } 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /lessons/lesson6/examples.jsonnet: -------------------------------------------------------------------------------- 1 | local example = (import 'coursonnet.libsonnet').example; 2 | [ 3 | example.new('./example1/example0.jsonnet'[2:], importstr './example1/example0.jsonnet', import './example1/example0.jsonnet'), 4 | example.new('./example1/example1.jsonnet'[2:], importstr './example1/example1.jsonnet', import './example1/example1.jsonnet'), 5 | example.new('./example1/example2.jsonnet'[2:], importstr './example1/example2.jsonnet', import './example1/example2.jsonnet'), 6 | example.new('./example1/example3.jsonnet'[2:], importstr './example1/example3.jsonnet', import './example1/example3.jsonnet'), 7 | example.new('./example1/example4.jsonnet'[2:], importstr './example1/example4.jsonnet', import './example1/example4.jsonnet'), 8 | example.new('./example1/example5.jsonnet'[2:], importstr './example1/example5.jsonnet', import './example1/example5.jsonnet'), 9 | example.new('./example1/example6.jsonnet'[2:], importstr './example1/example6.jsonnet', import './example1/example6.jsonnet'), 10 | example.new('./example1/example7.jsonnet'[2:], importstr './example1/example7.jsonnet', import './example1/example7.jsonnet'), 11 | example.new('./example1/pitfall1.jsonnet'[2:], importstr './example1/pitfall1.jsonnet', import './example1/pitfall1.jsonnet'), 12 | example.new('./example1/pitfall2.jsonnet'[2:], importstr './example1/pitfall2.jsonnet', import './example1/pitfall2.jsonnet'), 13 | example.new('./example1/pitfall3.jsonnet'[2:], importstr './example1/pitfall3.jsonnet', import './example1/pitfall3.jsonnet'), 14 | example.new('./example1/lib/webserver/correct.libsonnet'[2:], importstr './example1/lib/webserver/correct.libsonnet', import './example1/lib/webserver/correct.libsonnet'), 15 | example.new('./example1/lib/webserver/main.libsonnet'[2:], importstr './example1/lib/webserver/main.libsonnet', import './example1/lib/webserver/main.libsonnet'), 16 | example.new('./example1/lib/webserver/wrong1.libsonnet'[2:], importstr './example1/lib/webserver/wrong1.libsonnet', import './example1/lib/webserver/wrong1.libsonnet'), 17 | example.new('./example1/lib/webserver/wrong2.libsonnet'[2:], importstr './example1/lib/webserver/wrong2.libsonnet', import './example1/lib/webserver/wrong2.libsonnet'), 18 | example.new('./example1/lib/webserver/wrong3.libsonnet'[2:], importstr './example1/lib/webserver/wrong3.libsonnet', import './example1/lib/webserver/wrong3.libsonnet'), 19 | example.new('./example1/base.json'[2:], importstr './example1/base.json', import './example1/base.json'), 20 | example.new('./example1/example1.jsonnet.output'[2:], importstr './example1/example1.jsonnet.output', {filename:'./example1/example1.jsonnet.output'}), 21 | example.new('./example1/example2.jsonnet.output'[2:], importstr './example1/example2.jsonnet.output', {filename:'./example1/example2.jsonnet.output'}), 22 | example.new('./example1/example3.jsonnet.output'[2:], importstr './example1/example3.jsonnet.output', {filename:'./example1/example3.jsonnet.output'}), 23 | example.new('./example1/example4.jsonnet.output'[2:], importstr './example1/example4.jsonnet.output', {filename:'./example1/example4.jsonnet.output'}), 24 | example.new('./example1/example5.jsonnet.output'[2:], importstr './example1/example5.jsonnet.output', {filename:'./example1/example5.jsonnet.output'}), 25 | example.new('./example1/example6.jsonnet.output'[2:], importstr './example1/example6.jsonnet.output', {filename:'./example1/example6.jsonnet.output'}), 26 | example.new('./example1/example7.jsonnet.output'[2:], importstr './example1/example7.jsonnet.output', {filename:'./example1/example7.jsonnet.output'}), 27 | example.new('./example1/pitfall1.jsonnet.output'[2:], importstr './example1/pitfall1.jsonnet.output', {filename:'./example1/pitfall1.jsonnet.output'}), 28 | example.new('./example1/pitfall2.jsonnet.output'[2:], importstr './example1/pitfall2.jsonnet.output', {filename:'./example1/pitfall2.jsonnet.output'}), 29 | example.new('./example1/pitfall3.jsonnet.output'[2:], importstr './example1/pitfall3.jsonnet.output', {filename:'./example1/pitfall3.jsonnet.output'}), 30 | example.new('./example2/lib/webserver/2'[2:], importstr './example2/lib/webserver/2', {filename:'./example2/lib/webserver/2'}), 31 | example.new('./example2/lib/webserver/main.libsonnet'[2:], importstr './example2/lib/webserver/main.libsonnet', {filename:'./example2/lib/webserver/main.libsonnet'}), 32 | example.new('./example2/lib/webserver/Makefile'[2:], importstr './example2/lib/webserver/Makefile', {filename:'./example2/lib/webserver/Makefile'}), 33 | example.new('./example2/lib/webserver/make_test.output'[2:], importstr './example2/lib/webserver/make_test.output', {filename:'./example2/lib/webserver/make_test.output'}), 34 | example.new('./example2/lib/webserver/test/base.json'[2:], importstr './example2/lib/webserver/test/base.json', {filename:'./example2/lib/webserver/test/base.json'}), 35 | example.new('./example2/lib/webserver/test/.gitignore'[2:], importstr './example2/lib/webserver/test/.gitignore', {filename:'./example2/lib/webserver/test/.gitignore'}), 36 | example.new('./example2/lib/webserver/test/jsonnetfile.json'[2:], importstr './example2/lib/webserver/test/jsonnetfile.json', {filename:'./example2/lib/webserver/test/jsonnetfile.json'}), 37 | example.new('./example2/lib/webserver/test/jsonnetfile.lock.json'[2:], importstr './example2/lib/webserver/test/jsonnetfile.lock.json', {filename:'./example2/lib/webserver/test/jsonnetfile.lock.json'}), 38 | example.new('./example2/lib/webserver/test/lib/k.libsonnet'[2:], importstr './example2/lib/webserver/test/lib/k.libsonnet', {filename:'./example2/lib/webserver/test/lib/k.libsonnet'}), 39 | example.new('./example2/lib/webserver/test/main.libsonnet'[2:], importstr './example2/lib/webserver/test/main.libsonnet', {filename:'./example2/lib/webserver/test/main.libsonnet'}), 40 | ] 41 | -------------------------------------------------------------------------------- /lessons/lesson6/lesson.md: -------------------------------------------------------------------------------- 1 | Let's unit test the webserver library from the first exercise. 2 | 3 | %(example1/lib/webserver/main.libsonnet)s 4 | 5 | This library provides a number of functions to create a webserver. Each function 6 | eventually renders a bit of JSON. The `withImages()` function is supposed to be mixed in 7 | with the `new()`. While doing maintenance on this library or adding new features, 8 | a number of things could go wrong. A few unit tests can catch unintended changes early. 9 | 10 | --- 11 | 12 | %(example1/example0.jsonnet)s 13 | 14 | Let's generate a base from our library to build our tests on: 15 | 16 | `jsonnet -J lib -J vendor -o base.json example0.jsonnet` 17 | 18 | --- 19 | 20 | %(example1/base.json)s 21 | 22 | The output of the webserver deployment will look like this. Note that it doesn't include 23 | the hidden `container` field. This rendered representation will be used as the base for 24 | the unit tests. 25 | 26 | ### Initializing Testonnet 27 | 28 | For the unit tests, the [Testonnet](https://github.com/jsonnet-libs/testonnet) library 29 | provides a few primitives to get us started. 30 | 31 | `$ jb install github.com/jsonnet-libs/testonnet` 32 | 33 | Check out the [docs](https://github.com/jsonnet-libs/testonnet/blob/master/docs/README.md) for Testonnet. 34 | 35 | --- 36 | 37 | %(example1/example1.jsonnet)s 38 | 39 | A test suite is initialized by calling `new(name)`. The `name` will be printed during 40 | execution to help us find failing test cases. 41 | 42 | When a test case fails, Testonnet will use `error` to ensure a non-zero exit code. This 43 | has the side effect that the corresponding stack trace will be from the Testonnet 44 | library, rather than the failing test. When using `std.thisFile` in the `name`, it will 45 | be easier to find the failing test case. 46 | 47 | --- 48 | 49 | %(example1/example1.jsonnet.output)s 50 | 51 | Running the test suite can be done with this: 52 | 53 | `$ jsonnet -J vendor -J lib example1.jsonnet` 54 | 55 | The output will either show the failing test cases or count the successful test. 56 | 57 | --- 58 | 59 | ### Testing the webserver library 60 | 61 | #### `new()` 62 | 63 | %(example1/example2.jsonnet)s 64 | %(example1/example2.jsonnet.output)s 65 | 66 | `test.case.new(name, test)` adds a new test case to the suite. The `name` can be an 67 | arbitrary string, `test` is an object that can created with `test.expect`. In this 68 | example `test.expect.eq` compares 2 objects with the expectation that they are equal. 69 | 70 | The output from `webserver.new()` is compared to the rendered representation. Running the 71 | test suite returns a successful test. 72 | 73 | --- 74 | 75 | %(example1/example3.jsonnet)s 76 | %(example1/example3.jsonnet.output)s 77 | 78 | The `new()` function allows us to modify the `replicas` on the deployment, this will go 79 | into the 'actual' part of the test case. 80 | 81 | On the 'expected' part `base` is added with only the `replicas` attribute modified. 82 | 83 | This test ensures only the replicas are changed, it also reinforces the values tested in 84 | the 'Basic' test. 85 | 86 | #### `withImages()` 87 | 88 | %(example1/example4.jsonnet)s 89 | %(example1/example4.jsonnet.output)s 90 | 91 | Testing `withImages()` is a bit more complex. In the library this function modifies the 92 | hidden `container::` field, which eventually gets added to the deployment in `new()` 93 | through late-initialization. 94 | 95 | Again `new()` is called to set the 'actual' part, this time `withImages()` is 96 | concatenated to get a deployment with an alternative image. 97 | 98 | On the 'expected' side the container with name `httpd` in the deployment needs to be 99 | modified with the new image name, using the `mapContainerWithName` helper function to 100 | keep the test cases readable. 101 | 102 | Note that `mapContainerWithName` also preserves any other containers that may exist in 103 | the deployment, future-proofing the unit tests. 104 | 105 | ### Test-driven development 106 | 107 | Let's write a test for a new function `webserver.withImagePullPolicy(policy)`, which can 108 | then be added as a feature to the library. 109 | 110 | %(example1/example5.jsonnet)s 111 | 112 | The new test 'Set imagePullPolicy' is very similar to 'Set alternative image'. 113 | 114 | To use the same `base`, `new()` is concatenated with 115 | `withImagePullPolicy('Always')` on 'actual'. 116 | 117 | On 'expected' it uses the `mapWithContainerName` helper to set `imagePullPolicy` on 118 | the `httpd` container. 119 | 120 | --- 121 | 122 | %(example1/lib/webserver/wrong1.libsonnet)s 123 | 124 | Extending the library (referenced as `main`) with the `withImagePullPolicy()` function is 125 | quite straightforward. 126 | 127 | --- 128 | 129 | %(example1/example5.jsonnet.output)s 130 | 131 | Oh no, running the test shows a failure, how did that happen? The difference between 132 | expected and actual result can be found in the output... 133 | 134 | Turns out that the `test.expect.eq` function output is quite inconvenient, let's improve 135 | that. 136 | 137 | --- 138 | 139 | %(example1/example6.jsonnet)s 140 | 141 | To replace `test.expect.eq`, a new 'test' function needs to be created. This can be done 142 | with `test.expect.new(satisfy, message)`. 143 | 144 | The `satisfy` function should return a boolean with `actual` and `expected` as arguments. 145 | 146 | The `message` function returns a string and also accepts the `actual` and `expected` 147 | results as arguments, these can be used to display the results in the error message. 148 | 149 | --- 150 | 151 | %(example1/example6.jsonnet.output)s 152 | 153 | The output is now a bit more convenient. It turns out that the `container` is being 154 | replaced completely instead of having `imagePullPolicy` set. 155 | 156 | --- 157 | 158 | %(example1/lib/webserver/wrong1.libsonnet)s 159 | 160 | Can you spot the mistake? 161 | 162 | --- 163 | 164 | %(example1/lib/webserver/correct.libsonnet)s 165 | 166 | Turns out a `+` was forgotten on `container+:`. 167 | 168 | --- 169 | 170 | %(example1/example7.jsonnet.output)s 171 | 172 | With that fixed, the test suite succeeds. 173 | 174 | ### Pulling it together 175 | 176 | ``` 177 | example2/lib/webserver/ 178 | ├── main.libsonnet 179 | ├── Makefile 180 | └── test 181 | ├── base.json 182 | ├── jsonnetfile.json 183 | ├── lib/k.libsonnet 184 | └── main.libsonnet 185 | ``` 186 | 187 | With the test cases written, let's pull it all together in a `test/` subdirectory so that 188 | the test dependencies from `jsonnetfile.json` are not required to install the library. 189 | 190 | --- 191 | 192 | %(example2/lib/webserver/Makefile)s 193 | %(example2/lib/webserver/make_test.output)s 194 | 195 | With a `test` target in a Makefile, running the test cases becomes trivial. 196 | 197 | ### Pitfalls 198 | 199 | Just like with any test framework, a unit test can be written in such a way that they 200 | succeed while not actually validating the unit. 201 | 202 | #### Testing individual attributes 203 | 204 | %(example1/pitfall1.jsonnet)s 205 | %(example1/pitfall1.jsonnet.output)s 206 | 207 | While the unit tests here are valid on their own, they only validate individual 208 | attributes. They won't catch any changes `withImagePullPolicy()` might make to other 209 | attributes. 210 | 211 | --- 212 | 213 | %(example1/lib/webserver/wrong2.libsonnet)s 214 | 215 | For example, here `withImagePullPolicy()` function also changes `name` on the 216 | `container` while this was explicitly tested on the 'simple' use case. 217 | 218 | --- 219 | 220 | %(example1/pitfall2.jsonnet)s 221 | 222 | To cover for the name (and other tests), the unit tests for 'simple' need to be repeated 223 | for the 'imagePull' use case, resulting in an exponential growth of test case as the 224 | library gets extended. 225 | 226 | --- 227 | 228 | %(example1/pitfall2.jsonnet.output)s 229 | 230 | Adding the test shows the expected failure. 231 | 232 | #### Testing hidden attributes 233 | 234 | %(example1/pitfall3.jsonnet)s 235 | %(example1/pitfall3.jsonnet.output)s 236 | 237 | While a unit test can access and validate the content of a hidden attribute, it is likely 238 | not useful. From a testing perspective, the hidden attributes should be considered 239 | 'internals' to the function. 240 | 241 | As Jsonnet does late-initialization before returning a JSON, validating the output should 242 | also be done on all visible attributes it might affect. 243 | 244 | --- 245 | 246 | %(example1/lib/webserver/wrong3.libsonnet)s 247 | 248 | For example, here the `withImagePullPolicy()` function makes the `container` visible in 249 | the output, changing the intended behavior of `new()`. 250 | -------------------------------------------------------------------------------- /lessons/lesson6/main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local lesson = c.lesson; 3 | 4 | local examples = import './examples.jsonnet'; 5 | 6 | lesson.new( 7 | 'lesson6', 8 | 'Unit testing', 9 | ||| 10 | Performing maintenance on an existing library can be quite a task, the initial 11 | intention might not always be obvious. Adding a few unit tests can make a big 12 | difference years down the line. 13 | |||, 14 | [ 15 | 'Write unit tests in Jsonnet', 16 | 'Do test-driven development', 17 | 'Know how to avoid pitfalls', 18 | ], 19 | (importstr './lesson.md') % 20 | std.foldr( 21 | function(e, acc) 22 | acc { [e.filename]: e.render }, 23 | examples, 24 | {} 25 | ), 26 | ||| 27 | Writing unit tests can feel like a burden, but when done right they can be elegant 28 | and quite cheap to write. 29 | 30 | And remember: "A society grows great when old men plant trees whose shade they know 31 | they shall never sit in." 32 | |||, 33 | ) 34 | -------------------------------------------------------------------------------- /lessons/lessons.jsonnet: -------------------------------------------------------------------------------- 1 | [ 2 | (import './lesson1/main.jsonnet'), 3 | (import './lesson2/main.jsonnet'), 4 | (import './lesson3/main.jsonnet'), 5 | (import './lesson4/main.jsonnet'), 6 | (import './lesson5/main.jsonnet'), 7 | (import './lesson6/main.jsonnet'), 8 | ] 9 | -------------------------------------------------------------------------------- /main.jsonnet: -------------------------------------------------------------------------------- 1 | local c = import 'coursonnet.libsonnet'; 2 | local pages = import 'lessons/lessons.jsonnet'; 3 | 4 | local navMixin = function(pages, i) 5 | ( 6 | if i > 0 7 | then '[« previous](%s.html) ' % pages[i - 1].slug 8 | else '' 9 | ) 10 | + '[index](index.html)' 11 | + ( 12 | if i < (std.length(pages) - 1) 13 | then ' [next »](%s.html)' % pages[i + 1].slug 14 | else '' 15 | ) 16 | + '\n'; 17 | 18 | local about = (import 'lessons/about.jsonnet').render['about.md']; 19 | 20 | function(nav=false) 21 | (import 'lessons/index.jsonnet').render 22 | { 23 | 'about.md': 24 | if nav 25 | then 26 | '[index](index.html)\n' 27 | + about 28 | + '[index](index.html)\n' 29 | else about, 30 | } 31 | + { 32 | [pages[i].filename]: 33 | if nav 34 | then 35 | navMixin(pages, i) 36 | + pages[i].render 37 | + navMixin(pages, i) 38 | else 39 | pages[i].render 40 | for i in std.range(0, std.length(pages) - 1) 41 | 42 | } 43 | --------------------------------------------------------------------------------