#import "../Helpers/code-snippets.typ": code

== Backend
Das Backend wurde in go geschrieben und hat folgende Funktionalitäten und Schnittstellen:
- API Endpoint für die Verwaltung von Webseiten und dessen Status
- URL-Scanner um den Status eines URLs ausfindig zu machen
- Leader election um ein Leader Node im Kubernetes Cluster zu bestimmen welches den Scanner jede Minute ausführt
- Datenbankanbindung an PostgreSQL für Webseiten- und Statusinformationen

Externe Bibliotheken:
- Aufgaben Planung: 
    - github.com/go-co-op/gocron/v2
- Web Framework: 
    - github.com/gofiber/fiber/v2
- ORM:
    - gorm.io/gorm
    - gorm.io/driver/postgres
    - gorm.io/gorm/logger
- Kubernetes:
    - k8s.io/apimachinery/pkg/apis/meta/v1
    - k8s.io/client-go/kubernetes
    - k8s.io/client-go/rest
    - k8s.io/client-go/tools/leaderelection
    - k8s.io/client-go/tools/leaderelection/resourcelock

=== API Endpoint
==== Ausgabe aller Webseiten
*GET /api*
#code(caption: "Response")[```json
[
    {
        "ID": 49,
        "Name": "Andrin",
        "URL": "https://kessler.link/"
    },
    ...,
    {
        "ID": 82,
        "Name": "Gitlab",
        "URL": "https://gitlab.diller.gl/"
    }
]
```]
==== Webseite erstellen
*POST /api/website*
#code(caption: "Body")[```json
{
    "Name": "foo",
    "URL": "https://foo.bar/"
}
```]
#code(caption: "Response")[```json
Redirect zu "/"
```]
==== Webseite löschen
*DELETE /api/website/{websiteID}*
#code(caption: "Response")[```json
{
    "ID": <websiteID>,
    "Name": "foo",
    "URL": "https://foo.bar/"
}
```]
==== Status einer Webseite abfragen
*GET /api/website/{websiteID}/state*
#code(caption: "Response")[```json
[
    {
        "ID": 100,
        "CreatedAt": "2024-05-19T15:50:08.016902Z",
        "UpdatedAt": "2024-05-19T15:50:08.016902Z",
        "DeletedAt": null,
        "website": <websiteID>,
        "up": true,
        "return_code": 200,
        "request_date": "2024-05-19T15:50:07.773496Z",
        "response_time_ms": 242
    },
    ...
    {
        "ID": 1,
        "CreatedAt": "2024-05-18T15:49:08.033726Z",
        "UpdatedAt": "2024-05-18T15:49:08.033726Z",
        "DeletedAt": null,
        "website": <websiteID>,
        "up": true,
        "return_code": 200,
        "request_date": "2024-05-19T15:49:07.773956Z",
        "response_time_ms": 258
    }
]
```]
=== URL-Scanner
Der Scanner liest alle URLs aus der Datenbank aus und führt für jeden URL einen asynchronen Scan aus. 
Der Scan versucht eine HTTP GET Anfrage auf den URL auszuführen. Kann eine HTTP Verbindung aufgebaut werden wird der HTTP Statuscode gespeichert ansonsten bleibt dieser leer. Eine Webseite/URL gilt als "up" sofern eine HTTP Verbindung aufgebaut werden konnte und dessen Statuscode kleiner als 400 ist.

HTTP Statuscode Übersicht:
- 100 Information
- 200 Erfolgreich
- 300 Weiterleitung
- 400 Client Fehler
- 500 Server Fehler
Mehr dazu unter: https://httpwg.org/specs/rfc9110.html#status.codes

=== Leader election
Durch die Leader election wird im Kubernetes Cluster ein Node ausgewählt das den Scanner ausführt. Mithilfe von gocron können Aufgaben in regelmässigen Intervallen ausgeführt werden. Somit kann gewährleistet werden, dass die URLs genau einmal pro Minute von nur einem Node gescannt werden.

Bei der Leader Election Auswahl wird eine Gruppe von Kandidaten bestimmt. Diese Kandidaten versuchen sich selber als Anführer zu deklarieren. Einer der Kandidaten gewinnt die Auswahl und wird zum Anführer. Der Anführer sendet kontinuierlich "heartbeats", um seine Position als Anführer zu erneuern. Die anderen Kandidaten versuchen in regelmäßigen Abständen, Anführer zu werden. Dies stellt sicher, dass schnell ein neuer Anführer bestimmt wird, falls der aktuelle Anführer aus irgendeinem Grund ausfällt. Der Mechanismus nutzt das Kubernetes Koordinations Feature "Lease". Der Lease Lock wird über die Kubernetes API angesprochen und bietet somit eine geteilte Ressource an mit welcher die Nodes den Leader ausfinding machen können.

Wichtigste Parameter:
- LeaseDuration (45s): Zeitspanne die ein Leader Kandidat abwarten muss bevor er die Leader Funktion versucht zu übernehmen
- RenewDeadline (40s): Zeitspanne in der der Leader versucht seine Führung zu erneuern bevor er aufgibt.
- RetryPeriod (2s): Pause zwischen Abfragen auf das Lease Objekt

Lease Objekt:
- name: leaderelection-scanner
- namespace: site-pulse-360
- aquireTime: Zeitpunkt zu dem der Lease erworben wurde
- holderIdentity: Identität vom Leader
- leaseDuration (45s): Zeitspanne die ein Leader Kandidat abwarten muss bevor er die Leader Funktion versucht zu übernehmen
- renewTime: Zeitpunkt an dem der Leader das Lease Objekt zuletzt aktualisiert hat
#pagebreak()
=== Datenbankanbindung
Liest, speichert und löscht Datensätze in der PostgreSQL Datenbank. Die Anbindung an die PostgreSQL Datenbank ist über eine gewöhnliche DSN konfiguriert. Für die Representation der Datenbankentitäten wurde die ORM Bibliothek GORM verwendet, welches die Übersetzung von go Structs und Datenbankentitäten übernimmt. Über Auto Migration wird das Schema jeweils bei App Start aktualisiert, wobei zu gunsten vom Schutz der Daten keine Spalten gelöscht werden. Die Zugriffe auf die Datenbank erfolgen bei GORM jeweils automatisch über Transaktionen um die Datenenkonsistenz zu gewährleisten.
#code(caption: "backend/models/models.go")[```go
type Website struct {
    gorm.Model
    Name string `json:"name" gorm:"index;not null;default:null"`
    URL string `json:"url" gorm:"index;not null;default:null"`
    States []Status `json:"states" gorm:"constraint:OnDelete:CASCADE"`
}
type Status struct {
    gorm.Model
    WebsiteID uint `json:"website" gorm:"not null;default:null"`
    Up bool `json:"up" gorm:"not null;default:false"` // on zero values the default gets used boolean false -> zero value
    ReturnCode uint `json:"return_code" gorm:"default:null"`
    RequestDate time.Time `json:"request_date" gorm:"index;not null;default:null"`
    ResponseTimeMs uint `json:"response_time_ms" gorm:"not null;default:null"`
}
```]
=== Dockerfile
Beim Dockerfile haben wir uns für einen Multi-Stage-Build entschieden, dadurch erhalten wir ein viel kleineres Image und können den Vorteil von Golang, die ganze Applikation in ein einziges ausführbares Binary zu compilen, optimal ausnutzen. In der ersten Stage werden zuerst alle benötigten Golang Modules installiert und danach die ganze Applikation ins File `/usr/local/bin/sitepulse` compiled. In der zweiten und finalen Stage verwenden wir dann ein Alpine Image, um das Image so klein wie möglich zu halten, aber doch noch gewisse Grundfunktionalitäten wie eine Shell zur Verfügung zu haben. Wir erstellen einen neuen User und Gruppe `sitepulse` und kopieren das Binary von der ersten Stage ins Image. Zum Schluss wird der Port 3000 exposed und als Hauptprozess des Containers das Binary gesetzt. Um die Security zu erhöhen, wird der Hauptprozess mit dem User `sitepulse` und nicht mit `root` ausgeführt.
#figure(
  code(
    numbers: true,
    caption: "Dockerfile",
  )[```dockerfile
# build stage
FROM golang:1.22.3-alpine as build-stage
WORKDIR /usr/src/app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN go build -v -o /usr/local/bin/sitepulse ./cmd

# production stage
FROM alpine:3.19.1 as production-stage
RUN addgroup -g 1000 sitepulse && adduser -h /home/sitepulse -s /bin/sh -G sitepulse -D -u 1000 sitepulse
USER 1000:1000
COPY --from=build-stage --chown=1000:1000 --chmod=0770 /usr/local/bin/sitepulse /usr/local/bin/sitepulse
EXPOSE 3000
CMD ["sitepulse"]
```]
)