blob: 551d3cde02a951bb05f7f1ef22e82149ffc1eebd [file] [log] [blame] [view]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001# Kubernetes tutorial
2
3In this tutorial we show how to convert Kubernetes configuration files
4for a collection of microservices.
5
6The configuration files are scrubbed and renamed versions of
7real-life configuration files.
8The files are organized in a directory hierarchy grouping related services
9in subdirectories.
10This is a common pattern.
11The `cue` tooling has been optimized for this use case.
12
Emil Hessman66ec9592019-07-14 17:58:27 +020013In this tutorial we will address the following topics:
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010014
151. convert the given YAML files to CUE
161. hoist common patterns to parent directories
171. use the tooling to rewrite CUE files to drop unnecessary fields
181. repeat from step 2 for different subdirectories
191. define commands to operate on the configuration
Joel Longtineb6544662019-06-11 16:59:12 +0000201. extract CUE templates directly from Kubernetes Go source
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100211. manually tailor the configuration
221. map a Kubernetes configuration to `docker-compose` (TODO)
23
24
25## The given data set
26
27The data set is based on a real-life case, using different names for the
28services.
29All the inconsistencies of the real setup are replicated in the files
30to get a realistic impression of how a conversion to CUE would behave
31in practice.
32
33The given YAML files are ordered in following directory
34(you can use `find` if you don't have tree):
35
36```
37$ tree ./testdata | head
38.
39└── services
40 ├── frontend
41 │ ├── bartender
42 │ │ └── kube.yaml
43 │ ├── breaddispatcher
44 │ │ └── kube.yaml
45 │ ├── host
46 │ │ └── kube.yaml
47 │ ├── maitred
48...
49```
50
51Each subdirectory contains related microservices that often share similar
52characteristics and configurations.
53The configurations include a large variety of Kubernetes objects, including
54services, deployments, config maps,
55a daemon set, a stateful set, and a cron job.
56
57The result of the first tutorial is in the `quick`, for "quick and dirty"
58directory.
59A manually optimized configuration can be found int `manual`
60directory.
61
62
63## Importing existing configuration
64
65We first make a copy of the data directory.
66
67```
Marcel van Lohuizenac39cd72019-06-11 22:27:54 +020068$ cp -a original tmp
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010069$ cd tmp/services
70```
71
72<!-- TODO
73Although not strictly necessary, we mark the root of the configuration tree
74for good measure.
75
76```
Marcel van Lohuizen65ccf1f2019-06-11 22:55:23 +020077$ touch ../cue.mod
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +020078cue mod init
Marcel van Lohuizen02173f82018-12-20 13:27:07 +010079```
80-->
81
82Let's try to use the `cue import` command to convert the given YAML files
83into CUE.
84
85```
86$ cue import ./...
87Import failed: must specify package name with the -p flag
88```
89
90Since we have multiple packages and files, we need to specify the package to
91which they should belong.
92
93```
94$ cue import ./... -p kube
95Import failed: list, flag, or files flag needed to handle multiple objects in file "./frontend/bartender/kube.yaml"
96```
97
98Many of the files contain more than one Kubernetes object.
99Moreover, we are creating a single configuration that contains all objects
100of all files.
101We need to organize all Kubernetes objects such that each is individually
Emil Hessman66ec9592019-07-14 17:58:27 +0200102identifiable within a single configuration.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100103We do so by defining a different struct for each type putting each object
104in this respective struct keyed by its name.
105This allows objects of different types to share the same name,
106just as is allowed by Kubernetes.
107To accomplish this, we tell `cue` to put each object in the configuration
108tree at the path with the "kind" as first element and "name" as second.
109
110```
111$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -f
112```
113
114The added `-l` flag defines the labels for each object, based on values from
115each object, using the usual CUE syntax for field labels.
116In this case, we use a camelcase variant of the `kind` field of each object and
117use the `name` field of the `metadata` section as the name for each object.
118We also added the `-f` flag to overwrite the few files that succeeded before.
119
120Let's see what happened:
121
122```
123$ tree . | head
124.
125└── services
126 ├── frontend
127 │ ├── bartender
128 │ │ ├── kube.cue
129 │ │ └── kube.yaml
130 │ ├── breaddispatcher
131 │ │ ├── kube.cue
132 │ │ └── kube.yaml
133...
134```
135
136Each of the YAML files is converted to corresponding CUE files.
137Comments of the YAML files are preserved.
138
139The result is not fully pleasing, though.
140Take a look at `mon/prometheus/configmap.cue`.
141
142```
143$ cat mon/prometheus/configmap.cue
144package kube
145
146apiVersion: "v1"
147kind: "ConfigMap"
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200148metadata: name: "prometheus"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100149data: {
150 "alert.rules": """
151 groups:
152 - name: rules.yaml
153...
154```
155
156The configuration file still contains YAML embedded in a string value of one
157of the fields.
158The original YAML file might have looked like it was all structured data, but
159the majority of it was a string containing, hopefully, valid YAML.
160
161The `-R` option attempts to detect structured YAML or JSON strings embedded
162in the configuration files and then converts these recursively.
163
164```
165$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -f -R
166```
167
168Now the file looks like:
169
170```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200171$ cat mon/prometheus/configmap.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100172package kube
173
174import "encoding/yaml"
175
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200176configMap: prometheus: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100177 apiVersion: "v1"
178 kind: "ConfigMap"
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200179 metadata: name: "prometheus"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100180 data: {
181 "alert.rules": yaml.Marshal(_cue_alert_rules)
182 _cue_alert_rules: {
183 groups: [{
184...
185```
186
187That looks better!
188The resulting configuration file replaces the original embedded string
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200189with a call to `yaml.Marshal` converting a structured CUE source to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100190a string with an equivalent YAML file.
191Fields starting with an underscore (`_`) are not included when emitting
192a configuration file (they are when enclosed in double quotes).
193
194```
195$ cue eval ./mon/prometheus -e configMap.prometheus
196apiVersion: "v1"
197kind: "ConfigMap"
198metadata: {
199 name: "prometheus"
200}
201data: {
202 "alert.rules": """
203 groups:
204 - name: rules.yaml
205...
206```
207
208Yay!
209
210
211## Quick 'n Dirty Conversion
212
213In this tutorial we show how to quickly eliminate boilerplate from a set
214of configurations.
215Manual tailoring will usually give better results, but takes considerably
216more thought, while taking the quick and dirty approach gets you mostly there.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100217The result of such a quick conversion also forms a good basis for
218a more thoughtful manual optimization.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100219
220### Create top-level template
221
222Now we have imported the YAML files we can start the simplification process.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100223
224Before we start the restructuring, lets save a full evaluation so that we
225can verify that simplifications yield the same results.
226
227```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200228$ cue eval -c ./... > snapshot
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100229```
230
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200231The `-c` option tells `cue` that only concrete values, that is valid JSON,
232are allowed.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100233We focus on the objects defined in the various `kube.cue` files.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100234A quick inspection reveals that many of the Deployments and Services share
235common structure.
236
237We copy one of the files containing both as a basis for creating our template
238to the root of the directory tree.
239
240```
241$ cp frontend/breaddispatcher/kube.cue .
242```
243
244Modify this file as below.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100245
246```
247$ cat <<EOF > kube.cue
248package kube
249
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200250service: [ID=_]: {
Marko Mikulicicc3d0b482019-07-09 13:58:21 +0200251 apiVersion: "v1"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100252 kind: "Service"
253 metadata: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200254 name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100255 labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200256 app: ID // by convention
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100257 domain: "prod" // always the same in the given files
258 component: string // varies per directory
259 }
260 }
261 spec: {
262 // Any port has the following properties.
263 ports: [...{
264 port: int
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100265 protocol: *"TCP" | "UDP" // from the Kubernetes definition
266 name: string | *"client"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100267 }]
268 selector: metadata.labels // we want those to be the same
269 }
270}
271
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200272deployment: [ID=_]: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100273 apiVersion: "extensions/v1beta1"
274 kind: "Deployment"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200275 metadata name: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100276 spec: {
277 // 1 is the default, but we allow any number
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200278 replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100279 template: {
280 metadata labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200281 app: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100282 domain: "prod"
283 component: string
284 }
285 // we always have one namesake container
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200286 spec containers: [{ name: ID }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100287 }
288 }
289}
290EOF
291```
292
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200293By replacing the service and deployment name with `[ID=_]` we have changed the
294definition into a template matching any field.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200295CUE bind the field name to `ID` as a result.
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +0000296During importing we used `metadata.name` as a key for the object names,
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200297so we can now set this field to `ID`.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100298
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100299Templates are applied to (are unified with) all entries in the struct in which
300they are defined,
301so we need to either strip fields specific to the `breaddispatcher` definition,
302generalize them, or remove them.
303
304One of the labels defined in the Kubernetes metadata seems to be always set
305to parent directory name.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100306We enforce this by defining `component: string`, meaning that a field
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +0100307of name `component` must be set to some string value, and then define this
308later on.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100309Any underspecified field results in an error when converting to, for instance,
310JSON.
311So a deployment or service will only be valid if this label is defined.
312
313<!-- TODO: once cycles in disjunctions are implemented
314 port: targetPort | int // by default the same as targetPort
315 targetPort: port | int // by default the same as port
316
317Note that ports definition for service contains a cycle.
318Specifying one of the ports will break the cycle.
319The meaning of cycles are well-defined in CUE.
320In practice this means that a template writer does not have to make any
321assumptions about which of the fields that can be mutually derived from each
322other a user of the template will want to specify.
323-->
324
325Let's compare the result of merging our new template to our original snapshot.
326
327```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200328$ cue eval ./... -c > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100329--- ./mon/alertmanager
330non-concrete value (string)*:
331 ./kube.cue:11:15
332
333non-concrete value (string)*:
334 ./kube.cue:11:15
335
336non-concrete value (string)*:
337 ./kube.cue:34:16
338```
339
340<!-- TODO: better error messages -->
341
342Oops.
343The alert manager does not specify the `component` label.
344This demonstrates how constraints can be used to catch inconsistencies
345in your configurations.
346
347As there are very few objects that do not specify this label, we will modify
348the configurations to include them everywhere.
349We do this by setting a newly defined top-level field in each directory
350to the directory name and modify our master template file to use it.
351
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200352<!--
353```
354$ cue add */kube.cue -p kube --list <<EOF
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200355Component :: "{{.DisplayPath}}"
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200356EOF
357```
358-->
359
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100360```
361# set the component label to our new top-level field
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200362$ sed -i.bak 's/component:.*string/component: Component/' kube.cue && rm kube.cue.bak
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100363
364# add the new top-level field to our previous template definitions
365$ cat <<EOF >> kube.cue
366
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200367Component :: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100368EOF
369
370# add a file with the component label to each directory
371$ ls -d */ | sed 's/.$//' | xargs -I DIR sh -c 'cd DIR; echo "package kube
372
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200373Component :: \"DIR\"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100374" > kube.cue; cd ..'
375
376# format the files
377$ cue fmt kube.cue */kube.cue
378```
379
380Let's try again to see if it is fixed:
381
382```
Marcel van Lohuizendb4e4d22019-04-18 08:43:57 +0200383$ cue eval -c ./... > snapshot2
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100384$ diff snapshot snapshot2
385...
386```
387
388Except for having more consistent labels and some reordering, nothing changed.
389We are happy and save the result as the new baseline.
390
391```
392$ cp snapshot2 snapshot
393```
394
395The corresponding boilerplate can now be removed with `cue trim`.
396
397```
398$ find . | grep kube.cue | xargs wc | tail -1
399 1792 3616 34815 total
400$ cue trim ./...
401$ find . | grep kube.cue | xargs wc | tail -1
402 1223 2374 22903 total
403```
404
405`cue trim` removes configuration from files that is already generated
406by templates or comprehensions.
407In doing so it removed over 500 lines of configuration, or over 30%!
408
409The following is proof that nothing changed semantically:
410
411```
412$ cue eval ./... > snapshot2
413$ diff snapshot snapshot2 | wc
414 0 0 0
415```
416
417We can do better, though.
418A first thing to note is that DaemonSets and StatefulSets share a similar
419structure to Deployments.
420We generalize the top-level template as follows:
421
422```
423$ cat <<EOF >> kube.cue
424
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200425daemonSet: [ID=_]: _spec & {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100426 apiVersion: "extensions/v1beta1"
427 kind: "DaemonSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200428 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100429}
430
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200431statefulSet: [ID=_]: _spec & {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100432 apiVersion: "apps/v1beta1"
433 kind: "StatefulSet"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200434 Name :: ID
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100435}
436
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200437deployment: [ID=_]: _spec & {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100438 apiVersion: "extensions/v1beta1"
439 kind: "Deployment"
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200440 Name :: ID
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100441 spec replicas: *1 | int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100442}
443
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200444configMap: [ID=_]: {
445 metadata: name: ID
446 metadata: labels: component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100447}
448
449_spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200450 Name :: string
451
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200452 metadata: name: Name
453 metadata: labels: component: Component
454 spec: template: {
455 metadata: labels: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200456 app: Name
457 component: Component
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100458 domain: "prod"
459 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200460 spec: containers: [{name: Name}]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100461 }
462}
463EOF
464$ cue fmt
465```
466
467The common configuration has been factored out into `_spec`.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200468We introduced `Name` to aid both specifying and referring
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100469to the name of an object.
470For completeness, we added `configMap` as a top-level entry.
471
472Note that we have not yet removed the old definition of deployment.
473This is fine.
474As it is equivalent to the new one, unifying them will have no effect.
Emil Hessman66ec9592019-07-14 17:58:27 +0200475We leave its removal as an exercise to the reader.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100476
477Next we observe that all deployments, stateful sets and daemon sets have
478an accompanying service which shares many of the same fields.
479We add:
480
481```
482$ cat <<EOF >> kube.cue
483
484// Define the _export option and set the default to true
485// for all ports defined in all containers.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200486_spec: spec: template: spec: containers: [...{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100487 ports: [...{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100488 _export: *true | false // include the port in the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100489 }]
490}]
491
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200492for x in [deployment, daemonSet, statefulSet] for k, v in x {
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200493 service: "\(k)": {
494 spec: selector: v.spec.template.metadata.labels
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100495
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200496 spec: ports: [ {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200497 Port = p.containerPort // Port is an alias
498 port: *Port | int
499 targetPort: *Port | int
500 } for c in v.spec.template.spec.containers
501 for p in c.ports
502 if p._export ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200503 }
504}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100505EOF
506$ cue fmt
507```
508
509This example introduces a few new concepts.
Emil Hessman66ec9592019-07-14 17:58:27 +0200510Open-ended lists are indicated with an ellipsis (`...`).
511The value following an ellipsis is unified with any subsequent elements and
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100512defines the "type", or template, for additional list elements.
513
514The `Port` declaration is an alias.
515Aliases are only visible in their lexical scope and are not part of the model.
516They can be used to make shadowed fields visible within nested scopes or,
517in this case, to reduce boilerplate without introducing new fields.
518
519Finally, this example introduces list and field comprehensions.
Emil Hessman66ec9592019-07-14 17:58:27 +0200520List comprehensions are analogous to list comprehensions found in other
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100521languages.
522Field comprehensions allow inserting fields in structs.
523In this case, the field comprehension adds a namesake service for any
524deployment, daemonSet, and statefulSet.
525Field comprehensions can also be used to add a field conditionally.
526
527
528Specifying the `targetPort` is not necessary, but since many files define it,
Emil Hessman66ec9592019-07-14 17:58:27 +0200529defining it here will allow those definitions to be removed
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100530using `cue trim`.
531We add an option `_export` for ports defined in containers to specify whether
532to include them in the service and explicitly set this to false
533for the respective ports in `infra/events`, `infra/tasks`, and `infra/watcher`.
534
535For the purpose of this tutorial, here are some quick patches:
536```
537$ cat <<EOF >> infra/events/kube.cue
538
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200539deployment: events: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100540EOF
541
542$ cat <<EOF >> infra/tasks/kube.cue
543
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200544deployment: tasks: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100545EOF
546
547$ cat <<EOF >> infra/watcher/kube.cue
548
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200549deployment: watcher: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100550EOF
551```
552In practice it would be more proper form to add this field in the original
553port declaration.
554
555We verify that all changes are acceptable and store another snapshot.
556Then we run trim to further reduce our configuration:
557
558```
559$ cue trim ./...
560$ find . | grep kube.cue | xargs wc | tail -1
561 1129 2270 22073 total
562```
Emil Hessman66ec9592019-07-14 17:58:27 +0200563This is after removing the rewritten and now redundant deployment definition.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100564
565We shaved off almost another 100 lines, even after adding the template.
566You can verify that the service definitions are now gone in most of the files.
567What remains is either some additional configuration, or inconsistencies that
568should probably be cleaned up.
569
Emil Hessman66ec9592019-07-14 17:58:27 +0200570But we have another trick up our sleeve.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100571With the `-s` or `--simplify` option we can tell `trim` or `fmt` to collapse
572structs with a single element onto a single line. For instance:
573
574```
575$ head frontend/breaddispatcher/kube.cue
576package kube
577
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200578deployment: breaddispatcher: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100579 spec: {
580 template: {
581 metadata: {
582 annotations: {
583 "prometheus.io.scrape": "true"
584 "prometheus.io.port": "7080"
585 }
586$ cue trim ./... -s
587$ head -7 frontend/breaddispatcher/kube.cue
588package kube
589
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200590deployment: breaddispatcher: spec: template: {
591 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100592 "prometheus.io.scrape": "true"
593 "prometheus.io.port": "7080"
594 }
595$ find . | grep kube.cue | xargs wc | tail -1
596 975 2116 20264 total
597```
598
599Another 150 lines lost!
600Collapsing lines like this can improve the readability of a configuration
601by removing considerable amounts of punctuation.
602
603
604### Repeat for several subdirectories
605
606In the previous section we defined templates for services and deployments
607in the root of our directory structure to capture the common traits of all
608services and deployments.
609In addition, we defined a directory-specific label.
610In this section we will look into generalizing the objects per directory.
611
612
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +0100613#### Directory `frontend`
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100614
615We observe that all deployments in subdirectories of `frontend`
616have a single container with one port,
617which is usually `7080`, but sometimes `8080`.
618Also, most have two prometheus-related annotations, while some have one.
619We leave the inconsistencies in ports, but add both annotations
620unconditionally.
621
622```
623$ cat <<EOF >> frontend/kube.cue
624
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200625deployment: [string]: spec: template: {
626 metadata: annotations: {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100627 "prometheus.io.scrape": "true"
628 "prometheus.io.port": "\(spec.containers[0].ports[0].containerPort)"
629 }
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200630 spec: containers: [{
Marcel van Lohuizene5d8d092019-01-30 15:58:07 +0100631 ports: [{containerPort: *7080 | int}] // 7080 is the default
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100632 }]
633}
634EOF
635$ cue fmt ./frontend
636
637# check differences
638$ cue eval ./... > snapshot2
639$ diff snapshot snapshot2
640368a369
641> prometheus.io.port: "7080"
642577a579
643> prometheus.io.port: "8080"
644$ cp snapshot2 snapshot
645```
646
647Two lines with annotations added, improving consistency.
648
649```
650$ cue trim -s ./frontend/...
651$ find . | grep kube.cue | xargs wc | tail -1
652 931 2052 19624 total
653```
654
Emil Hessman66ec9592019-07-14 17:58:27 +0200655Another 40 lines removed.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100656We may have gotten used to larger reductions, but at this point there is just
657not much left to remove: in some of the frontend files there are only 4 lines
Emil Hessman66ec9592019-07-14 17:58:27 +0200658of configuration left.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100659
660
661#### Directory `kitchen`
662
663In this directory we observe that all deployments have without exception
664one container with port `8080`, all have the same liveness probe,
665a single line of prometheus annotation, and most have
666two or three disks with similar patterns.
667
668Let's add everything but the disks for now:
669
670```
671$ cat <<EOF >> kitchen/kube.cue
672
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200673deployment: [string]: spec: template: {
674 metadata: annotations: "prometheus.io.scrape": "true"
675 spec: containers: [{
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100676 ports: [{
677 containerPort: 8080
678 }]
679 livenessProbe: {
680 httpGet: {
681 path: "/debug/health"
682 port: 8080
683 }
684 initialDelaySeconds: 40
685 periodSeconds: 3
686 }
687 }]
688}
689EOF
690$ cue fmt ./kitchen
691```
692
693A diff reveals that one prometheus annotation was added to a service.
694We assume this to be an accidental omission and accept the differences
695
696Disks need to be defined in both the template spec section as well as in
697the container where they are used.
698We prefer to keep these two definitions together.
699We take the volumes definition from `expiditer` (the first config in that
700directory with two disks), and generalize it:
701
702```
703$ cat <<EOF >> kitchen/kube.cue
704
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200705deployment: [ID=_]: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200706 hasDisks :: *true | bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100707
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200708 // field comprehension using just "if"
709 if hasDisks {
710 volumes: [{
711 name: *"\(ID)-disk" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200712 gcePersistentDisk: pdName: *"\(ID)-disk" | string
713 gcePersistentDisk: fsType: "ext4"
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100714 }, {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200715 name: *"secret-\(ID)" | string
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200716 secret: secretName: *"\(ID)-secrets" | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100717 }, ...]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +0200718
719 containers: [{
720 volumeMounts: [{
721 name: *"\(ID)-disk" | string
722 mountPath: *"/logs" | string
723 }, {
724 mountPath: *"/etc/certs" | string
725 name: *"secret-\(ID)" | string
726 readOnly: true
727 }, ...]
728 }]
729 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100730}
731EOF
732
733$ cat <<EOF >> kitchen/souschef/kube.cue
734
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200735deployment: souschef: spec: template: spec: {
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200736 hasDisks :: false
737}
738
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100739EOF
740$ cue fmt ./kitchen/...
741```
742
743This template definition is not ideal: the definitions are positional, so if
744configurations were to define the disks in a different order, there would be
745no reuse or even conflicts.
746Also note that in order to deal with this restriction, almost all field values
Emil Hessman66ec9592019-07-14 17:58:27 +0200747are just default values and can be overridden by instances.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100748A better way would be define a map of volumes,
749similarly to how we organized the top-level Kubernetes objects,
750and then generate these two sections from this map.
751This requires some design, though, and does not belong in a
752"quick-and-dirty" tutorial.
753Later in this document we introduce a manually optimized configuration.
754
755We add the two disk by default and define a `_hasDisks` option to opt out.
756The `souschef` configuration is the only one that defines no disks.
757
758```
759$ cue trim -s ./kitchen/...
760
761# check differences
762$ cue eval ./... > snapshot2
763$ diff snapshot snapshot2
764...
765$ cp snapshot2 snapshot
766$ find . | grep kube.cue | xargs wc | tail -1
767 807 1862 17190 total
768```
769
770The diff shows that we added the `_hadDisks` option, but otherwise reveals no
771differences.
772We also reduced the configuration by a sizeable amount once more.
773
774However, on closer inspection of the remaining files we see a lot of remaining
775fields in the disk specifications as a result of inconsistent naming.
Emil Hessman66ec9592019-07-14 17:58:27 +0200776Reducing configurations like we did in this exercise exposes inconsistencies.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100777The inconsistencies can be removed by simply deleting the overrides in the
778specific configuration.
779Leaving them as is gives a clear signal that a configuration is inconsistent.
780
781
782### Conclusion of Quick 'n Dirty tutorial
783
784There is still some gain to be made with the other directories.
Emil Hessman66ec9592019-07-14 17:58:27 +0200785At nearly a 1000-line, or 55%, reduction, we leave the rest as an exercise to
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100786the reader.
787
788We have shown how CUE can be used to reduce boilerplate, enforce consistencies,
789and detect inconsistencies.
790Being able to deal with consistencies and inconsistencies is a consequence of
791the constraint-based model and harder to do with inheritance-based languages.
792
793We have indirectly also shown how CUE is well-suited for machine manipulation.
794This is a factor of syntax and the order independence that follows from its
795semantics.
796The `trim` command is one of many possible automated refactor tools made
797possible by this property.
798Also this would be harder to do with inheritance-based configuration languages.
799
800
801## Define commands
802
803The `cue export` command can be used to convert the created configuration back
804to JSON.
805In our case, this requires a top-level "emit value"
806to convert our mapped Kubernetes objects back to a list.
807Typically, this output is piped to tools like `kubectl` or `etcdctl`.
808
809In practice this means typing the same commands ad nauseam.
810The next step is often to write wrapper tools.
811But as there is often no one-size-fits-all solution, this lead to the
812proliferation of marginally useful tools.
813The `cue` tool provides an alternative by allowing the declaration of
814frequently used commands in CUE itself.
815Advantages:
816
817- added domain knowledge that CUE may use for improved analysis,
818- only one language to learn,
819- easy discovery of commands,
820- no further configuration required,
821- enforce uniform CLI standards across commands,
822- standardized commands across an organization.
823
824Commands are defined in files ending with `_tool.cue` in the same package as
825where the configuration files are defined on which the commands should operate.
826Top-level values in the configuration are visible by the tool files
827as long as they are not shadowed by top-level fields in the tool files.
828Top-level fields in the tool files are not visible in the configuration files
829and are not part of any model.
830
831The tool definitions also have access to additional builtin packages.
832A CUE configuration is fully hermetic, disallowing any outside influence.
833This property enables automated analysis and manipulation
834such as the `trim` command.
835The tool definitions, however, have access to such things as command line flags
836and environment variables, random generators, file listings, and so on.
837
838We define the following tools for our example:
839
840- ls: list the Kubernetes objects defined in our configuration
841- dump: dump all selected objects as a YAML stream
842- create: send all selected objects to `kubectl` for creation
843
844### Preparations
845
846To work with Kubernetes we need to convert our map of Kubernetes objects
847back to a simple list.
848We create the tool file to do just that.
849
850```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200851$ cat <<EOF > kube_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100852package kube
853
854objects: [ x for v in objectSets for x in v ]
855
856objectSets: [
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200857 service,
858 deployment,
859 statefulSet,
860 daemonSet,
861 configMap,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100862]
863EOF
864```
865
866### Listing objects
867
868Commands are defined in the `command` section at the top-level of a tool file.
869A `cue` command defines command line flags, environment variables, as well as
870a set of tasks.
871Examples tasks are load or write a file, dump something to the console,
872download a web page, or execute a command.
873
874We start by defining the `ls` command which dumps all our objects
875
876```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200877$ cat <<EOF > ls_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100878package kube
879
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200880import (
881 "text/tabwriter"
882 "tool/cli"
883 "tool/file"
884)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100885
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200886command: ls: {
887 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200888 text: tabwriter.Write([
889 "\(x.kind) \t\(x.metadata.labels.component) \t\(x.metadata.name)"
890 for x in objects
891 ])
892 }
893
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200894 task: write: file.Create & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200895 filename: "foo.txt"
896 contents: task.print.text
897 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100898}
899EOF
900```
901<!-- TODO: use "let" once implemented-->
902
903NOTE: THE API OF THE TASK DEFINITIONS WILL CHANGE.
904Although we may keep supporting this form if needed.
905
906The command is now available in the `cue` tool:
907
908```
909$ cue cmd ls ./frontend/maitred
910Service frontend maitred
911Deployment frontend maitred
912```
913
914As long as the name does not conflict with an existing command it can be
915used as a top-level command as well:
916```
917$ cue ls ./frontend/maitred
918...
919```
920
921If more than one instance is selected the `cue` tool may either operate
922on them one by one or merge them.
923The default is to merge them.
Marcel van Lohuizen09491352018-12-20 20:20:54 +0100924Different instances of a package are typically not compatible:
925different subdirectories may have different specializations.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100926A merge pre-expands templates of each instance and then merges their root
927values.
Marcel van Lohuizen98187612019-09-03 12:48:25 +0200928The result may contain conflicts, such as our top-level `Component` field,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100929but our per-type maps of Kubernetes objects should be free of conflict
930(if there is, we have a problem with Kubernetes down the line).
Emil Hessman66ec9592019-07-14 17:58:27 +0200931A merge thus gives us a unified view of all objects.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100932
933```
934$ cue ls ./...
935Service frontend bartender
936Service frontend breaddispatcher
937Service frontend host
938Service frontend maitred
939Service frontend valeter
940Service frontend waiter
941Service frontend waterdispatcher
942Service infra download
943Service infra etcd
944Service infra events
945
946...
947
948Deployment proxy goget
949Deployment proxy nginx
950StatefulSet infra etcd
951DaemonSet mon node-exporter
952```
953
954### Dumping a YAML Stream
955
956The following adds a command to dump the selected objects as a YAML stream.
957
958<!--
959TODO: add command line flags to filter object types.
960-->
961```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200962$ cat <<EOF > dump_tool.cue
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100963package kube
964
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200965import (
966 "encoding/yaml"
967 "tool/cli"
968)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100969
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +0200970command: dump: {
971 task: print: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200972 text: yaml.MarshalStream(objects)
973 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +0100974}
975EOF
976```
977
978<!--
979TODO: with new API as well as conversions implemented
980command dump task print: cli.Print(text: yaml.MarshalStream(objects))
981
982or without conversions:
983command dump task print: cli.Print & {text: yaml.MarshalStream(objects)}
984-->
985
986The `MarshalStream` command converts the list of objects to a '`---`'-separated
987stream of YAML values.
988
989
990### Creating Objects
991
992The `create` command sends a list of objects to `kubectl create`.
993
994```
995$ cat <<EOF > create_tool.cue
996package kube
997
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +0200998import (
999 "encoding/yaml"
1000 "tool/exec"
1001 "tool/cli"
1002)
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001003
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001004command: create: {
1005 task: kube: exec.Run & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001006 cmd: "kubectl create --dry-run -f -"
1007 stdin: yaml.MarshalStream(objects)
1008 stdout: string
1009 }
1010
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001011 task: display: cli.Print & {
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001012 text: task.kube.stdout
1013 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001014}
1015EOF
1016```
1017
1018This command has two tasks, named `kube` and `display`.
1019The `display` task depends on the output of the `kube` task.
1020The `cue` tool does a static analysis of the dependencies and runs all
Emil Hessman66ec9592019-07-14 17:58:27 +02001021tasks which dependencies are satisfied in parallel while blocking tasks
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001022for which an input is missing.
1023
1024```
1025$ cue create ./frontend/...
1026service "bartender" created (dry run)
1027service "breaddispatcher" created (dry run)
1028service "host" created (dry run)
1029service "maitred" created (dry run)
1030service "valeter" created (dry run)
1031service "waiter" created (dry run)
1032service "waterdispatcher" created (dry run)
1033deployment.extensions "bartender" created (dry run)
1034deployment.extensions "breaddispatcher" created (dry run)
1035deployment.extensions "host" created (dry run)
1036deployment.extensions "maitred" created (dry run)
1037deployment.extensions "valeter" created (dry run)
1038deployment.extensions "waiter" created (dry run)
1039deployment.extensions "waterdispatcher" created (dry run)
1040```
1041
1042A production real-life version of this could should omit the `--dry-run` flag
1043of course.
1044
Joel Longtineb6544662019-06-11 16:59:12 +00001045### Extract CUE templates directly from Kubernetes Go source
1046
1047```
1048$ cue get go k8s.io/api/core/v1
1049$ cue get go k8s.io/api/extensions/v1beta1
1050$ cue get go k8s.io/api/apps/v1beta1
1051
1052```
1053
1054Now that we have the Kubernetes definitions in `pkg`, we can import and use them:
1055
1056```
1057$ cat <<EOF > k8s_defs.cue
1058package kube
1059
1060import (
1061 "k8s.io/api/core/v1"
1062 extensions_v1beta1 "k8s.io/api/extensions/v1beta1"
1063 apps_v1beta1 "k8s.io/api/apps/v1beta1"
1064)
1065
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001066service: [string]: v1.Service
1067deployment: [string]: extensions_v1beta1.Deployment
1068daemonSet: [string]: extensions_v1beta1.DaemonSet
1069statefulSet: [string]: apps_v1beta1.StatefulSet
Joel Longtineb6544662019-06-11 16:59:12 +00001070EOF
1071```
1072
1073And, finally, we'll format again:
1074
1075```
1076cue fmt
1077```
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001078
1079## Manually tailored configuration
1080
1081In Section "Quick 'n Dirty" we showed how to quickly get going with CUE.
1082With a bit more deliberation, one can reduce configurations even further.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001083Also, we would like to define a configuration that is more generic and less tied
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001084to Kubernetes.
1085
1086We will rely heavily on CUEs order independence, which makes it easy to
1087combine two configurations of the same object in a well-defined way.
1088This makes it easy, for instance, to put frequently used fields in one file
1089and more esoteric one in another and then combine them without fear that one
1090will override the other.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001091We will take this approach in this section.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001092
1093The end result of this tutorial is in the `manual` directory.
1094In the next sections we will show how to get there.
1095
1096
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001097### Outline
1098
Emil Hessman66ec9592019-07-14 17:58:27 +02001099The basic premise of our configuration is to maintain two configurations,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001100a simple and abstract one, and one compatible with Kubernetes.
1101The Kubernetes version is automatically generated from the simple configuration.
1102Each simplified object has a `kubernetes` section that get gets merged into
1103the Kubernetes object upon conversion.
1104
1105We define one top-level file with our generic definitions.
1106
1107```
1108// file cloud.cue
1109package cloud
1110
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001111service: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001112 name: *Name | string // the name of the service
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001113
1114 ...
1115
1116 // Kubernetes-specific options that get mixed in when converting
1117 // to Kubernetes.
1118 kubernetes: {
1119 }
1120}
1121
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001122deployment: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001123 name: *Name | string
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001124 ...
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001125}
1126```
1127
1128A Kubernetes-specific file then contains the definitions to
1129convert the generic objects to Kubernetes.
1130
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001131Overall, the code modeling our services and the code generating the kubernetes
1132code is separated, while still allowing to inject Kubernetes-specific
1133data into our general model.
1134At the same time, we can add additional information to our model without
Emil Hessman66ec9592019-07-14 17:58:27 +02001135it ending up in the Kubernetes definitions causing it to barf.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001136
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001137
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001138### Deployment Definition
1139
1140For our design we assume that all Kubernetes Pod derivatives only define one
1141container.
1142This is clearly not the case in general, but often it does and it is good
1143practice.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001144Conveniently, it simplifies our model as well.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001145
1146We base the model loosely on the master templates we derived in
1147Section "Quick 'n Dirty".
1148The first step we took is to eliminate `statefulSet` and `daemonSet` and
1149rather just have a `deployment` allowing different kinds.
1150
1151```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001152deployment: [Name=_]: _base & {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001153 name: *Name | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001154 ...
1155```
1156
1157The kind only needs to be specified if the deployment is a stateful set or
1158daemonset.
1159This also eliminates the need for `_spec`.
1160
1161The next step is to pull common fields, such as `image` to the top level.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001162
Emil Hessman66ec9592019-07-14 17:58:27 +02001163Arguments can be specified as a map.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001164```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001165 arg: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001166 args: [ "-\(k)=\(v)" for k, v in arg ] | [...string]
1167```
1168
1169If order matters, users could explicitly specify the list as well.
1170
1171For ports we define two simple maps from name to port number:
1172
1173```
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001174 // expose port defines named ports that is exposed in the service
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001175 expose: port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001176
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001177 // port defines a named port that is not exposed in the service.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001178 port: [string]: int
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001179```
1180Both maps get defined in the container definition, but only `port` gets
1181included in the service definition.
1182This may not be the best model, and does not support all features,
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001183but it shows how one can chose a different representation.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001184
1185A similar story holds for environment variables.
1186In most cases mapping strings to string suffices.
1187The testdata uses other options though.
1188We define a simple `env` map and an `envSpec` for more elaborate cases:
1189
1190```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001191 env: [string]: string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001192
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001193 envSpec: [string]: {}
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001194 envSpec: {"\(k)" value: v for k, v in env}
1195```
1196The simple map automatically gets mapped into the more elaborate map
1197which then presents the full picture.
1198
1199Finally, our assumption that there is one container per deployment allows us
1200to create a single definition for volumes, combining the information for
1201volume spec and volume mount.
1202
1203```
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001204 volume: [Name=_]: {
Roger Peppe1ce0c512019-09-24 15:29:39 +01001205 name: *Name | string
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001206 mountPath: string
1207 subPath: null | string
Roger Peppe1ce0c512019-09-24 15:29:39 +01001208 readOnly: bool
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001209 kubernetes: {}
1210 }
1211```
1212
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001213All other fields that we way want to define can go into a generic kubernetes
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001214struct that gets merged in with all other generated kubernetes data.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001215This even allows us to augment generated data, such as adding additional
1216fields to the container.
1217
1218
1219### Service Definition
1220
1221The service definition is straightforward.
1222As we eliminated stateful and daemon sets, the field comprehension to
1223automatically derive a service is now a bit simpler:
1224
1225```
1226// define services implied by deployments
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001227service: "\(k)": {
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001228
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001229 // Copy over all ports exposed from containers.
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001230 for Name, Port in spec.expose.port {
1231 port: "\(Name)": {
1232 port: *Port | int
1233 targetPort: *Port | int
1234 }
1235 }
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001236
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001237 // Copy over the labels
1238 label: spec.label
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001239
1240} for k, spec in deployment
1241```
1242
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001243The complete top-level model definitions can be found at
1244[doc/tutorial/kubernetes/manual/services/cloud.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/cloud.cue).
1245
1246The tailorings for this specific project (the labels) are defined
1247[here](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/kube.cue).
1248
1249
1250### Converting to Kubernetes
1251
1252Converting services is fairly straightforward.
1253
1254```
1255kubernetes services: {
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001256 for k, x in service {
1257 "\(k)": x.kubernetes & {
1258 apiVersion: "v1"
1259 kind: "Service"
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001260
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001261 metadata: name: x.name
1262 metadata: labels: x.label
1263 spec: selector: x.label
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001264
Marcel van Lohuizen23623fa2019-10-23 19:28:59 +02001265 spec: ports: [ p for p in x.port ]
Marcel van Lohuizen9af9a902019-09-07 20:30:10 +02001266 }
1267 }
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001268}
1269```
1270
1271We add the Kubernetes boilerplate, map the top-level fields and mix in
1272the raw `kubernetes` fields for each service.
1273
1274Mapping deployments is a bit more involved, though analogous.
1275The complete definitions for Kubernetes conversions can be found at
1276[doc/tutorial/kubernetes/manual/services/k8s.cue](https://cue.googlesource.com/cue/+/master/doc/tutorial/kubernetes/manual/services/k8s.cue).
1277
1278Converting the top-level definitions to concrete Kubernetes code is the hardest
1279part of this exercise.
1280That said, most CUE users will never have to resort to this level of CUE
1281to write configurations.
1282For instance, none of the files in the subdirectories contain comprehensions,
Emil Hessman66ec9592019-07-14 17:58:27 +02001283not even the template files in these directories (such as `kitchen/kube.cue`).
Marcel van Lohuizen1e0fe9c2018-12-21 00:17:06 +01001284Furthermore, none of the configuration files in any of the
1285leaf directories contain string interpolations.
Marcel van Lohuizen50a01f32018-12-20 21:47:59 +01001286
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001287
1288### Metrics
1289
1290The fully written out manual configuration can be found in the `manual`
1291subdirectory.
1292Running our usual count yields
1293```
Marcel van Lohuizen7dbf2dc2019-06-07 19:37:04 +02001294$ find . | grep kube.cue | xargs wc | tail -1
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001295 542 1190 11520 total
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001296```
1297This does not count our conversion templates.
1298Assuming that the top-level templates are reusable, and if we don't count them
Emil Hessman66ec9592019-07-14 17:58:27 +02001299for both approaches, the manual approach shaves off about another 150 lines.
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001300If we count the templates as well, the two approaches are roughly equal.
1301
1302
1303### Conclusions Manual Configuration
1304
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001305We have shown that we can further compact a configuration by manually
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001306optimizing template files.
Emil Hessman66ec9592019-07-14 17:58:27 +02001307However, we have also shown that the manual optimization only gives
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001308a marginal benefit with respect to the quick-and-dirty semi-automatic reduction.
Emil Hessman66ec9592019-07-14 17:58:27 +02001309The benefits for the manual definition largely lies in the organizational
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001310flexibility one gets.
1311
1312Manually tailoring your configurations allows creating an abstraction layer
1313between logical definitions and Kubernetes-specific definitions.
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001314At the same time, CUE's order independence
Emil Hessman66ec9592019-07-14 17:58:27 +02001315makes it easy to mix in low-level Kubernetes configuration wherever it is
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001316convenient and applicable.
1317
1318Manual tailoring also allows us to add our own definitions without breaking
Marcel van Lohuizen09491352018-12-20 20:20:54 +01001319Kubernetes.
1320This is crucial in defining information relevant to definitions,
Marcel van Lohuizen02173f82018-12-20 13:27:07 +01001321but unrelated to Kubernetes, where they belong.
1322
1323Separating abstract from concrete configuration also allows us to create
1324difference adaptors for the same configuration.
1325
1326
1327<!-- TODO:
1328## Conversion to `docker-compose`
Tarun Gupta Akiralabb2b6512019-06-03 13:24:12 +00001329-->