Olaf Bohlen
2023-03-21 0cfd14f46c75d2eef2ed792f2cb27c68415a6be1
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
* I need a cookie recipe 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 cookierecipes 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/cookierecipes.de.eenfach.olbohlen created
$ oc login -u developer
#+end_example
 
Now the cluster knows about the CRD and we could store recipes!
 
* Storing some sample recipes
 
We try to store sample cookie recipes...but:
 
#+begin_example
$ oc create -f sample-cookie.yaml
Error from server (Forbidden): error when creating "sample-cookie.yaml": 
cookierecipes.de.eenfach.olbohlen is forbidden: User "developer" cannot create 
resource "cookierecipes" in API group "de.eenfach.olbohlen" in the namespace "kitchen"
Error from server (Forbidden): error when creating "sample-cookie.yaml": 
cookierecipes.de.eenfach.olbohlen is forbidden: User "developer" cannot create 
resource "cookierecipes" 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 recipes
- a =ClusterRole= that allows editing recipes
- 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/cookierecipe-edit created
clusterrole.rbac.authorization.k8s.io/cookierecipe-view created
clusterrolebinding.rbac.authorization.k8s.io/cookierecipe-edit created
#+end_example
 
The =ClusterRoleBinding= "cookierecipe-edit" allows =system:authenticated:oauth=
group members to edit =cookierecipes=.
=system:authenticated:oauth= contains all users that logged in via the OAuth
service (via an =IdentityProvider=).
 
* Storing some sample recipes (hopefully this time!!)
 
Now we should be able to create the sample recipes:
 
#+begin_example
$ oc login -u developer
$ oc create -f sample-cookie.yaml
cookierecipe.de.eenfach.olbohlen/vintage-chocolate-chip created
cookierecipe.de.eenfach.olbohlen/double-dipped-shortbread created
$ oc get cookierecipe
NAME                       AGE
double-dipped-shortbread   17s
vintage-chocolate-chip     17s
#+end_example
 
There is no functionality here - we just stored the recipes in the etcd via the K8s API.
 
* Now can we do anything with our recipes?
Of course we can *oc get -o yaml* for example on them and filter:
 
#+begin_example
$ oc get cookierecipe 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 CookieRecipe 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/recipe-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 =cookierecipes= based on the =ClusterRoleBinding= we created earlier.
For that reason we also create a =RoleBinding= (namespaced!) that allows reading recipes:
 
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/cookierecipe-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/recipe-processor created
$ oc get pod
NAME                                 READY   STATUS    RESTARTS   AGE
recipe-processor-7f9969697b-qt9lv   1/1     Running   0          17s
$ oc logs -f recipe-processor-7f9969697b-qt9lv
 
 
New recipe found: double-dipped-shortbread
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 180 degrees Celsius
 
Fetching ingredients from recipe:
----------------------------------
Fetching 200grams of salted butter (softened)
[...]
#+end_example
 
The Operator will process both sample recipes.
 
* Test the Operator
 
We should test if the Operator notices new recipes, so let's create a third
recipe 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
cookierecipe.de.eenfach.olbohlen/oaty-hazelnut created
#+end_example
 
After a few seconds, we should see in the Operator log:
#+begin_example
New recipe found: oaty-hazelnut
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 180 degrees Celsius
[...]
#+end_example
 
* Test for updated recipes
 
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 recipe.
 
#+begin_example
$ oc patch cookierecipes double-dipped-shortbread --type merge \
> -p '{"spec":{"temperature":172}}'
cookierecipe.de.eenfach.olbohlen/double-dipped-shortbread patched
#+end_example
 
And again in the log you should see
 
#+begin_example
New recipe found: double-dipped-shortbread
--------------------------------------------------------------------------
 
Pre: we heat up the oven to 172 degrees Celsius
 
Fetching ingredients from recipe:
----------------------------------
[...]
#+end_example