kadet-hub logokadet-hub

Stirling-PDF on Kubernetes without Helm, with Kapitan

Stirling-PDF 3.2.0, a web UI and HTTP API that merges, splits, converts and edits PDF files, with its settings and users on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from the project’s Helm chart stirling-pdf-chart 4.0.0.

What it deploys

Required parameters

applications.stirling-pdf.admin.username and applications.stirling-pdf.admin.password are the login and password of the first admin. The recipe puts them into the Secret stirling-pdf-admin, from which the container reads SECURITY_INITIALLOGIN_USERNAME and SECURITY_INITIALLOGIN_PASSWORD. Stirling-PDF creates the account on its first start, while its database on the claim holds no user, and does not create the default admin with the password stirling then. A later change of either parameter does not reach the account. Change the password in the web UI instead.

Set the password to a ref with the backend of your choice. Without either parameter the compile stops with Interpolation key '<key>' not found.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.stirling-pdf
parameters:
  namespace: stirling-pdf
  kapitan:
    vars:
      target: stirling-pdf
  applications:
    stirling-pdf:
      admin:
        username: <login>
        password: <ref>

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.0:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/stirling-pdf:1.0.0@sha256:095a0a26d9e5b800bbd520d2a09d98b31ef8e7b440dcfccc778afdec45e217bc
        output_path: inventory/classes/recipes/stirling-pdf

The first command fetches the recipe into inventory/classes/recipes/stirling-pdf, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/stirling-pdf@sha256:095a0a26d9e5b800bbd520d2a09d98b31ef8e7b440dcfccc778afdec45e217bc \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

<c> stands for components.stirling-pdf-stirling-pdf-chart.

Chart value Inventory
image.tag <c>.image, tag and digest
envs <c>.env, a mapping of name to value
persistence.size <c>.persistent_volume_claims.stirling-pdf-stirling-pdf-chart.storage
persistence.storageClass <c>.persistent_volume_claims.stirling-pdf-stirling-pdf-chart.storage_class
resources <c>.resources
service.type <c>.service.type
rootPath <c>.env.SYSTEM_ROOTURIPATH and the path of each probe in <c>.healthcheck
nodeSelector, tolerations, affinity <c>.node_selector, <c>.tolerations, <c>.affinity
deployment.preStopSleepSeconds, .terminationGracePeriodSeconds <c>.lifecycle.preStop.exec.command, <c>.grace_period
probes.startup.failureThreshold <c>.healthcheck.startup.failure_threshold

The chart’s resources are empty. Its README asks for a memory limit, because the image sizes the Java heap from it. Without one the heap may grow to 70% of the node’s memory.

Running as stirlingpdfuser, the image cannot start LibreOffice as its second user stirlingofficeuser, which needs root. LibreOffice then runs as stirlingpdfuser in the image’s Landlock and seccomp sandbox.

The chart’s Ingress, HTTPRoute, ServiceMonitor, PodDisruptionBudget, HorizontalPodAutoscaler and cluster mode with PostgreSQL, Valkey, S3 and a licence are off by default and not part of the recipe. The ServiceAccount has no role.

Any list set in the target is appended to the recipe’s list. To replace one, set the key to ~ in a class included after the recipe and the new list in the target.

Proof

check stirling-pdf --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests. The four objects differ in three ways. The Deployment and the Service select on name, the chart on app.kubernetes.io/name and instance, and both select the same pods. The container is named after the component, the chart’s stirling-pdf-chart. The ServiceAccount sets automountServiceAccountToken: true, the API default the chart leaves unset, while the pod opts out as in the chart. The Secret stirling-pdf-admin is only in the recipe, accepted as a Referenced Secret: the chart’s container reads exactly its keys username and password through envs. The chart binds no role.

tests/smoke checks that the pod passes Pod Security restricted and that /api/v1/info/status reports the image tag through the Service. The merge endpoint and the sign-up answer 401 without a login, and so does a login as admin with stirling. As the admin of tests/target.yml it merges two one-page PDFs that the test writes, and qpdf --check and pdftotext in the image find a valid file with both pages in order. The test changes the setting ui.appNameNavbar through the admin API, deletes the pod, and reads the new value from its successor, which loads it from settings.yml on the claim.

Versions

The recipe's files are in kadet-hub/recipes. All recipes