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