diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd63833..ab33920 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
## CHANGELOG
+### [24.6.3] - Jun 24, 2024
+- Fixed an issue with `/logs` as it didn't adhere to the default `Content-Security-Policy`
+- Updated instructions for deploying JinjaFx Server as a Container using Kubernetes
+
### [24.6.2] - Jun 20, 2024
- The ETag hash is now across all additional headers including `Content-Type` and `Content-Security-Policy` as well as the content itself
@@ -315,6 +319,7 @@
### 21.11.0 - Nov 29, 2021
- Initial release
+[24.6.3]: https://github.com/cmason3/jinjafx_server/compare/24.6.2...24.6.3
[24.6.2]: https://github.com/cmason3/jinjafx_server/compare/24.6.1...24.6.2
[24.6.1]: https://github.com/cmason3/jinjafx_server/compare/24.6.0...24.6.1
[24.6.0]: https://github.com/cmason3/jinjafx_server/compare/24.5.0...24.6.0
diff --git a/README.md b/README.md
index 9110a7f..49aabfd 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,9 @@ Once JinjaFx Server has been started with the `-s` argument then point your web
JFX_WEBLOG_KEY - specify a key to allow access to web log interface
```
-For health checking purposes, if you specify the URL `/ping` then you should get an "OK" response if the JinaFx Server is up and working (these requests are omitted from the logs). The preferred method of running the JinjaFx Server is with HAProxy in front of it as it supports TLS termination and HTTP/2 - please see the [/podman](https://github.com/cmason3/jinjafx_server/blob/main/podman) directory for more information about running JinjaFx as a Rootless Podman container.
+For health checking purposes, if you specify the URL `/ping` then you should get an "OK" response if the JinaFx Server is up and working (these requests are omitted from the logs).
+
+The preferred method of running the JinjaFx Server is with HAProxy in front of it as it supports TLS termination and HTTP/2 (and more recently HTTP/3 using QUIC) or using a container orchestration tool like Kubernetes - please see the [/kubernetes](/kubernetes) directory for more information about running JinjaFx using Kubernetes.
The "-r", "-s3" or "-github" arguments (mutually exclusive) allow you to specify a repository ("-r" is a local directory, "-s3" is an AWS S3 URL and "-github" is a GitHub repository) that will be used to store DataTemplates on the server via the "Get Link" and "Update Link" buttons. The generated link is guaranteed to be unique and a different link will be created every time - version 1.3.0 changed the behaviour, where previously the same link was always generated for the same DataTemplate, but this made it difficult to update DataTemplates without the link changing as it was basically a cryptographic hash of your DataTemplate. If you use an AWS S3 bucket then you will also need to provide some credentials via the two environment variables which has read and write permissions to the S3 URL.
@@ -124,7 +126,7 @@ Under the field the `text` key is always mandatory, but the following optional k
- `type` - if set to "password" then echo is turned off - used for inputting sensitive values
-In addition to the above prompt syntax, we also support the ability to specify a custom html input form to provide greater flexibility. As JinjaFx is built on Bootstrap 5, it uses the Bootstrap 5 Modal syntax to specify what is contained in the body of your modal form. Bootstrap works on a row and column grid with each row comprising of 12 columns - you use the various "col-n" classes to specify how wide each element is.
+In addition to the above prompt syntax, we also support the ability to specify a custom html input form to provide greater flexibility. As JinjaFx is built on Bootstrap 5, it uses the Bootstrap 5 Modal syntax to specify what is contained in the body of your modal form. Bootstrap works on a row and column grid with each row comprising of 12 columns - you use the various "col-n" classes to specify how wide each element is.
You can specify a custom input form using the `body` variable under `jinjafx_input` within your "vars.yml" - if this exists then whatever you have in `prompt` is ignored.
diff --git a/jinjafx_server.py b/jinjafx_server.py
index 7c4dcda..c5120b8 100755
--- a/jinjafx_server.py
+++ b/jinjafx_server.py
@@ -28,7 +28,7 @@
import re, argparse, hashlib, traceback, glob, hmac, uuid, struct, binascii, gzip, requests, ctypes, subprocess
import cmarkgfm, emoji
-__version__ = '24.6.2'
+__version__ = '24.6.3'
llock = threading.RLock()
rlock = threading.RLock()
@@ -392,7 +392,7 @@ def do_GET(self, head=False, cache=True, versioned=False):
headers = {
'X-Content-Type-Options': 'nosniff',
- 'Content-Security-Policy': "default-src 'self'; style-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline'; script-src 'self' https://cdnjs.cloudflare.com; img-src data: *; frame-ancestors 'none'",
+ 'Content-Security-Policy': "default-src 'self'; style-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline'; script-src 'self' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src data: *; frame-ancestors 'none'",
'Referrer-Policy': 'strict-origin-when-cross-origin'
}
etag = '"' + hashlib.sha224(repr(headers).encode('utf-8') + b'|' + r[0].encode('utf-8') + b'; ' + r[2]).hexdigest() + '"'
diff --git a/podman/Dockerfile b/kubernetes/Dockerfile
similarity index 100%
rename from podman/Dockerfile
rename to kubernetes/Dockerfile
diff --git a/kubernetes/README.md b/kubernetes/README.md
new file mode 100644
index 0000000..944751a
--- /dev/null
+++ b/kubernetes/README.md
@@ -0,0 +1,45 @@
+## JinjaFx Server as a Container in Kubernetes
+
+JinjaFx Server will always be available in Docker Hub at [https://hub.docker.com/repository/docker/cmason3/jinjafx_server](https://hub.docker.com/repository/docker/cmason3/jinjafx_server) - the `latest` tag will always refer to the latest released version, although it is recommended to use explicit version tags.
+
+The following steps will run JinjaFx Server in a container using Kubernetes Ingress - Ingress is basically the same concept as Virtual Hosting (the default Ingress uses nginx), which works with HTTP and relies on the "Host" header to direct the request to the correct container. In a Virtual Hosting scenario you would typically point different DNS A records towards the same IP, but in our example we are using a Wildcard DNS entry for our whole Kubernetes cluster, e.g:
+
+```
+*.{CLUSTER}.{DOMAIN}. 28800 IN A {HOST IP}
+```
+
+This approach also allows us to use a single wildcard TLS certificate, which covers all containers under the cluster sub-domain. The example Kubernetes manifest (`kubernetes.yml`) assumes we will be activating the Web Log as well as using a GitHub backed repository to store JinjaFx DataTemplates.
+
+Once you have updated `kubernetes.yml` with your deployment specific values you would typically perform the following steps:
+
+### Generate Certificate Signing Request
+
+The following step is used to generate a CSR for your TLS certificiate. The Common Name (CN) isn't actually used as we will be using the "subjectAltName" field as it allows multiple values (you could also use something like Let's Encrypt here, but this is out of scope of this document):
+
+```
+openssl req -nodes -newkey rsa:2048 -keyout ingress.key -out ingress.csr -subj "/CN={CN}/emailAddress={emailAddress}/O={O}/L={L}/ST={ST}/C={C}" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:*.{CLUSTER}.{DOMAIN}"))
+```
+
+### Generate TLS Secret with Signed Certificate
+
+Once you have a signed certificate you would create a Kubernetes TLS secret called `ingress-tls` using the private key and signed public certificate:
+
+```
+kubectl create secret tls ingress-tls --cert=ingress.crt --key=ingress.key
+```
+
+### Save Environment Variables as Kubernetes Secrets
+
+To pass the GitHub Token as well as the key used for the Web Log we use Kubernetes secrets that we map into environment variables in the manifest:
+
+```
+kubectl create secret generic jinjafx --from-literal=github-token={TOKEN} --from-literal=jfx-weblog-key={KEY}
+```
+
+### Apply the Kubernetes Manifest
+
+```
+kubectl apply -f kubernetes.yml
+```
+
+If everything has worked then you should be able to point your web browser at `https://jinjafx.{CLUSTER}.{DOMAIN}` and it should present you with JinjaFx.
diff --git a/kubernetes/kubernetes.yml b/kubernetes/kubernetes.yml
new file mode 100644
index 0000000..9706ca4
--- /dev/null
+++ b/kubernetes/kubernetes.yml
@@ -0,0 +1,84 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: jinjafx-ingress
+spec:
+ ingressClassName: public
+ tls:
+ - hosts:
+ - "*.{CLUSTER}.{DOMAIN}"
+ secretName: ingress-tls
+ rules:
+ - host: "jinjafx.{CLUSTER}.{DOMAIN}"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: jinjafx-service
+ port:
+ number: 8080
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: jinjafx-service
+spec:
+ selector:
+ app: jinjafx
+ ports:
+ - protocol: TCP
+ port: 8080
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: jinjafx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: jinjafx
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxUnavailable: 0
+ maxSurge: 1
+ template:
+ metadata:
+ labels:
+ app: jinjafx
+ spec:
+ containers:
+ - name: jinjafx
+ image: docker.io/cmason3/jinjafx_server:latest
+ args: ["-pandoc", "-github", "{OWNER}/{REPO}", "-weblog"]
+ ports:
+ - containerPort: 8080
+ readinessProbe:
+ httpGet: { scheme: HTTP, port: 8080, path: /ping }
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 5
+ livenessProbe:
+ httpGet: { scheme: HTTP, port: 8080, path: /ping }
+ periodSeconds: 30
+ timeoutSeconds: 5
+ env:
+ - name: TZ
+ value: "Europe/London"
+ - name: GITHUB_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: jinjafx
+ key: github-token
+ - name: JFX_WEBLOG_KEY
+ valueFrom:
+ secretKeyRef:
+ name: jinjafx
+ key: jfx-weblog-key
+
diff --git a/podman/README.md b/podman/README.md
deleted file mode 100644
index 0d31ba2..0000000
--- a/podman/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## Rootless Podman for JinjaFx Server
-
-JinjaFx Server will always be available in Docker Hub at [https://hub.docker.com/repository/docker/cmason3/jinjafx_server](https://hub.docker.com/repository/docker/cmason3/jinjafx_server) - the `latest` tag will always refer to the latest released version.
-
-The following commands will launch a container for JinjaFx Server which listens on localhost on port 8080.
-
-Rootless Podman has two methods of running, either with root inside the container, which is mapped to the current non-root user outside the container, or if we use `UserNS=keep-id` then we use the current outside non-root user inside the container as well. Running with non-root inside and non-root outside is always preferred from a security perspective and as JinjaFx Server doesn't require root priviledges this is what this does.
-
-For ease of logging, we will also create a persistent logfile outside of the container and will give the local user access to it via a mapped volume (as we are using a persistent logfile we will also disable logging inside the container using `LogDriver=none`).
-
-```
-mkdir ~/logs
-touch ~/logs/jinjafx.log
-```
-
-The following commands require Podman v4.5 or higher and use the new quadlets method of deploying containers via systemd. We are also passing through the `JFX_WEBLOG_KEY` environment variable that we store as a Podman Secret.
-
-```
-printf | podman secret create jfx_weblog_key -
-
-curl https://raw.githubusercontent.com/cmason3/jinjafx_server/main/podman/jinjafx.container \
- -Os --create-dirs --output-dir ~/.config/containers/systemd
-
-systemctl --user daemon-reload
-
-systemctl --user start jinjafx
-```
-
-This will then run the 'jinjafx' container via systemd (and restart on reboot) as the current non-root user. You should be able to point your browser at http://127.0.0.1:8080 and it will be passed through to the JinjaFx Server (although the preferred approach is running HAProxy in front of JinjaFx Server so it can deal with TLS termination with HTTP/2 or HTTP/3).
diff --git a/podman/jinjafx.container b/podman/jinjafx.container
deleted file mode 100644
index fb970ad..0000000
--- a/podman/jinjafx.container
+++ /dev/null
@@ -1,21 +0,0 @@
-[Unit]
-Description=JinjaFx Server
-
-[Container]
-ContainerName=jinjafx
-Image=docker.io/cmason3/jinjafx_server:latest
-Volume=${HOME}/logs/jinjafx.log:/var/log/jinjafx.log:Z
-Secret=jfx_weblog_key,type=env,target=JFX_WEBLOG_KEY
-Exec=-weblog -pandoc -logfile /var/log/jinjafx.log
-PublishPort=127.0.0.1:8080:8080
-DropCapability=all
-Timezone=local
-LogDriver=none
-UserNS=keep-id
-
-[Service]
-TimeoutStartSec=300
-Restart=always
-
-[Install]
-WantedBy=multi-user.target default.target
diff --git a/www/logs.css b/www/logs.css
new file mode 100644
index 0000000..2547c55
--- /dev/null
+++ b/www/logs.css
@@ -0,0 +1,13 @@
+body {
+ color: white;
+ background: #000040;
+}
+pre {
+ height: 100%;
+ font-family: 'Fira Code', monospace;
+ font-size: 14px;
+ font-variant-ligatures: none;
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-y: hidden;
+}
diff --git a/www/logs.html b/www/logs.html
index 859f669..bdcdf14 100644
--- a/www/logs.html
+++ b/www/logs.html
@@ -8,65 +8,8 @@
-
-
+
+
diff --git a/www/logs.js b/www/logs.js
new file mode 100644
index 0000000..1b027df
--- /dev/null
+++ b/www/logs.js
@@ -0,0 +1,42 @@
+(function() {
+ let interval = 60;
+
+ function scroll() {
+ let e = document.getElementById('container');
+ e.scrollTop = e.scrollHeight;
+ }
+
+ function update() {
+ var xHR = new XMLHttpRequest();
+ xHR.open("GET", '/logs?raw', true);
+
+ xHR.onload = function() {
+ if (this.status == 200) {
+ document.getElementById('container').innerHTML = xHR.responseText;
+ scroll();
+ setTimeout(update, interval * 1000);
+ }
+ else {
+ document.getElementById('container').innerHTML = 'HTTP ERROR ' + this.status;
+ }
+ };
+
+ xHR.onerror = function() {
+ document.getElementById('container').innerHTML = 'XMLHttpRequest ERROR';
+ setTimeout(update, interval * 1000);
+ };
+
+ xHR.ontimeout = function() {
+ document.getElementById('container').innerHTML = 'XMLHttpRequest TIMEOUT';
+ setTimeout(update, interval * 1000);
+ };
+
+ xHR.timeout = 3000;
+ xHR.send();
+ }
+
+ window.onresize = scroll;
+ window.onload = function() {
+ update();
+ };
+})();