From f4103b535f93209128033dcf6f0287676dd741a5 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Fri, 14 Jun 2024 13:47:08 +0100 Subject: [PATCH] docs: rework tutorial to not use global $ (#1046) * docs: rework tutorial to not use global $ * Apply suggestions from code review Co-authored-by: Horst Gutmann * Update docs/src/content/docs/tutorial/abstraction.md Co-authored-by: Horst Gutmann --------- Co-authored-by: Horst Gutmann --- docs/src/content/docs/tutorial/abstraction.md | 142 +++++------ docs/src/content/docs/tutorial/k-lib.mdx | 103 ++++++-- docs/src/content/docs/tutorial/parameters.md | 223 ++++++++++++++---- 3 files changed, 313 insertions(+), 155 deletions(-) diff --git a/docs/src/content/docs/tutorial/abstraction.md b/docs/src/content/docs/tutorial/abstraction.md index 398462b9a..d25da9f8c 100644 --- a/docs/src/content/docs/tutorial/abstraction.md +++ b/docs/src/content/docs/tutorial/abstraction.md @@ -5,7 +5,7 @@ sidebar: --- While we won't need to touch the resource definitions directly that frequently -anymore now that we have the `_config` object for our tunables, the +anymore now that our deployments definitions are parametrized, the `main.jsonnet` file is still very long and hard to read. Especially because of all the brackets, it's even worse than yaml at the moment. @@ -13,41 +13,44 @@ all the brackets, it's even worse than yaml at the moment. Let's start cleaning this up by separating logical pieces into distinct files: -- `main.jsonnet`: Still our main file, containing the `_config` object and importing the other files -- `grafana.jsonnet`: `Deployment` and `Service` for the Grafana instance -- `prometheus.jsonnet`: `Deployment` and `Service` for the Prometheus server +- `main.jsonnet`: Still our main file, importing the other files +- `grafana.libsonnet`: `Deployment` and `Service` for the Grafana instance +- `prometheus.libsonnet`: `Deployment` and `Service` for the Prometheus server + +:::note +The extension for Jsonnet libraries is `.libsonnet`. While you do +not have to use it, it distinguishes helper code from actual configuration. +::: ```jsonnet -// /environments/default/grafana.jsonnet +// /environments/default/grafana.libsonnet { - // DO NOT use the root level here. - // Include the grafana subkey, otherwise $ won't work. - grafana: { + new(name, port)::{ deployment: { apiVersion: 'apps/v1', kind: 'Deployment', metadata: { - name: $._config.grafana.name, + name: name, }, spec: { selector: { matchLabels: { - name: $._config.grafana.name, + name: name, }, }, template: { metadata: { labels: { - name: $._config.grafana.name, + name: name, }, }, spec: { containers: [ { image: 'grafana/grafana', - name: $._config.grafana.name, + name: name, ports: [{ - containerPort: $._config.grafana.port, + containerPort: port, name: 'ui', }], }, @@ -61,18 +64,18 @@ Let's start cleaning this up by separating logical pieces into distinct files: kind: 'Service', metadata: { labels: { - name: $._config.grafana.name, + name: name, }, - name: $._config.grafana.name, + name: name, }, spec: { ports: [{ - name: '%s-ui' % $._config.grafana.name, - port: $._config.grafana.port, - targetPort: $._config.grafana.port, + name: '%s-ui' % name, + port: port, + targetPort: port, }], selector: { - name: $._config.grafana.name, + name: name, }, type: 'NodePort', }, @@ -81,39 +84,20 @@ Let's start cleaning this up by separating logical pieces into distinct files: } ``` -The file should contain just the same that was located under the `grafana` key -on the root object before. Do the same for `/environments/default/prometheus.jsonnet` as well. +The file should contain an object with the same function that was defined under the `grafana` in `/environments/default/main.jsonnet`, but called `new` instead of `grafana`. +Do the same for `/environments/default/prometheus.libsonnet` as well. ```jsonnet // /environments/default/main.jsonnet -// Think of `import` as copy-pasting the contents -// of ./grafana.jsonnet here -(import "grafana.jsonnet") + -(import "prometheus.jsonnet") + +local grafana = import "grafana.libsonnet"; +local prometheus = import "prometheus.libsonnet"; + { - _config:: { - grafana: { - port: 3000, - name: "grafana", - }, - prometheus: { - port: 9090, - name: "prometheus" - } - } + grafana: grafana.new("grafana", 3000), + prometheus: prometheus.new("prometheus", 9090), } ``` -:::note[Clarification] -It might seem odd at first sight, that this code works, because -`grafana.jsonnet` still refers to the root object using `$`, even -though it is outside of the file's scope. -However, Jsonnet is lazy-evaluated which means that the contents of -`grafana.jsonnet` are **first "copied"** into `main.jsonnet` (the root -object) and **then evaluated**. This means the above code actually consists of -all three objects joined to one big object, which is then converted to JSON. -::: - ## Helper utilities While `main.jsonnet` is now short and very readable, the other two files are not @@ -124,37 +108,30 @@ Let's use functions to create some useful helpers to reduce the amount of repetition. For that, we create a new file called `kubernetes.libsonnet`, which will hold our Kubernetes utilities. -:::note -The extension for Jsonnet libraries is `.libsonnet`. While you do -not have to use it, it distinguishes helper code from actual configuration. -::: - ### A Deployment constructor Creating a `Deployment` requires some mandatory information and a lot of boilerplate. A function that creates one could look like this: ```jsonnet +// /environments/default/kubernetes.libsonnet { - // hidden k namespace for this library - k:: { - deployment: { - new(name, containers): { - apiVersion: "apps/v1", - kind: "Deployment", - metadata: { + deployment: { + new(name, containers):: { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + name: name, + }, + spec: { + selector: { matchLabels: { name: name, - }, - spec: { - selector: { matchLabels: { + }}, + template: { + metadata: { labels: { name: name, }}, - template: { - metadata: { labels: { - name: name, - }}, - spec: { containers: containers } - } + spec: { containers: containers } } } } @@ -165,25 +142,18 @@ boilerplate. A function that creates one could look like this: Invoking this function will substitute all the variables with the respective passed function parameters and return the assembled object. -To use it, just add it to the root object in `main.jsonnet`: +Let's simplify our `grafana.libsonnet` a bit: ```jsonnet - (import "kubernetes.libsonnet") + // this line adds it - (import "grafana.jsonnet") + - (import "prometheus.jsonnet") + - { /* ... */ } -``` +local k = import "kubernetes.libsonnet"; -Let's simplify our `grafana.jsonnet` a bit: - -```jsonnet { - grafana: { - deployment: $.k.deployment.new("grafana", [{ + new(name, port):: { + deployment: k.deployment.new(name, [{ image: 'grafana/grafana', - name: 'grafana', + name: name, ports: [{ - containerPort: 3000, + containerPort: port, name: 'ui', }], }]), @@ -192,18 +162,18 @@ Let's simplify our `grafana.jsonnet` a bit: kind: 'Service', metadata: { labels: { - name: 'grafana', + name: name, }, - name: 'grafana', + name: name, }, spec: { ports: [{ - name: 'grafana-ui', - port: 3000, - targetPort: 3000, + name: '%s-ui' % name, + port: port, + targetPort: port, }], selector: { - name: 'grafana', + name: name, }, type: 'NodePort', }, @@ -213,7 +183,7 @@ Let's simplify our `grafana.jsonnet` a bit: ``` This drastically simplified the creation of the `Deployment`, because we do not -need to remember how exactly a `Deployment` is structured anymore. Just call use +need to remember how exactly a `Deployment` is structured anymore. Just use our helper and you are good to go. :::tip[Task] diff --git a/docs/src/content/docs/tutorial/k-lib.mdx b/docs/src/content/docs/tutorial/k-lib.mdx index eeec1a08f..99d4aa648 100644 --- a/docs/src/content/docs/tutorial/k-lib.mdx +++ b/docs/src/content/docs/tutorial/k-lib.mdx @@ -102,8 +102,8 @@ First we need to import it in `main.jsonnet`: ```diff - local k = import "kubernetes.libsonnet"; + local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet"; - local grafana = import "grafana.jsonnet"; - local prometheus = import "prometheus.jsonnet"; + local grafana = import "grafana.libsonnet"; + local prometheus = import "prometheus.libsonnet"; { /* ... */ } ``` @@ -115,10 +115,10 @@ aliasing. ::: Now that we have installed the correct version, let's use it in -`/environments/default/grafana.jsonnet` instead of our own helper: +`/environments/default/grafana.libsonnet` instead of our own helper: ```jsonnet -// /environments/default/grafana.jsonnet +// /environments/default/grafana.libsonnet local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet"; { @@ -128,13 +128,13 @@ local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet" local port = k.core.v1.containerPort, local service = k.core.v1.service, // defining the objects: - grafana: { + new(name, port):: { // deployment constructor: name, replicas, containers - deployment: deploy.new(name=$._config.grafana.name, replicas=1, containers=[ + deployment: deploy.new(name, replicas=1, containers=[ // container constructor - container.new($._config.grafana.name, "grafana/grafana") + container.new(, "grafana/grafana") + container.withPorts( // add ports to the container - [port.new("ui", $._config.grafana.port)] // port constructor + [port.new("ui", 3000)] // port constructor ), ]), @@ -155,6 +155,50 @@ whole picture: ```jsonnet local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet"; +{ + local deployment = k.apps.v1.deployment, + local container = k.core.v1.container, + local port = k.core.v1.containerPort, + local service = k.core.v1.service, + + prometheus: { + deployment: deployment.new( + name="prometheus", replicas=1, + containers=[ + container.new("prometheus", "prom/prometheus") + + container.withPorts([port.new("api", 9090)]), + ], + ), + service: k.util.serviceFor(self.deployment), + }, + grafana: { + deployment: deployment.new( + name="grafana", replicas=1, + containers=[ + container.new("grafana", "grafana/grafana") + + container.withPorts([port.new("ui", 3000)]), + ], + ), + service: + k.util.serviceFor(self.deployment) + + service.mixin.spec.withType("NodePort"), + }, +} +``` + +That's a pretty big improvement, considering how verbose and error-prone it was +before! + +## Bonus: Config object + +While this is already a huge improvement, we can do a bit more. There is still some repetition in the `main.jsonnet` file. +The most straightforward way to address this is by creating a hidden object that holds all actual values in a single place to be consumed by the actual resources. + +Luckily, Jsonnet has the `key:: "value"` stanza for private fields. Such are only available during compiling and will be removed from the actual output. + +Such an object could look like this: + +```jsonnet { _config:: { grafana: { @@ -165,7 +209,26 @@ local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet" port: 9090, name: "prometheus" } - }, + } +} +``` + +We can then replace hardcoded values with a reference to this object: + +```diff lang="jsonnet" ///.*/ +local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet"; + +{ // <- This is $ ++ _config:: { ++ grafana: { ++ port: 3000, ++ name: "grafana", ++ }, ++ prometheus: { ++ port: 9090, ++ name: "prometheus" ++ } ++ } local deployment = k.apps.v1.deployment, local container = k.core.v1.container, @@ -174,20 +237,27 @@ local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet" prometheus: { deployment: deployment.new( - name=$._config.prometheus.name, replicas=1, +- name="prometheus", replicas=1, ++ // $ refers to the outermost object ++ name=$._config.prometheus.name, replicas=1, containers=[ - container.new($._config.prometheus.name, "prom/prometheus") - + container.withPorts([port.new("api", $._config.prometheus.port)]), +- container.new("prometheus", "prom/prometheus") +- + container.withPorts([port.new("api", 9090)]), ++ container.new($._config.prometheus.name, "prom/prometheus") ++ + container.withPorts([port.new("api", $._config.prometheus.port)]), ], ), service: k.util.serviceFor(self.deployment), }, grafana: { deployment: deployment.new( - name=$._config.grafana.name, replicas=1, +- name="grafana", replicas=1, ++ name=$._config.grafana.name, replicas=1, containers=[ - container.new($._config.grafana.name, "grafana/grafana") - + container.withPorts([port.new("ui", $._config.grafana.port)]), +- container.new("grafana", "grafana/grafana") +- + container.withPorts([port.new("ui", 3000)]), ++ container.new($._config.grafana.name, "grafana/grafana") ++ + container.withPorts([port.new("ui", $._config.grafana.port)]), ], ), service: @@ -196,6 +266,3 @@ local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet" }, } ``` - -That's a pretty big improvement, considering how verbose and error-prone it was -before! diff --git a/docs/src/content/docs/tutorial/parameters.md b/docs/src/content/docs/tutorial/parameters.md index f955679dc..97d99c041 100644 --- a/docs/src/content/docs/tutorial/parameters.md +++ b/docs/src/content/docs/tutorial/parameters.md @@ -9,73 +9,194 @@ in terms of maintainability and readability. To do so, the following sections will explore some ways Jsonnet provides us with. -## Config object +## Functions parameters -The most straightforward thing to do is creating a hidden object that holds all -actual values in a single place to be consumed by the actual resources. +Defining our deployment in a single block is not the best solution. +Luckily with Jsonnet we can split our configuration into smaller, self-contained chunks. -Luckily, Jsonnet has the `key:: "value"` stanza for private fields. Such are -only available during compiling and will be removed from the actual output. +Let's start by creating a new function in `main.jsonnet` responsible for creating a Grafana deployment: -Such an object could look like this: +```diff lang="jsonnet" +// envirnoments/default/main.jsonnet +local grafana() = { + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'grafana', + }, + spec: { + selector: { + matchLabels: { + name: 'grafana', + }, + }, + template: { + metadata: { + labels: { + name: 'grafana', + }, + }, + spec: { + containers: [ + { + image: 'grafana/grafana', + name: 'grafana', + ports: [{ + containerPort: 3000, + name: 'ui', + }], + }, + ], + }, + }, + }, + }, + service: { + apiVersion: 'v1', + kind: 'Service', + metadata: { + labels: { + name: 'grafana', + }, + name: 'grafana', + }, + spec: { + ports: [{ + name: 'grafana-ui', + port: 3000, + targetPort: 3000, + }], + selector: { + name: 'grafana', + }, + type: 'NodePort', + }, + }, +}; +``` + +and let's use it in our main configuration: + +```diff lang="jsonnet" +// environments/default/main.jsonnet +local grafana() = { + # ... +}; -```jsonnet { - _config:: { - grafana: { - port: 3000, - name: "grafana", - }, - prometheus: { - port: 9090, - name: "prometheus" - } - } -} +- grafana: { +- # ... +- }, ++ grafana: grafana(), + prometheus: #... +}; ``` -We can then replace hardcoded values with a reference to this object: - -```diff -{ // <- This is $ - _config:: { /* .. */ }, - grafana: { - service: { - apiVersion: 'v1', - kind: 'Service', - metadata: { - labels: { -- name: 'grafana', -+ name: $._config.grafana.name, // $ refers to the outermost object +We can then replace hardcoded values by adding parameters to our function: + +```diff lang="jsonnet" +// environments/default/main.jsonnet +-local grafana() = { ++local grafana(name, port) = { + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { +- name: 'grafana', ++ name: name, + }, + spec: { + selector: { + matchLabels: { +- name: 'grafana', ++ name: name, }, -- name: 'grafana', -+ name: $._config.grafana.name, }, - spec: { - ports: [{ -- name: 'grafana-ui', -+ name: '%s-ui' % $._config.grafana.name, // printf-style formatting -- port: 3000, -+ port: $._config.grafana.port, -- targetPort: 3000, -+ targetPort: $._config.grafana.port, - }], - selector: { -- name: 'grafana', -+ name: $._config.grafana.name, + template: { + metadata: { + labels: { +- name: 'grafana', ++ name: name, + }, }, - type: 'NodePort', + spec: { + containers: [ + { + image: 'grafana/grafana', +- name: 'grafana', ++ name: name, + ports: [{ +- containerPort: 3000, ++ containerPort: port, + name: 'ui', + }], + }, + ], + }, + }, + }, + }, + service: { + apiVersion: 'v1', + kind: 'Service', + metadata: { + labels: { +- name: 'grafana', ++ name: name, }, +- name: 'grafana', ++ name: name, + }, + spec: { + ports: [{ +- name: 'grafana-ui', +- port: 3000, +- targetPort: 3000, ++ name: '%s-ui' % name, // printf-style formatting ++ port: port, ++ targetPort: port, + }], + selector: { +- name: 'grafana', ++ name: name, + }, + type: 'NodePort', }, }, -} +}; ``` -Here we see that we can easily refer to other parts of the configuration using -the outer-most object `$` (the root level). Every value is just a regular -variable that you can refer to using the same familiar syntax from other C-like -languages. +and update the usage accordingly: + +```diff lang="jsonnet" +// environments/default/main.jsonnet +local grafana(name, port) = { + # ... +}; + +{ +- grafana: grafana(), ++ grafana: grafana('grafana', 3000), + prometheus: #... +}; +``` + +:::tip +You can also set default values for function parameters: + +```jsonnet +local grafana(name='grafana', port=3000) = { + # ... +}; +``` + +::: Now we do not only have a single place to change tunables, but also won't suffer from mismatching labels and selectors anymore, as they are defined in a single place and all changed at once. + +:::tip[Task] +Now do the same for the Prometheus deployment by creating a function `prometheus` that takes a `name` and a `port` as parameters. +:::