├── 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 | Write an extensible library
44 | Understanding Package management
45 | Exercise: rewrite a library with k8s-libsonnet
46 | Further developing libraries
47 | Providing documentation with Docsonnet
48 | Unit testing
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 |
24 | Rewrite a library
25 | Vendor and use k8s-libsonnet
26 | Understand the lib/k.libsonnet
convention
27 |
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 | Write an extensible library
78 | Understanding Package management
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 |
--------------------------------------------------------------------------------