kadet-hub logokadet-hub

Mattermost on Kubernetes without Helm, with Kapitan

Mattermost Team Edition 12.0.2, a team chat server with channels, direct messages and file sharing, with its files on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from Mattermost’s Helm chart mattermost-team-edition 6.6.111. The database is the included recipe postgresql.

What it deploys

Required parameters

applications.mattermost.admin.username, applications.mattermost.admin.email and applications.mattermost.admin.password are the first admin’s account. Set the password as a ref with the backend of your choice. Mattermost requires at least 8 characters. Without one of the parameters the compile stops with Interpolation key '<key>' not found. postgresql requires no parameter.

Include

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

classes:
  - recipes.mattermost
parameters:
  namespace: mattermost
  kapitan:
    vars:
      target: mattermost
  applications:
    mattermost:
      admin:
        username: admin
        email: admin@example.com
        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/mattermost:1.0.0@sha256:398f87bf0a7f9ff5bdff6bd3a4ec25c852db9beee801808e2b0c37c48390ca16
        output_path: inventory/classes/recipes/mattermost

Version 1.0.0 includes postgresql 1.1.1. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:

tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9
mkdir -p inventory/classes/recipes/postgresql
tar -xf "$tmp/postgresql.tar" -C inventory/classes/recipes/postgresql --no-same-owner

The first command fetches the recipe into inventory/classes/recipes/mattermost, 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 and of the recipes it includes:

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

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9 \
  --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.mattermost-mattermost-team-edition, <s> for generators.kubernetes.secrets.

Chart value Inventory
image.tag <c>.image, tag and digest
config <s>.mattermost-mattermost-team-edition-config.string_data.<variable>.value, such as MM_SERVICESETTINGS_SITEURL
extraEnvVars <c>.env
persistence.data.size generators.kubernetes.persistent_volume_claims.mattermost-mattermost-team-edition.storage, 10Gi
persistence.plugins.size generators.kubernetes.persistent_volume_claims.mattermost-mattermost-team-edition-plugins.storage, 1Gi
persistence.*.storageClass storage_class next to storage
service.type generators.kubernetes.services.mattermost-team-edition.type, ClusterIP
resources <c>.resources
nodeSelector, tolerations, affinity <c>.node_selector, <c>.tolerations, <c>.affinity
externalDB.externalConnectionString <s>.mattermost-mattermost-team-edition-mattermost-dbsecret.string_data.mattermost.dbsecret.value, from applications.postgresql

Mattermost keeps its configuration in the database. A variable MM_<SECTION>_<SETTING> in the Secret mattermost-mattermost-team-edition-config overrides the setting and locks it in the System Console. Set MM_SERVICESETTINGS_SITEURL to the URL users reach Mattermost at. Without it Mattermost logs at start that some features will operate incorrectly. The pod annotation checksum/config is the chart’s hash of that Secret for the values of tests/values.yaml. It does not follow the target’s values, so after changing the Secret restart the Deployment with kubectl rollout restart.

Both claims are ReadWriteOnce, and the Deployment rolls out a new pod before the old one stops. On a cluster where the new pod lands on another node it waits for the claim until the old pod is gone. The chart’s Ingress, HTTPRoute and MySQL subchart are not part of the recipe.

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 mattermost --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests, with cnpg installed first. The objects of postgresql are left out of the comparison and applied with the recipe. The Deployment and the Service select on name, the chart on app.kubernetes.io/name, instance and managed-by, and both select the same pods. The container is named after the component, the chart’s mattermost-team-edition. The pod sets automountServiceAccountToken: true, the API default the chart leaves unset. The chart binds no role. Its Helm test Pod/mattermost-mattermost-team-edition-test is left out. tests/values.yaml adds the Job mattermost-admin through the chart’s extraDeploy, as a user would next to the chart. The Secret mattermost-admin with its password is only in the recipe, and the chart’s side reads it. tests/target.yml runs one PostgreSQL instance.

tests/smoke checks that the pods pass Pod Security restricted and that /api/v4/system/ping answers through the Service. It waits for the Job mattermost-admin and logs in as its admin. Through the API the client configuration names the image tag and a Team Edition build, and a sign-up without an invitation gets 403. The admin creates a team, a channel and a second user, and posts a message with a file. The second user reads both, and reads them again after the test deleted the pod and its successor is ready, which needs the database and the claim. The test also runs the Job again, which completes without a new account, and the admin still logs in.

Versions

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