# OCI Registry einrichten

Um eigene Images zu verwalten und zu pushen, wird ein [OCI
Registry](https://docs.docker.com/registry/) benötigt.

Im folgenden Beispiel wird die Installation über `yaml` Dateien durchgeführt,
die direkt über `kubectl` importiert werden. Dazu werden die im folgenden
aufgeführten Beispiele in Dateien gepackt und über denn Befehl `kubectl apply -f
$DATEINAME` importiert.

### Namespace
Zunächst wird ein Namespace erstellt in dem die Registry läuft. Für das Beispiel
ist das der Namespace `registry`.

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: registry
```

### Storage
Damit die Daten der Registry auch nach einem Neustart erhalten bleiben, wird ein
Storae benötigt. Über die `default` Storage Klasse wird dafür ein
*PersistentVolumeClaim* erstellt. Dieser erstellt dann ein PersistentVolume
sobald dieses von dem Deployment angefordert wird.

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry
  namespace: registry
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: "5Gi"
#  storageClassName: longhorn
```

Wird eine andere als die `default` StorageClass gewünscht kann diese mit dem
Parameter `storageClassName` angegeben werden.

### Secrets

Damit die Registry nicht ohne Authentifizierung läuft muss im Vorfeld eine
`htpasswd` Datei erstellt werden um darüber die Benutzer zu verwalten.

Zunächst wird die `htpasswd` Datei erstellt. Dies lässt sich am einfachsten über
den `httpd` OCI Container durchführen. Dieser enthält das benötigte Progamm
`htpasswd`. Im Beispiel wird `podman` genutzt. Der Befehl kann gegen `docker`
getauscht werden.

```bash
user@linux $ podman run --rm --entrypoint htpasswd docker.io/httpd -Bbn \
                testuser testpassword > htpasswd
```

Über die Datei `htpasswd` wird anschließend ein `Secret` im Namespace `registry` erstellt.

```bash
user@linux $ kubectl create secret generic registry-secret --dry-run=client \
                -o yaml --type opaque --from-file htpasswd -n registry
apiVersion: v1
data:
  htpasswd: dGVzdHVzZXI6JDJ5JDA1JGVSNXliR3hoU0gxSmVtcEIxR0dZMnVwUFVJb1lFQnFTSXhaVVlEYUpYRld6SVV0UTVReDFDCgo=
kind: Secret
metadata:
  creationTimestamp: null
  name: registry-secret
  namespace: registry
type: opaque
```

### Deployment

Über das Deployment wird die Definition des Registry Pods durchgeführt. Dieser
wird dann über ein automatisch erstelltes ReplicaSet gestartet.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
      release: registry
  template:
    metadata:
      labels:
        app: registry
        release: registry
    spec:
      containers:
        env:
        - name: REGISTRY_STORAGE_DELETE_ENABLED
          value: "true"
        - name: REGISTRY_AUTH
          value: htpasswd
        - name: REGISTRY_AUTH_HTPASSWD_REALM
          value: Registry Realm
        - name: REGISTRY_AUTH_HTPASSWD_PATH
          value: /auth/htpasswd
        image: registry:2
        imagePullPolicy: ifNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 5000
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        name: registry
        ports:
        - containerPort: 5000
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 5000
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        volumeMounts:
        - mountPath: /auth
          name: auth
          readOnly: true
        - mountPath: /var/lib/registry/
          name: data
      restartPolicy: Always
      securityContext:
        fsGroup: 1000
        runAsUser: 1000
      terminationGracePeriodSeconds: 30
      volumes:
      - name: auth
        secret:
          defaultMode: 420
          items:
          - key: htpasswd
            path: htpasswd
          secretName: registry-secret
      - name: data
        persistentVolumeClaim:
          claimName: registry
```

### Service

Nach dem Einlesen des Deployments existiert der Pod `registry` im Namespace
`registry`, es existiert aber noch kein Zugriff von Außen auf den Container.
Dazu wird ein Service erstellt.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: registry
spec:
  ports:
  - name: http-5000
    port: 5000
    protocol: TCP
    targetPort: 5000
  selector:
    app: registry
    release: registry
  type: ClusterIP
```

Der Service ist in der Lage den passenden Pod auf Grund des `selector` zu finden
und diesem eine ClusterIP zu geben. Dies ClusterIP mit dem Port `5000` ist dann
eine Möglichkeit auf den Dienst zuzugreifen.

Allerdings nur auf einer der Cluster Nodes. Um auch von Außen auf den Dienst
zugreifen zu können macht es Sinn einen Ingress zu erstellen über den dann der
Zugriff erfolgt.

### Ingress

Für den Ingress wird eine DNS Domain benötigt, die auf den Cluster zeigt. Für
das Beispiel ist es `registry.internal`. Um auch per `https` auf den Dienst
zugreifen zu können muss ein TLS Zertifikat vorhanden sein. Diese kann wie Folgt
erstellt werden:

```bash
user@linux $ openssl req -x509 -newkey rsa:4096 -keyout tls.key -nodes \
                -out tls.crt -days 365 -subj '/CN=registry.internal'
```

Statt des selbstzertifizierten Zertifikats kann auch ein Let's Encrypt
Zertifikat über `cert-manager` oder ein bestehendes Zertifikat verwendet werden.

Aus dem Zertifikat wird nun wiederum ein `secret` erstellt.

```bash
user@linux $ kubectl create secret tls registry-tls -n registry \
                --key=tls.key --cert=tls.crt --dry-run=client -o yaml
apiVersion: v1
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZHVENDQXdHZ0F3SUJBZ0lVZC9kV1Q2TEloLzBlR2JJZXNqclA1WEQyQ0hBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0hERWFNQmdHQTFVRUF3d1JjbVZuYVhOMGNua3VhVzUwWlhKdVlXd3dIaGNOTWpVd05ERTNNVEV4TmpBMApXaGNOTWpZd05ERTNNVEV4TmpBMFdqQWNNUm93R0FZRFZRUUREQkZ5WldkcGMzUnllUzVwYm5SbGNtNWhiRENDCkFpSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBTFJwdUYxcW5OTXdUUDNrdy9SWFdMSWUKU1dIVXJ0U3dGMjJML0RSL0N3bWRCU0RvbjIvRHJFcTdqcmYwOUkrS21SMGJlRWNNczJUTDQvSVpIT2RYQkVZTgpMbzNJTTR5c1ZoRUh1ajRDekFQYzc5eW95U20vYjFSRVpjSElKd0ltSS93UjRrSlFhODU3eTZZVVR5enhUbFdyCm8zUXdLcHEwcnhwdDgxN083SkI0THp1V1dyTklqdWpJVHh2YXNXR1ozd0NuSmNIczRzWlZpZk1KUGZhV2RpUysKd2hTZDR6U1J1VnZFWFZMalFmYkk2bGtJVTF4THIrR2VPL0c5WVZ2TmFJeVZsT0JUd1Jqa0hDT2pOa0dpR1dKVAp1b2JoTE43KzNWaFdmYjZ3enQwV3BHM0NJTjVFemJNOUxQems3aVljdTk2RVJ3dlhvREp6eGRSbUF3WUZRY2twCjEzd1VoSGdFdkZUZDlvZk9HZ3graDd3cm1Mdk1mZitCRUk3amRNUEhST3UwSVpqSGxOQTVMRzA3ai9rbE5xWmsKMXdhZHNyUjNDZjVpM0tVbmlpMTYxQ29WMitjU1YyRTIyUFBrVnNsVklKYmJvT0VXZGc0c2ZNTHd5Y3ZSVk8xcQovVjl6MEVsajBhMllSTUdXMWI4cXIwNkpHUkRvVEFKSFNzc1R1dm5icmJ6ZzJOdEl0NFd0OFZpVXRFVjdTMkpTCnN6WmhwSTRUUlN2bHRwbUlneXVMWDUzVmNWWUhxRXBFY2xZOC9tSkdmdXJzWGxhdm9NSmlaYk5MMWpLaUJkL1kKNkRxMTJnMmMvc3BXOXlYeGJyQnlIWEZraVZpc21pOG5tTFYxQTFsSHpGc3ZTdnIzZTRpVWpLV0t3RklsQ2o4Rgorak42c2FFdjFmZHFwUldERExXMUFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCVElWdGo0bDdCNVczcUw1T0NOClBrWFlNWkdpcWpBZkJnTlZIU01FR0RBV2dCVElWdGo0bDdCNVczcUw1T0NOUGtYWU1aR2lxakFQQmdOVkhSTUIKQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFBcUtWVjFHM2JwRHFMN04rTnpNTitGK0lIMwphUVd3ZGFRSGtuN2JXYmNBamQvUEZ1YXJPdCtMaUMvUmQ5VUhLWVA3MGdHZmQ5YTFmWjBGTlZDb3RZR2ZvYnVDCjNUV0RnanRXNy8wSi9ENXFNR3REWk1UVW1NVGVheVR5VlpsdUlxcTdGMklDYjU0R0k2WnR4aWRuNUw5bkhpQU8KRGFmQnZkMTFPQ2ViZ1VsRHBVR3FoaW50ZmJsVkY1NUhwUjdiSEpqV2U1WDAva3NyZi92a0RUMEVLYTlzZkhrUwpLTTZaNm9LTVlDUEdrL1d4dzA0YkF2dzU2U0pMUnFTU0NCL1pqQVQxazhyeW1ieENMekczN1NPeDJ4NWpueEF5CkZ5RldUcExHb2hOZmRURDlxOGxlSjJYMUh5L1gwVVgyM1Vpek5US2kyRjMxallDVXQ5QkJwbEx6ekM5bW1BeEgKbm9FMnNKS3lKbXdDdTk3VGhvOUlnSHpPMVZHV3Qyb2NPQ0lNdkxqY2ZoTUpHR2F5MFVoQ3JvYk9XWlBHS3VndAoyQU85VW9WaDVWMzloVDJVSkFVZFdDN0hsYi9RdFhLL3ZyczgybW5kZDVyUXFXajVrSjFEbXRNaldFak1DcVlvCjJPd0tSc1pqT2ZzcGhDSlpIOFR5eFoxY1V3Q0laMEsrVzFxeURSWDYwUkF4M3ZtMFBueU1YNGR4aFJsMWl6VmEKZi9jc1d3K09mSnVnVzZ6Zm1VdlV4by95WG0vSElHVExaL003OUlBY3NrbnJuM0pqWTBubGlmd0h4bFV1U0F6ZwoxN2ZIcjdGcjk3NHIzYWZLdWZnSU8rR1VkZnFsQWZhWkxmcGtqdGhqZ3FEV0VBRlNaSEhxcXJuWnZva2hhZURpClIyZUZVT0JRT1czaHhhVTdZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQzBhYmhkYXB6VE1FejkKNU1QMFYxaXlIa2xoMUs3VXNCZHRpL3cwZndzSm5RVWc2Sjl2dzZ4S3U0NjM5UFNQaXBrZEczaEhETE5reStQeQpHUnpuVndSR0RTNk55RE9NckZZUkI3bytBc3dEM08vY3FNa3B2MjlVUkdYQnlDY0NKaVA4RWVKQ1VHdk9lOHVtCkZFOHM4VTVWcTZOME1DcWF0SzhhYmZOZXp1eVFlQzg3bGxxelNJN295RThiMnJGaG1kOEFweVhCN09MR1ZZbnoKQ1QzMmxuWWt2c0lVbmVNMGtibGJ4RjFTNDBIMnlPcFpDRk5jUzYvaG5qdnh2V0ZieldpTWxaVGdVOEVZNUJ3agpvelpCb2hsaVU3cUc0U3plL3QxWVZuMitzTTdkRnFSdHdpRGVSTTJ6UFN6ODVPNG1ITHZlaEVjTDE2QXljOFhVClpnTUdCVUhKS2RkOEZJUjRCTHhVM2ZhSHpob01mb2U4SzVpN3pIMy9nUkNPNDNURHgwVHJ0Q0dZeDVUUU9TeHQKTzQvNUpUYW1aTmNHbmJLMGR3bitZdHlsSjRvdGV0UXFGZHZuRWxkaE50ano1RmJKVlNDVzI2RGhGbllPTEh6Qwo4TW5MMFZUdGF2MWZjOUJKWTlHdG1FVEJsdFcvS3E5T2lSa1E2RXdDUjByTEU3cjUyNjI4NE5qYlNMZUZyZkZZCmxMUkZlMHRpVXJNMllhU09FMFVyNWJhWmlJTXJpMStkMVhGV0I2aEtSSEpXUFA1aVJuN3E3RjVXcjZEQ1ltV3oKUzlZeW9nWGYyT2c2dGRvTm5QN0tWdmNsOFc2d2NoMXhaSWxZckpvdko1aTFkUU5aUjh4YkwwcjY5M3VJbEl5bAppc0JTSlFvL0Jmb3plckdoTDlYM2FxVVZnd3kxdFFJREFRQUJBb0lDQUNPVTlFRXNYVGpEdm5PV3NsVVhBdzNDCkxtdm1sQUtycGpzN1VDY1VaVnRraGhYcSswTUNQeEZRTTRJeGhDaHBSL0IzTWY0bFVaNVFIaWxwN1lyczNSRnAKMlNRcWQ0eEhrd1B4MTdnU09pV0s1aDNKaGo4L0c5aHRVdnBvbk5UdUs4dXp4VUdaOWVFNEJqNi9vNnZ6L2FTawo0T3h4OFgwb3BKNWNVQk1EVWFIZXFrWWd6Y3V0UkU1QjcyUkplaEdMVlBHZWhiSmRJNDdGWVJzM1YrcXgyeVE5CklTOEt3cnhqLzBCTGxySlowdkUyeUExK09GNnNLTWloT2laZjJQdFBwRTZSMDlaNGZrMkg4OHJOams1Y2g1QWUKZmx2S3ZseE92TjRHMU84T2xRWUN5TFJEQk5PQmsyWkY2d0VIcjdhQmxVWDZKYjcrcklBMjJDNGZYbnBLQUNLMApTY0Jjd3d6dzBmQTF4a3BaTk1zSVVpLzhwLzJvUjlyY0loaUNRSTd0WFlueERRSENWZ3dVNGJITjJhakdHMUJICitZTmZsK2t4QWk1WUJja0p0bHVjQXJoWWdHUFd0b1FmWnFJU3IrNFVkWEFTZ3VBOGp5aVdHVVlBWnFuUlVrWEMKYjBseTIyR0x5dWRoN2ZJSnFmQXR3Sm5ON25icmpDZy90dHFOc1FMZWgxWGpVYUNYbExvMmVTVmFlZHNZa0dJNwpKMXVwckJiWVlqQURDa1hLaDhHYnB0cEZBZmpRekU2TjEyRTd1dVF1eGdrRXRBZnp6QlIrWTh3ejhpWkFPR043ClpWcUYwTWlRV21UWmRhM3RVSnM5QnpKMDBPZDhNbzZsVHFYTlFxOXU0TkxoOThzTTRsc0dhU0lLYmFTS09RQ2MKTkpKdXc3M3R2V0ltZlNQNm5GbVRBb0lCQVFEOFVmSTlwTU52R2picjQ3Y00vZElYRk44NWFQZ2VDd2RXRXh2KwpWUG9VeVd2THp0a3BtdWRUdWZXY25aVzVZSTZRZUVtcmk0YjNkbUMySHgvS0tTL0c0a0tvdFZncFJhd3RjSUpNCnh4ck5YdTdZSjhadkg2bG84NUxHbmlQVDR2NDRNR2dHNjFBRVREVEorakUrSStwVmpnazdEKzNXQnd5RkRSbXAKaWVoSSs2THU0VDI1U3ZEYjdaZW1OT1RXbjY1N25BOURXUzUrRUtQY00yOFJid3VTdEJiMEZNTWptUFhJM25NcwpyeDBkd2lNc0wzNStOdU9qRk14VEpocnQ5RHFESUJBUGdtdUNKTUNuVHlwakdKR01DbzJZVXpZdko3U0Z0TjMvClBCd0dQb050cjFkUW4ydnpVeHluZVBTeDZydDZYUlhOeFQyTU1tUmg2dndoTUNtdkFvSUJBUUMzQzAzTU90NFgKRTVnQkxPZzc4L2ZWb2txNURCa0tBdGlPM3lJWnFHS1BXUHRnQ2x5dUpzZ012SVI3M3hoVkJ6YTJCWkMvUkNyMApkQWRRbnZXYXRhb1YyN3YzMFBzQ0xRaFU3Q2NpdWdxRlJUN1lnV0RIQk5tbkVWeHExOWYxd2pTcHlIdVl1bUkyCk5SczZCME90RHRleld3TndvWkRCZGN1VnR5cXFReDdWRFErcHRKZ290dnhtbWNRcTN4WnhQczNuWmhVazJ1VlUKL241SnhzNWFLOHpNZHRoUWpucGdOYzVFQmhEa1pjK1dyTmM1WjBtNUsvMnh5dlFsdXF6R1AzWnJYUlFFcm1VYwpOWkQrRVJxdVVuY3BJS3pMOU8yVHFKZHoySGlQOWptSUJKOTdjVUVrNHRQcTJZYnFlWHY1SGxRczk5TGtXTFFQCm9rVHp4RUthWkFQYkFvSUJBQk5yYXkxN1pOOUVNVDN1aE1Rbk5PZzUzd1JZSStDTUVNQjdNQWhmR2ZCSG9GaEwKVDBONGZKMWhEcHBETnFiUjI2Y2EydkN0Q0hJN1ZpNHVMeEFzSTFVM1ROSVhRdlhLT3ZvMFVwTCtMLzNtZEpPRwpYQ3RwVUd5WGNwQiswMXNYZVdGeHVFL3dCSFNRT08vaXBhK1dyQTV4cEJ3N21aNkRaRjlKSnRSNW5Mc2hRdVVICnlPeGQ3RFBCRVk3bE8wZVplcUJnM252Y2ZVdU9sRTM2VjI0TUVlUDBvRitneVhRUDdick1CQ2xJUVFGdFU2K3UKNkJsbXVCWnhIMWkyOGhPbVhHcURLVGxJWEdYajFrQUtROFRrdXk4QUVPM01XYnMvWHhGN1hpcXF3YTFwV2t3eQoxcEdKODVFQ3NJM2pMVklVTXVHMEVGMko5TGE1bWlnL0liQ2NPRmtDZ2dFQWNyalgrcitPZ0tJY0sraHNhVjhBCkh0cEh3UER2SkpJaURuR1ZHc1dwZURTSjRHaStLN3hNdDRiVDloc2VVZlJpZXZURUFzeURxNUNwSVFOdjVaWWsKVXV6VVEzNnNRM0hiL2ZYQWxZaEtuYnFIcUFSMmxtWFkwRmdXTnpTdXV1NE1PYmxZbGFHRGVud1FZMjg0SUZmawpJeHBRN3Ara2JVd1oyK3pnQmJScHQ1Z25EL01MUnNGaE1pRC9qT3NqVXJqTkxsR1J3UGFjczRlSnVVMnIrV0hvClN1SFZzNitJelpJTGhDRFZKNEVUK1ZpOHo5S0hwMUlHQUErMnM0cnJUSmxyNitCN2NtK25HV1lOU04rSmZHWmQKSHQ3R3psSGxNUjJDUnU0T3FoUGxmUFdBeTBweFRPQUpkL0Q4R1NzbzR6cjZBUEVXTVYzVEJOVnNKK2dDNG1iNgo3d0tDQVFFQXoxWEdMV1pILytyblozdmVkVzh2U0U0cks0N3RXQ0dwQlJsM0xSTTBia1ZlaGJQN3JLRHg4a3MwCmtqQUtXei8rb3Vud1BqSGR0T2dBZmhVcUR6cS85Q2IzOFBOQ1Q3emM5dmlnc2FXc3ZuU05aSWNjakhFV2hpT1QKcjg1L3h6ZG82WkorOE1WQzl0Wll5YjRudmNuWkhVV2dKSmljYm1PMTAxcnJxVG1uUi8xcFZwVnF2R3FwNG5lRgprbm5mS1oydnhwZUlsQjIyQ20rNGlLNzdLekgveW1oc2xGcE5nR3FNS0F3RFlqM0NHK21hc0RPMXVXUUhSWjZWCmxQYjB0WHNOVVNYLysyb1d5U2VYWlBTbnhMd1JvUDZPdFFGTjdSR0MyekN6b05tdndnV3l5RVVrNHFtVW9vOVYKSTBrSFBkNkhFVXFkcTF3SVJ5WjBrWWFMakNJRkNBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
kind: Secret
metadata:
  creationTimestamp: null
  name: registry-tls
  namespace: registry
type: kubernetes.io/tls
```

Dieses `secret` wird in dem folgenden Ingress verwendet.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    # cert-manager.io/cluster-issuer: acme-production
    # kubernetes.io/tls-acme: "true"
    ingress.kubernetes.io/proxy-body-size: 1024m
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/proxy-body-size: 1024m
  name: registry
  namespace: registry
spec:
  rules:
  - host: registry.internal
    http:
      paths:
      - backend:
          service:
            name: registry
            port:
              number: 5000
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - registry.internal
    secretName: registry-tls
````

Die beiden auskommentierten Einträge sind die Verwendung von `cert-manager` und
einem ClusterIssuer gedacht. Der ClusterIssuer muss dafür zunächst angelegt sein
und die Domain von extern durch den Zertifizierungsdienst - z. B. Let's Encrypt
- erreichbar sein.

Nach dem Erstellen des Ingress ist ist die Registry über
https://registry.internal erreichbar.
