| | |
| | | #+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: \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 title:nil ^:nil |
| | | # +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: Weird OpenShift Behaviours |
| | | |
| | | etcd, no foreign keys |
| | | -> permissions on non existing users |
| | | |
| | | |
| | | k8s does not realize immediate node failures |
| | | |
| | | k8s what happens if a container dies? |
| | | |
| | | where run probes (startup, readiness, liveness) |
| | | |
| | | API weirdness |
| | | - how to program the API, discover the API |
| | | |
| | | your frieds: |
| | | - jq |
| | | - yq |
| | | - xq :) |
| | | - jsonpath |
| | | |
| | | |
| | | - how to store cookie receipts in etcd via CRDs |
| | | |
| | | - I'm an operator with my pocket calculator: build your own operator |
| | | |
| | | - how to create ocp resources with etcd native |
| | | |
| | | - how does s2i work, where runs git clone, where the build, where are the artifacts saved... |
| | | #+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 |
| | | |