Olaf Bohlen
2023-01-04 c14f64b34920f8c2720cd5b0664baa349916467c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
#+TODO: TODO(t) NEXT(n) WAITING(w) SOMEDAY(s) DELEGATED(g) PROJ(p) PLANNED(l) | DONE(d) FORWARDED(f) CANCELLED(c)
#+startup: beamer
#+LaTeX_CLASS: beamer
#+LaTeX_CLASS_OPTIONS: [a4paper]
#+LaTeX_CLASS_OPTIONS: [captions=tableheading]
#+LATEX_HEADER: \usetheme{Warsaw} \usepackage{courier}
#+LATEX_HEADER: \usepackage{textpos}
#+LATEX_HEADER: \RequirePackage{fancyvrb}
#+LATEX_HEADER: \DefineVerbatimEnvironment{verbatim}{Verbatim}{fontsize=\tiny}
# +LATEX_HEADER: \setbeamercolor{title}{fg=green}
# +LATEX_HEADER: \setbeamercolor{structure}{fg=black}
# +LATEX_HEADER: \setbeamercolor{section in head/foot}{fg=green}
# +LATEX_HEADER: \setbeamercolor{subsection in head/foot}{fg=green}
# +LATEX_HEADER: \setbeamercolor{item}{fg=green}
#+LATEX_HEADER: \setbeamerfont{frametitle}{family=\ttfamily}
# logo
# +LATEX_HEADER: \addtobeamertemplate{frametitle}{}{ \begin{textblock*}{100mm}(0.85\textwidth,-0.8cm) \includegraphics[height=0.7cm,width=2cm]{niit-logo.png} \end{textblock*}}
#+OPTIONS: toc:nil ^:nil
#+LANGUAGE: en
#+TITLE: Diving into OpenShift
 
 
 
* Helpful tools on the way
- *jq(1)* is a JSON query tool for the command line, a must have
- *yq* is a python script that does YAML queries
- yes, there's also *xq* for XML but that's for the JAVA folks ;)
- *jo* to produce JSON output on the shell
 
How to get?
- jq: it's very likely shipped as a package with your distribution
- yq: install via =pip install --user yq=
- xq: install via =pip install --user xq=
- jo: get the sources from https://github.com/jpmens/jo
 
* jq
The most basic usage is filter down a JSON for a specific attributes value.
#+begin_src js
{
    "kind": "ServiceAccount",
    "apiVersion": "v1",
    "metadata": {
        "name": "foobar",
        "creationTimestamp": null
    }
}
#+end_src
 
To get the name of the ServiceAccount you would run
 
=jq .metadata.name=
 
If you have a list, and want to see all items, use [], for example:
 
=jq .items[]=
 
If you want to get a specific list entry by index, run
 
=jq .items[5]= to get the sixth entry
 
* yq
*yq* works very similar in the basic usage as *jq*, however per default it outputs - confusing enough - JSON (because it's a wrapper for jq).
If you use the =-y= option, it will output YAML. 
 
I find this very handy for reading data from =ConfigMaps=:
 
#+begin_example
$ oc get cm foobar -o yaml | yq -y .data
key: value
text: hello world
#+end_example
 
* jo
*jo* is a very special tool that can be very interesting if you have shell commands producing output that
should be converted into JSON. More examples can be found here: https://jpmens.net/2016/03/05/a-shell-command-to-create-json-jo/
 
#+begin_example
$ ps -fo uid,pid,ppid,stime,tty,time,args | nawk '$NF~/ksh/ { 
> printf("uid=%s pid=%s ppid=%s stime=%s tty=%s cputime=%s comm=%s\n"
> , $1, $2, $3, $4, $5, $6, $7)}' | while read line; do jo ${line}
> done | jo -pa
[
   {
      "uid": 4100,
      "pid": 4880,
      "ppid": 4001,
      "stime": "Jan_01",
      "tty": "pts/2",
      "cputime": "00:00",
      "comm": "-ksh"
   },
   {
      "uid": 4100,
      "pid": 20599,
      "ppid": 4880,
      "stime": "23:46:19",
      "tty": "pts/2",
      "cputime": "00:00",
      "comm": "-ksh"
   }
]
#+end_example
 
* oc get -o jsonpath
If you use *oc* or *kubectl* to retreive JSON data, you can also filter down with JSONPATH
instead of using *jq*. The basic syntax is simple, imagine you want the hostname of a route, 
which is in the =host= key under the =spec=:
 
#+begin_example
$ oc get -o jsonpath='{"http://"}{.spec.host}{"\n"}' route hi
http://hi-olbohlen-demo.apps-crc.testing
#+end_example
 
And because we are clever, we will create a shell alias from it:
#+begin_example
$ alias gr='oc get -o jsonpath="{\"http://\"}{.spec.host}{\"\n\"}" route '
[crc@lol cookie-operator]$ gr hi
http://hi-olbohlen-demo.apps-crc.testing
$ curl $(gr hi)
<html>
 <head>
  <title>hello php</title>
[...]
#+end_example
 
* Declarative versus Imperative
 
It's important to understand that Kubernetes works declarative.
What does that mean?
 
Traditionally we work imperative with computers, we tell them what and how to execute.
The problem here is, that the result is undefined, there are so many side-cases to catch.
 
If we work declarative, we declare our will to the computer and rely on algorithms that
shall fulfill our will. Of course this still can fail, but ideally the code results in "failed" or "completed".
 
* Controller Scheme
#+begin_src ditaa :file ocp-workshop1.png :cmdline -E -s 0.8
 
+---------------------------+       +-----------------------+       +-------------------------+
|{s}                        |       |    Kube API Server    |       | Kube Controller Manager |
| etcd                      |       |                       |       |                         |
|                           | <---> |etcd connector         | <---> | watches API for updates |
|/kubernetes.io/pods/ns/pod |       |                       |       | if update: run code     |
|                           |       |       REST Connector  |       |                         |
+---------------------------+       +-----------------------+       +-------------------------+
                                               ^
                                               | HTTP(S)/REST
                                               |  (JSON)
                                               v
                                    +-----------------------+
                                    |       REST Client     |
                                    |                       |
                                    | oc/kubectl            |
                                    |  (converts JSON to    |
                                    |   output format)      |
                                    |                       |
                                    +-----------------------+
#+end_src
 
* Syntactic sugar
OpenShift's *oc* client, but also *kubectl* have various sub-commands to make the start with Kubernetes easier.
Especially in the beginning the idea to declare everything in JSON or YAML doesn't sound convincing.
 
But if you look under the hood, commands like *oc new-app* or *kubectl create* will send declarations.
 
You can validate that by doing a dry-run:
 
#+begin_example
$ oc create --dry-run=client -o yaml sa foobar
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: null
  name: foobar
#+end_example
 
* Dry-Runs
There are two ways of dry-runs: =client= and =server=
 
If we do a client dry-run, nothing gets sent to the kubernetes API, so we basically only test
if we can convert the local YAML into JSON. Illegal values, missing required keys or immutable values cannot be detected.
 
if you however use a =server= dry-run, your declaration gets sent to the API, but it will not get persisted into the ETCD.
 
#+begin_example
$ oc create --dry-run=server -o yaml sa foobar
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2023-01-02T20:33:23Z"
  name: foobar
  namespace: kitchen
  uid: 3bfc1a4c-9a87-4bfa-9dd7-c0941a98ec1f
$ oc get sa foobar
Error from server (NotFound): serviceaccounts "foobar" not found
#+end_example
 
As you can see, you get an =uid= and a =creationTimestamp= back. However, it will not actually be created.
 
* Client Side debugging
Turn on the debugging in *oc* / *kubectl* with =--loglevel=x=, the higher the value, the more verbose. 
If you want to see a lot of details, =--loglevel=9= is very helpful.
 
For example if you want to validate how server side dry-runs work, run:
#+begin_example
$ oc --loglevel=9 create --dry-run=server -o yaml sa foobar
I0102 21:39:10.590303 1993958 loader.go:372] Config loaded from file:  /home/crc/.kube/config
[...]
I0102 21:39:10.818643 1993958 request.go:1073] Request Body: {"kind":"ServiceAccount",
"apiVersion":"v1","metadata":{"name":"foobar","creationTimestamp":null}}
I0102 21:39:10.818703 1993958 round_trippers.go:466] curl -v -XPOST  
-H "Content-Type: application/json" -H "Accept: application/json, */*" 
-H "User-Agent: oc/4.11.0 (linux/amd64) kubernetes/1928ac4" 
-H "Authorization: Bearer <masked>" 'https://api.crc.testing:6443/api/v1/namespaces/kitchen/
serviceaccounts?dryRun=All&fieldManager=kubectl-create&fieldValidation=Ignore'
I0102 21:39:10.822375 1993958 round_trippers.go:553] POST https://api.crc.testing:6443/api/
v1/namespaces/kitchen/serviceaccounts?dryRun=All&fieldManager=kubectl-create&
fieldValidation=Ignore 201 Created in 3 milliseconds
[...]
#+end_example
 
Very handy is that with =--loglevel=9=, *oc* will also print a *curl* command line that could be used to
simulate that call.
 
* Use curl to modify K8s resources
 
Using the log output from the previous slide, we can now try to create a =ServiceAccount= via *curl*.
We need of course the JSON declaration...thankfull we can generate that with a client dry-run:
 
#+begin_example
$ oc create --dry-run=client -o json sa foobar >/tmp/sa-foobar.json
$ curl -k -s -XPOST -d "@/tmp/sa-foobar.json" -H "Content-Type: application/json" \
> -H "Accept: application/json, */*" -H "User-Agent: oc/4.11.0 (linux/amd64) kubernetes/1928ac4" \ 
> -H "Authorization: Bearer $(oc whoami -t)" \
> 'https://api.crc.testing:6443/api/v1/namespaces/api-access/serviceaccounts?fieldManager=\
> kubectl-create&fieldValidation=Ignore'
$ oc get sa foobar
NAME     SECRETS   AGE
foobar   1         11s
#+end_example
 
And how to delete the =ServiceAccount=? Simple:
#+begin_example
$ curl -k -XDELETE -H "Content-Type: application/json" -H "Accept: application/json, */*" \
> -H "User-Agent: oc/4.11.0 (linux/amd64) kubernetes/1928ac4" \
> -H "Authorization: Bearer $(oc whoami -t)" \
> 'https://api.crc.testing:6443/api/v1/namespaces/api-access/serviceaccounts/foobar'
{"kind":"ServiceAccount","apiVersion":"v1","metadata":{"name":"foobar",
"namespace":"api-access","uid":"94a1c0b4-2177-41b7-bbe2-0ff5e3932785","resourceVersion":"1258915",
"creationTimestamp":"2023-01-02T20:57:35Z"},"secrets":[{"name":"foobar-dockercfg-brshk"}],
"imagePullSecrets":[{"name":"foobar-dockercfg-brshk"}]}
$ oc get sa foobar
Error from server (NotFound): serviceaccounts "foobar" not found
#+end_example
 
* Weird effects with the K8s API
 
- the API will ignore unkown keys, so you could put a "=Olaf: was here=" key-value pair anywhere you want, the API will happily ignore it!
- that means, if you *oc edit* a resource and damage the name of a key or dictionary which is not mandatory, it will throw away those definitions (=OAuth/cluster.spec.IdentityProviders=)
- You can create dependent resources on non-existing objects, i.e. you can give non-existing users privileges: ETCD does not support referential integrity, so no forein key constraints
- your declaration will be processed after your client terminated the REST connection, so always check the results!
 
* Short Refresh on RBAC
Kubernetes supports Role Based Access Controls (RBAC)
 
We have five different resources here:
- =Users=
- =Groups=
- =ServiceAccounts=
- =Roles= / =ClusterRoles=
- =RoleBindings= / =ClusterRoleBindings=
 
* Users and Groups
=Users= are a resource that have an access token, to talk to the external API. 
 
=Groups= bundle =Users=, as you would expect.
 
There are also system Users (they contain colons..) which are *hard coded* and there is no way to list them via the API.
The same is true for system Groups!
 
System Users cannot authenticate via the external API.
 
* ServiceAccounts
If =Pods= would run under the User who created the =Pod=, others probably couldn't operate on them.
For that reason we need =ServiceAccounts=, they bundle permissions (=Roles=, =SCCs=), specific secrets (for pulling Images, etc) 
and they have an API Access Token that we can use within the =Pod= to talk to the Kubernetes API.
 
* Roles / ClusterRoles
Where is the difference between =Roles= and =ClusterRoles=?
 
It just says if the resource itself is scoped in a =Namespace= or if it's available for the whole Cluster.
If you want to define a Role for all Namespaces in your Cluster, you create a =ClusterRole=.
If you need a local Role just for a specific Namespace, you create a =Role=.
 
* How does a ClusterRole look like?
A =Role= contains a list of =Rules=, specifying what is allowed if you own that =Role=.
The most simple one is the =cluster-admin= =ClusterRole=.
 
#+begin_example
$ oc get clusterrole cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2022-12-06T10:10:16Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: cluster-admin
  resourceVersion: "75"
  uid: d6fd1146-5bc7-4815-8170-3905ae1e2856
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'
#+end_example
So you can issue any command verb on any resource in any apiGroup. (iddqd)
 
* And what is a RoleBinding?
#+begin_src ditaa :file rbac.png :cmdline -E -s 0.8
+------+                                                  +------+
| User |                                                  | Role |
|      |--------+        +--------------------+     +-----|      |
+------+        +------->| RoleBinding        | <---+     +------+
                         |                    |
+----------------+  +--->|                    |
| ServiceAccount |--+    +--------------------+
|                |             ^
+----------------+             |
                               |
+-------+                      |
| Group |----------------------+
|       |
+-------+
#+end_src
 
#+begin_example
$ oc get rolebinding admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  creationTimestamp: "2023-01-02T20:55:05Z"
  name: admin
  namespace: api-access
  resourceVersion: "1258364"
  uid: a59f1f83-86b1-47eb-97ea-985f13d03102
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: kubeadmin
#+end_example
 
* And a ClusterRoleBinding?
 
Well, a =ClusterRoleBinding= binds on a Cluster-scope, so the permissions take effect cluster-wide.
Note here that there is no =metadata.namespace:= attribute
#+begin_example
$ oc get clusterrolebinding cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2022-12-06T10:10:17Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: cluster-admin
  resourceVersion: "159"
  uid: b971b2ac-e3a8-4db9-ba91-f017ec0a7a5f
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:masters
#+end_example
 
* I need a cookie receipt database!
 
...and because it makes total sense, we are going to abuse the K8s API for it.
 
- thankfully we can extend K8s with Custom Resource Definitions (CRDs)
- but how does it work?
- =CustomResourceDefinitions= are themselves a =Resource=, based on a =ResourceDefinition=
 
#+begin_example
$ oc api-resources | egrep "(NAME|CustomResourceDefinition)"
NAME                       SHORTNAMES  APIVERSION               NAMESPACED  KIND
customresourcedefinitions  crd,crds    apiextensions.k8s.io/v1  false       CustomResourceDefinition
$ oc explain crds
KIND:     CustomResourceDefinition
VERSION:  apiextensions.k8s.io/v1
 
DESCRIPTION:
     CustomResourceDefinition represents a resource that should be exposed on
     the API server. Its name MUST be in the format <.spec.name>.<.spec.group>.
[...]
#+end_example
 
* Preparing the cookiereceipts CRD
 
Let's create the CRD from https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/cookie-crd.yaml
 
#+begin_example
$ oc new-project kitchen
Now using project "kitchen" on server "https://api.crc.testing:6443".
$ oc create -f cookie-crd.yaml
Error from server (Forbidden): error when creating "cookie-crd.yaml": 
customresourcedefinitions.apiextensions.k8s.io is forbidden: User "developer"
cannot create resource "customresourcedefinitions" in API group "apiextensions.k8s.io"
at the cluster scope
 
$ oc login -u kubeadmin
$ oc create -f cookie-crd.yaml
customresourcedefinition.apiextensions.k8s.io/cookiereceipts.de.eenfach.olbohlen created
$ oc login -u developer
#+end_example
 
Now the cluster knows about the CRD and we could store receipts!
 
* Storing some sample receipts
 
We try to store sample cookie receipts...but:
 
#+begin_example
$ oc create -f sample-cookie.yaml
Error from server (Forbidden): error when creating "sample-cookie.yaml": 
cookiereceipts.de.eenfach.olbohlen is forbidden: User "developer" cannot create 
resource "cookiereceipts" in API group "de.eenfach.olbohlen" in the namespace "kitchen"
Error from server (Forbidden): error when creating "sample-cookie.yaml": 
cookiereceipts.de.eenfach.olbohlen is forbidden: User "developer" cannot create 
resource "cookiereceipts" in API group "de.eenfach.olbohlen" in the namespace "kitchen"
#+end_example
 
We need to set up some RBAC resources first:
- a =ClusterRole= that allows viewing receipts
- a =ClusterRole= that allows editing receipts
- and a =ClusterRoleBinding= that allows that for authenticated users
 
* Creating RBAC resources
 
Apply the RBAC definitions from: https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/cookie-rbac.yaml
#+begin_example
$ oc login -u kubeadmin
$ oc create -f cookie-rbac.yaml
clusterrole.rbac.authorization.k8s.io/cookiereceipt-edit created
clusterrole.rbac.authorization.k8s.io/cookiereceipt-view created
clusterrolebinding.rbac.authorization.k8s.io/cookiereceipt-edit created
#+end_example
 
The =ClusterRoleBinding= "cookiereceipt-edit" allows =system:authenticated:oauth=
group members to edit =cookiereceipts=.
=system:authenticated:oauth= contains all users that logged in via the OAuth
service (via an =IdentityProvider=).
 
* Storing some sample receipts (hopefully this time!!)
 
Now we should be able to create the sample receipts:
 
#+begin_example
$ oc login -u developer
$ oc create -f sample-cookie.yaml
cookiereceipt.de.eenfach.olbohlen/vintage-chocolate-chip created
cookiereceipt.de.eenfach.olbohlen/double-dipped-shortbread created
$ oc get cookiereceipt
NAME                       AGE
double-dipped-shortbread   17s
vintage-chocolate-chip     17s
#+end_example
 
There is no functionality here - we just stored the receipts in the etcd via the K8s API.
 
* Can we validate that in the ETCD?
 
Yes, we can use *etcdctl* to look into the db
 
#+begin_example
$ oc login -u kubeadmin
$ oc rsh -n openshift-etcd -c etcdctl \
> etcd-crc-pbwlw-master-0 etcdctl get / --prefix --keys-only | grep -i cookie
/kubernetes.io/apiextensions.k8s.io/customresourcedefinitions/cookiereceipts.de.eenfach.olbohlen
/kubernetes.io/apiserver.openshift.io/apirequestcounts/cookiereceipts.v1.de.eenfach.olbohlen
/kubernetes.io/clusterrolebindings/cookiereceipt-edit
/kubernetes.io/clusterroles/cookiereceipt-edit
/kubernetes.io/clusterroles/cookiereceipt-view
/kubernetes.io/de.eenfach.olbohlen/cookiereceipts/kitchen/double-dipped-shortbread
/kubernetes.io/de.eenfach.olbohlen/cookiereceipts/kitchen/vintage-chocolate-chip
/kubernetes.io/secrets/openshift-machine-config-operator/cookie-secret
#+end_example
 
The =etcd pod= in the =openshift-etcd= project has a sidecar container called =etcdctl=
which contains the *etcdctl* utility and the correct environment.
The data hierarchy starts with =/= and with =--keys-only= we don't get the values.
 
* Let's look into a receipt
 
Check if we can *etcdctl get* on a known receipt:
#+begin_example
$ oc rsh -n openshift-etcd -c etcdctl etcd-crc-pbwlw-master-0 etcdctl \
> get /kubernetes.io/de.eenfach.olbohlen/cookiereceipts/kitchen/vintage-chocolate-chip 
/kubernetes.io/de.eenfach.olbohlen/cookiereceipts/kitchen/vintage-chocolate-chip
{"apiVersion":"de.eenfach.olbohlen/v1","kind":"CookieReceipt","metadata":{
"creationTimestamp":"2023-01-02T15:18:49Z","generation":1,"managedFields":
[{"apiVersion":"de.eenfach.olbohlen/v1","fieldsType":"FieldsV1",
 
[...]
#+end_example
With *etcdctl put*, we could also update the entry and *etcdctl watch* allows us to 
see updates to reosurces.
 
#+begin_example
$ oc rsh -n openshift-etcd -c etcdctl etcd-crc-pbwlw-master-0 etcdctl watch \
> --prefix /kubernetes.io/de.eenfach.olbohlen/cookiereceipts
PUT
/kubernetes.io/de.eenfach.olbohlen/cookiereceipts/kitchen/double-dipped-shortbread
{"apiVersion":"de.eenfach.olbohlen/v1","kind":"CookieReceipt","metadata":{"annotations":
{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":
\"de.eenfach.olbohlen/v1\",\"kind\":\"CookieReceipt\",\"metadata\":{
[...]
#+end_example
(run an *oc* apply/replace/create/delete in another terminal)
 
* Now can we do anything with our receipts?
Of course we can *oc get -o yaml* for example on them and filter:
 
#+begin_example
$ oc get cookiereceipt vintage-chocolate-chip -o yaml | yq -y .spec.ingredients[0]
amount: 150
name: salted butter
remarks: softened
unit: grams
#+end_example
 
This is handy, as we can extract exactly the data which we need at a time.
 
But...it's a lot of manual work...
 
* I'm an operator with my pocket calculator
 
Operators were introduced as "Kubernetes Native Applications" and that actually
means nothing. Operators are in the end just =Pods=.
 
These Pods run one or more containers, but one container should run a =Controller=
that can interprete your =CustomResources=.
 
So let's write a CookieReceipt Operator. In shell-script... :)
 
Of course this Operator is not compatible with the =OperatorLifecycyleManager= (=OLM=),
so we have to install it manually.
 
* What do we need?
 
We need:
- a ContainerImage
- and therefore probably a *Containerfile*
- Controller code
 
Then we are going to build the Operator ContainerImage and push it to a Registry.
 
* Let's review the Containerfile
 
The Containerfile is here:
 
https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/Containerfile
 
the base image is a "kshbase" image, which itself is based upon ubi9 containing also a ksh93
and an oc client.
 
* Have a look at the Controller
 
The controller is written in KornShell 93 (ksh93), which is mostly bash compatible :)
 
The code is here:
 
https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/receipt-processor.ksh
 
* Now let's also have a look at the deployment
 
Note: this deployment does not use an =ImageStream=, so it would work also on native k8s
 
https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/cookie-operator-deployment.yaml
 
This deployment requires a =ServiceAccount= called "cookieprocessor", this =ServiceAccount= provides 
a Token to authenticate against the API (which we use in the controller script).
 
* The ServiceAccount
 
We need a =ServiceAccount=, but that alone will not help. The =ServiceAccount= is NOT member
of =system:authenticated:oauth=, so it can't read =cookiereceipts= based on the =ClusterRoleBinding= we created earlier.
For that reason we also create a =RoleBinding= (namespaced!) that allows reading receipts:
 
https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/cookieprocessor-sa.yaml
 
* Building the stuff together
 
#+begin_example
$ oc create -f cookieprocessor-sa.yaml
serviceaccount/cookieprocessor created
rolebinding.rbac.authorization.k8s.io/cookiereceipt-view created
#+end_example
 
The registry docker.eenfach.de requires login credentials, so we need to set up a secret and link it.
First login to the registry with *podman login*, then pick the resulting auth.json:
 
#+begin_example
$ podman login -u olbohlen docker.eenfach.de
Password: 
Login Succeeded!
$ oc create secret generic docker-eenfach-de \
> --from-file=.dockerconfigjson=${XDG_RUNTIME_DIR}/containers/auth.json \
> --type kubernetes.io/dockerconfigjson
secret/docker-eenfach-de created
$ oc secrets link cookieprocessor docker-eenfach-de --for pull
#+end_example
 
* Deploying the Operator
 
Now that we have everything in place, we will just deploy the Operator Pod:
 
#+begin_example
$ oc create -f cookie-operator-deployment.yaml 
deployment.apps/receipt-processor created
$ oc get pod
NAME                                 READY   STATUS    RESTARTS   AGE
receipt-processor-7f9969697b-qt9lv   1/1     Running   0          17s
$ oc logs -f receipt-processor-7f9969697b-qt9lv
 
 
New receipt found: double-dipped-shortbread
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 180 degrees Celsius
 
Fetching ingredients from receipt:
----------------------------------
Fetching 200grams of salted butter (softened)
[...]
#+end_example
 
The Operator will process both sample receipts.
 
* Test the Operator
 
We should test if the Operator notices new receipts, so let's create a third
receipt from
https://www.eenfach.de/gitblit/blob/~olbohlen!cookie-operator.git/master/oaty-hazelnut-cookies.yaml
 
#+begin_example
$ oc create -f oaty-hazelnut-cookies.yaml
cookiereceipt.de.eenfach.olbohlen/oaty-hazelnut created
#+end_example
 
After a few seconds, we should see in the Operator log:
#+begin_example
New receipt found: oaty-hazelnut
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 180 degrees Celsius
[...]
#+end_example
 
* Test for updated receipts
 
But what if we update a resource?
Keep the *oc logs -f* on the Operator Pod open, and in another terminal let's patch a receipt.
 
#+begin_example
$ oc patch cookiereceipts double-dipped-shortbread --type merge \
> -p '{"spec":{"temperature":172}}'
cookiereceipt.de.eenfach.olbohlen/double-dipped-shortbread patched
#+end_example
 
And again in the log you should see
 
#+begin_example
New receipt found: double-dipped-shortbread
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 172 degrees Celsius
 
Fetching ingredients from receipt:
----------------------------------
[...]
#+end_example
 
* Thank You
 
Thank you for your attention.\\
\\
Do you have any questions?\\
\\
Feel free to ask now or contact me later:
 
[[mailto:olaf.bohlen@niit.com][olaf.bohlen@niit.com]]