Kubernetes Shell Operator by Flant demo

Flant have anounced pretty cool shell-operator project

How it works

Imagine that you have script (any bash, pwsh, python, whatever) that will be called on events you are looking for (aka pod created, namespace deleted, etc)

To do so, your script should return wanted events when ran with --config argument

And whenever event will occur, script will be called with json passed as an argument

PowerShell-Operator

Here is full example for RBAC based cluster

First of all create namespace

apiVersion: v1
kind: Namespace
metadata:
  name: oper

Create custom resource definition (suppose we want to create our own redis operator)

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: redis.redis.io
  namespace: oper
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: redis.io
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      # Each version can be enabled/disabled by Served flag.
      served: true
      # One and only one version must be marked as the storage version.
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                memory:
                  type: integer
                replicas:
                  type: integer
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: redis
    # singular name to be used as an alias on the CLI and for display
    singular: redis
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: Redis

Apply all RBAC related stuff

apiVersion: v1
kind: ServiceAccount
metadata:
  name: oper
  namespace: oper

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: oper
  namespace: oper
rules:
  - apiGroups:
      - redis.io
    resources:
      - redis
    verbs:
      - get
      - watch
      - list

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: oper
  namespace: oper
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: oper
subjects:
  - kind: ServiceAccount
    name: oper
    namespace: oper

Config map with our script

apiVersion: v1
kind: ConfigMap
metadata:
  name: oper
  namespace: oper
data:
  entrypoint.sh: |
    #!/usr/bin/env bash

    # https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1#installation-via-direct-download---alpine-39-and-310
    apk add --no-cache ca-certificates less ncurses-terminfo-base krb5-libs libgcc libintl libssl1.1 libstdc++ tzdata userspace-rcu zlib icu-libs curl
    apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache lttng-ust
    curl -s -L https://github.com/PowerShell/PowerShell/releases/download/v7.1.3/powershell-7.1.3-linux-alpine-x64.tar.gz -o /tmp/powershell.tar.gz
    mkdir -p /opt/microsoft/powershell/7
    tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7
    chmod +x /opt/microsoft/powershell/7/pwsh
    ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh

    # https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x kubectl
    mv kubectl /usr/bin/


    # https://github.com/flant/shell-operator/blob/master/Dockerfile#L40
    exec /sbin/tini -- /shell-operator start
  oper.ps1: |
    #!/usr/bin/env pwsh

    if ($args[0] -eq '--config') {
      Write-Host '
      configVersion: v1
      kubernetes:
      - apiVersion: redis.io/v1
        kind: Redis
        executeHookOnEvent:
          - Added
          - Modified
          - Deleted
      '
    } else {
      $items = Get-Content $env:BINDING_CONTEXT_PATH | ConvertFrom-Json
      foreach($item in $items) {
        $event = $item.watchEvent
        $kind = $item.object.kind
        $name = $item.object.metadata.name
        $namespace = $item.object.metadata.namespace
        $replicas = $item.object.spec.replicas
        $memory = $item.object.spec.memory
        
        Write-Host "$event $kind $name $namespace $replicas $memory"

        if ($kind -eq "Redis") {
          if ($event -eq "Deleted") {
            kubectl -n $namespace delete deployment $name
          }
          
          if ($event -eq "Added") {
            kubectl -n $namespace create deployment $name --image=redis
          }

          if ($event -eq "Modified") {
            kubectl -n $namespace scale deployment $name --replicas=$replicas
          }
        }
      }
    }

Note that we are replacing entrypoint, installing powershell and using it instead of bash

Deploy shell operator itself

apiVersion: apps/v1
kind: Deployment
metadata:
  name: oper
  namespace: oper
  labels:
    app: oper
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oper
  template:
    metadata:
      labels:
        app: oper
    spec:
      containers:
        - name: oper
          # image: oper
          # imagePullPolicy: Never

          image: flant/shell-operator:latest
          command:
            - /entrypoint.sh
          imagePullPolicy: IfNotPresent

          volumeMounts:
            - name: oper
              subPath: oper.ps1
              mountPath: /hooks/oper.ps1
            - name: oper
              subPath: entrypoint.sh
              mountPath: /entrypoint.sh
      volumes:
        - name: oper
          configMap:
            name: oper
            defaultMode: 0755

And at the very end try to create our demo

apiVersion: redis.io/v1
kind: Redis
metadata:
  name: demo
  namespace: oper
spec:
  replicas: 1
  memory: 200

And if we look at our deployment logs we will see desired messages, all is left is to write the operator itself