OS Apps: Yal and Trillium

Published: Apr 23, 2024 by Isaac Johnson

YAL, or “Yet Another LandingPage” is a simple but functional landing page we can host and configure with some simple JSON. We’ll check that out in Kubernetes by making a quick manifest and applying it.

Trillium is a rich personal note organizer that like YAL can be hosted with Docker or Kubernetes. We’ll set this up and actually try using it for a while to see how it is to use on a daily basis.

Let’s dig in.

YAL

YAL can be downloaded and run locally, or in Docker.

Let’s do it all in Kubernetes instead.

I can create a manifest YAML

builder@LuiGi:~/Workspaces/yal$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yal-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yal
  template:
    metadata:
      labels:
        app: yal
    spec:
      containers:
      - name: yal
        image: timoreymann/yal:latest
        ports:
        - containerPort: 2024
        env:
        - name: YAL_PORT
          value: "2024"
        - name: YAL_PAGE_TITLE
          value: "freshBrewed"
        - name: YAL_CONFIG_FOLDER
          value: "/app/config"
        - name: YAL_IMAGES_FOLDER
          value: "/app/images"
        - name: YAL_ICONS_FOLDER
          value: "/app/icons"
        volumeMounts:
        - name: config-volume
          mountPath: "/app/config"
        - name: icons-volume
          mountPath: "/app/icons"
        - name: images-volume
          mountPath: "/app/images"
        # Add other environment variables as needed
      volumes:
      - name: config-volume
        persistentVolumeClaim:
          claimName: config-pvc
      - name: icons-volume
        persistentVolumeClaim:
          claimName: icons-pvc
      - name: images-volume
        persistentVolumeClaim:
          claimName: images-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: yal-service
spec:
  selector:
    app: yal
  ports:
  - protocol: TCP
    port: 80
    targetPort: 2024
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: config-pvc
spec:
  storageClassName: local-path
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: icons-pvc
spec:
  storageClassName: local-path
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: images-pvc
spec:
  storageClassName: local-path
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

Then apply

builder@LuiGi:~/Workspaces/yal$ kubectl create ns yal
namespace/yal created
builder@LuiGi:~/Workspaces/yal$ kubectl apply -f ./manifest.yaml -n yal
deployment.apps/yal-deployment created
service/yal-service created
persistentvolumeclaim/config-pvc created
persistentvolumeclaim/icons-pvc created
persistentvolumeclaim/images-pvc created

That crashes because the config.json needs to be in config first.

Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  44s                default-scheduler  0/3 nodes are available: persistentvolumeclaim "config-pvc" not found. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling..
  Normal   Scheduled         42s                default-scheduler  Successfully assigned yal/yal-deployment-6764c78884-j8t9q to builder-hp-elitebook-745-g5
  Normal   Pulled            40s                kubelet            Successfully pulled image "timoreymann/yal:latest" in 2.216625871s (2.216664703s including waiting)
  Normal   Pulled            39s                kubelet            Successfully pulled image "timoreymann/yal:latest" in 465.872652ms (465.911414ms including waiting)
  Normal   Pulling           22s (x3 over 42s)  kubelet            Pulling image "timoreymann/yal:latest"
  Normal   Pulled            22s                kubelet            Successfully pulled image "timoreymann/yal:latest" in 450.162235ms (450.191638ms including waiting)
  Normal   Created           22s (x3 over 40s)  kubelet            Created container yal
  Normal   Started           22s (x3 over 40s)  kubelet            Started container yal
  Warning  BackOff           10s (x4 over 38s)  kubelet            Back-off restarting failed container yal in pod yal-deployment-6764c78884-j8t9q_yal(4f145bda-166c-4f8d-a401-8677d521c62c)
builder@LuiGi:~/Workspaces/yal$ kubectl logs yal-deployment-6764c78884-j8t9q -n yal
yal 1.5.0 (24-04-05_15:06:17) by Timo Reymann
---------------------------------------------

Build information
GitSha              693c69e
Version             1.5.0
BuildTime           24-04-05_15:06:17
Go-Version          go1.22.2
OS/Arch             linux/amd64
---------------------------------------------
2024/04/07 22:37:59 Failed to render template: open /app/config/items.json: no such file or directory

Perhaps it is better ot change from a config-pvc to a mounted configmap volume

$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yal-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yal
  template:
    metadata:
      labels:
        app: yal
    spec:
      containers:
      - name: yal
        image: timoreymann/yal:latest
        ports:
        - containerPort: 2024
        env:
        - name: YAL_PORT
          value: "2024"
        - name: YAL_PAGE_TITLE
          value: "freshBrewed"
        - name: YAL_CONFIG_FOLDER
          value: "/app/config"
        - name: YAL_IMAGES_FOLDER
          value: "/app/images"
        - name: YAL_ICONS_FOLDER
          value: "/app/icons"
        - name: YAL_LOGO
          value: "http://freshbrewed-test.s3-website-us-east-1.amazonaws.com/img/happy_me_emoji.png"
        - name: YAL_MASCOT
          value: "http://freshbrewed-test.s3-website-us-east-1.amazonaws.com/img/happy_me_emoji.png"
        - name: YAL_BACKGROUND
          value: "http://freshbrewed-test.s3-website-us-east-1.amazonaws.com/content/images/2019/02/image-15.png"
        - name: YAL_FAVICON
          value: "https://freshbrewed.science/favicon.png"
        volumeMounts:
        - name: icons-volume
          mountPath: "/app/icons"
        - name: images-volume
          mountPath: "/app/images"
        - name: config-volume
          mountPath: "/app/config"
        # Add other environment variables as needed
      volumes:
      - name: icons-volume
        persistentVolumeClaim:
          claimName: icons-pvc
      - name: images-volume
        persistentVolumeClaim:
          claimName: images-pvc
      - name: config-volume
        projected:
          sources:
          - configMap:
              name: yaml-configjson-configmap
              items:
              - key: items.json
                path: items.json
          - configMap:
              name: yaml-searchenginejson-configmap
              items:
              - key: searchEngines.json
                path: searchEngines.json
---
apiVersion: v1
kind: Service
metadata:
  name: yal-service
spec:
  selector:
    app: yal
  ports:
  - protocol: TCP
    port: 80 
    targetPort: 2024
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: config-pvc
spec:
  storageClassName: managed-nfs-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: icons-pvc
spec:
  storageClassName: managed-nfs-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: images-pvc
spec:
  storageClassName: managed-nfs-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: yaml-configjson-configmap
data:
  items.json: |
    [
      {
        "title": "My Section Title",
        "entries": [
          {
            "text": "Freshbrewed Blog",
            "link": "https://freshbrewed.science",
            "description": "The freshbrewed blog",
            "icon": "https://microsoft.github.io/garnet/img/garnet-logo-diamond.png"
          }
        ]
      }
    ]

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: yaml-searchenginejson-configmap
data:
  searchEngines.json: |
    [
      {
        "title": "MySearch",
        "urlPrefix": "https://my.search?text=whatever"
      }
    ]

Then I can apply

builder@LuiGi:~/Workspaces/yal$ kubectl apply -f ./manifest.yaml -n yal
deployment.apps/yal-deployment configured
service/yal-service unchanged
persistentvolumeclaim/config-pvc unchanged
persistentvolumeclaim/icons-pvc unchanged
persistentvolumeclaim/images-pvc unchanged
configmap/yaml-configjson-configmap created
configmap/yaml-searchenginejson-configmap created

Now I can port-forward and see a very ugly page as I chose some random images with which to test

$ kubectl port-forward svc/yal-service -n yal 8888:80
Forwarding from 127.0.0.1:8888 -> 2024
Forwarding from [::1]:8888 -> 2024
Handling connection for 8888
Handling connection for 8888

/content/images/2024/04/yal-01.png

That is really bad.

I made a white pixel with https://onlinepngtools.com/generate-1x1-png and copied it to https://freshbrewed.science/whitepixel.png.

I updated the manifest to use that as a background


        - name: YAL_LOGO
          value: "https://freshbrewed.science/favicon.png"
        - name: YAL_MASCOT
          value: "http://freshbrewed-test.s3-website-us-east-1.amazonaws.com/img/happy_me_emoji.png"
        - name: YAL_BACKGROUND
          value: "https://freshbrewed.science/whitepixel.png"
        - name: YAL_FAVICON
          value: "https://freshbrewed.science/favicon.png"

and updated

$ kubectl apply -f manifest.yaml -n yal
deployment.apps/yal-deployment configured
service/yal-service unchanged
persistentvolumeclaim/config-pvc unchanged
persistentvolumeclaim/icons-pvc unchanged
persistentvolumeclaim/images-pvc unchanged
configmap/yaml-configjson-configmap unchanged
configmap/yaml-searchenginejson-configmap unchanged

That’s almost worse

/content/images/2024/04/yal-02.png

I think the more I mess with this, the uglier I make it.

I’ll just smile and say “thanks, but no” on this one.

Trillium

Trillium is a note taking app we can host in Docker or Kubernetes.

Since the Docker approach is pretty easy, let’s instead pivot to doing this all in Kubernetes.

We can create a manifest with a PVC:

$ cat trillium.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: trilium-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: trilium
  template:
    metadata:
      labels:
        app: trilium
    spec:
      containers:
        - name: trilium-container
          image: zadam/trilium
          ports:
            - containerPort: 8080
          env:
            - name: TRILIUM_DATA_DIR
              value: "/home/node/trilium-data"
      volumes:
        - name: trilium-data-volume
          persistentVolumeClaim:
            claimName: trilium-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: trilium-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
  name: trilium-service
spec:
  selector:
    app: trilium
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

Then create a namespace and apply

$ kubectl create ns trillium
namespace/trillium created

$ kubectl apply -f ./trillium.yaml -n trillium
deployment.apps/trilium-deployment created
persistentvolumeclaim/trilium-pvc created
service/trilium-service created

We can see it launched

$ kubectl get pods -n trillium
NAME                                  READY   STATUS    RESTARTS   AGE
trilium-deployment-59778d4dbb-pf5l7   1/1     Running   0          73s

$ kubectl get svc -n trillium
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
trilium-service   ClusterIP   10.43.244.185   <none>        8080/TCP   78s

I can now hit the service

$ kubectl port-forward svc/trilium-service -n trillium 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

We are presented with a setup page

/content/images/2024/04/trillium-01.png

Then I just set a password

/content/images/2024/04/trillium-02.png

And then I can use it

/content/images/2024/04/trillium-03.png

It’s suprisingly complete

/content/images/2024/04/trillium-04.png

We can play around with it

I now want to setup an ingress

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n trillium
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "ce9e21ac-f620-4209-bcaa-1a762c4c4eac",
  "fqdn": "trillium.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/trillium",
  "name": "trillium",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

I then create an Ingress yaml

$ cat trillium.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/websocket-services: trilium-service
  name: trillium-ingress
  namespace: trillium
spec:
  rules:
  - host: trillium.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: trilium-service
            port:
              number: 8080
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - trillium.tpk.pw
    secretName: trillium-tls

$ kubectl apply -f ./trillium.yaml -n trillium
ingress.networking.k8s.io/trillium-ingress created

I can now login with a proper URL

/content/images/2024/04/trillium-06.png

One of the killer features (to me) is exporting out as markdown.

This means we have portability with anything that does markdown. I often write my daily status updates in Google Docs, but its a pain to maintain formatting and export. However, I’ve often avoiding moving to other tools because I didn’t want lock in. This would allow me to dump reports for managers/end of month backup.

Real Usage

I wanted to try living with Trillium for a few days.

To do so meant creating a new project

We start by making a plain old note

/content/images/2024/04/trillium-07.png

When I click the + sign, it will create child notes turning the first into a folder icon

/content/images/2024/04/trillium-08.png

I couldn’t figure out how to add a Journal easily so I instead cloned from the Demo area

/content/images/2024/04/trillium-09.png

and saved it to my new folder

/content/images/2024/04/trillium-10.png

Then I just deleted the demo dates out (which were a few years old at this point and easy to identify)

/content/images/2024/04/trillium-11.png

I typically break a day down to Meetings, Actions, Pings and Plans. Here I’ll try just adding Plans and Actions

/content/images/2024/04/trillium-12.png

What I liked is that I could look at an overall Day-view that covered both my plans and actions

/content/images/2024/04/trillium-13.png

Summary

We checked out two projects today. The first was YAL. It’s not bad, per se, but it really didn’t fit my needs. I likely won’t be exposing that endpoint.

The second was Trillium which is a Personal Notes Organizer. It’s surprisingly rich for something with no commercial backend. Though they encourage one to donate to Ukraine if one likes the software.

YAL Trillium OpenSource Containers Kubernetes

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes