Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions charts/outprint/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ sources:
- https://github.com/taskmedia/helm
icon: https://media.task.media/images/logo.png
type: application
version: 1.1.1
version: 1.2.0
# outprint release tag without the leading v
appVersion: "1.0.1"

Expand All @@ -32,5 +32,7 @@ annotations:
- name: outprint-injector
image: ghcr.io/taskmedia/outprint-injector:1.0.1
artifacthub.io/changes: |
- kind: changed
description: Bump Outprint to 1.0.1
- kind: added
description: packages.git, packages.image and packages.existingClaim mount your own Template and Configuration packages
- kind: added
description: extraVolumes and extraVolumeMounts for Gateway and Worker
34 changes: 34 additions & 0 deletions charts/outprint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,37 @@ ingressRoute:
```

Needs `injector.enabled` and the Traefik (and, for the certificate, cert-manager) CRDs in the cluster.

## Your own Templates and Configurations

The chart ships `neutral` and `example` in the image. To use your own packages, set one source under
`packages` (documentation: Install > Run Outprint > Your own Templates and Configurations):

```yaml
packages:
git:
url: git@github.com:example/letters.git
ref: v1.0.0 # tag, branch or commit
path: outprint # folder with templates/ and configurations/
sshKeySecret: letters-deploy-key # key `ssh-privatekey`, optional `known_hosts`
serviceConfig:
bindings:
- template: letter
configuration: acme
defaultBinding:
template: letter
configuration: acme
```

With `git`, a Job (at install and on a change of `url`, `ref`, `commit` or `path`) and a CronJob (every
`packages.git.interval`) resolve `ref` to a commit. On a change they clone it (Git LFS included), run
`outprint validate` against the service configuration and switch over only if it passes; then they restart
Gateway and Worker. A broken revision never replaces a working one. `packages.git.commit` refuses any other
commit. Only the Job has network access; the Worker keeps its NetworkPolicy. The revisions live on a
`ReadWriteOnce` PVC, so on a multi-node cluster pin Gateway, Worker and the Job to one node or use a
ReadWriteMany storage class. When a change adds a Binding that the running revision does not have yet, the
Gateway and the Worker restart until the Job has switched over.

`packages.image` mounts an OCI image as a Kubernetes image volume (Kubernetes 1.35+ or the `ImageVolume`
feature gate); `packages.existingClaim` mounts a PVC you maintain. `gateway.extraVolumes` /
`gateway.extraVolumeMounts` (and the same under `worker`) cover everything else.
25 changes: 25 additions & 0 deletions charts/outprint/files/packages-activate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Switch /packages/current to the validated revision, then restart Gateway and
# Worker so they load it.
set -eu

PK=/packages
[ -f "$PK/.pending" ] || { echo "no change"; exit 0; }
sha="$(cat "$PK/.pending")"
had_revision=false
[ -f "$PK/.revision" ] && had_revision=true

ln -sfn "rev-$sha" "$PK/.current.tmp"
mv -T "$PK/.current.tmp" "$PK/current"
printf '%s\n' "$sha" >"$PK/.revision"
rm -f "$PK/.pending"
echo "activated $sha"

# newest first; the active revision is the newest
ls -dt "$PK"/rev-* | tail -n +"$((KEEP_REVISIONS + 2))" | xargs -r rm -rf

# On the first sync Gateway and Worker are still waiting for the packages.
if [ "$had_revision" = true ]; then
for d in $DEPLOYMENTS; do
kubectl --namespace "$NAMESPACE" rollout restart "deployment/$d"
done
fi
71 changes: 71 additions & 0 deletions charts/outprint/files/packages-sync.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Resolve GIT_REF to a commit and, if it differs from the active one, check it
# out (Git LFS included) as /packages/rev-<commit>. Leaves /packages/.pending
# for the validate and activate steps. Never touches the active revision.
set -eu

PK=/packages
rm -f "$PK/.pending"

apk add --no-cache git-lfs openssh-client >/dev/null

if [ -f /ssh/ssh-privatekey ]; then
install -m 600 /ssh/ssh-privatekey /tmp/id
check=accept-new
if [ -f /ssh/known_hosts ]; then
cp /ssh/known_hosts /tmp/known_hosts
check=yes
fi
export GIT_SSH_COMMAND="ssh -i /tmp/id -o IdentitiesOnly=yes -o StrictHostKeyChecking=$check -o UserKnownHostsFile=/tmp/known_hosts"
fi

if printf '%s' "$GIT_REF" | grep -Eq '^[0-9a-f]{40}$'; then
sha="$GIT_REF"
else
sha=""
for candidate in "refs/tags/$GIT_REF^{}" "refs/tags/$GIT_REF" "refs/heads/$GIT_REF"; do
sha="$(git ls-remote "$GIT_URL" "$candidate" | awk 'NR==1 {print $1}')"
[ -n "$sha" ] && break
done
fi
if [ -z "$sha" ]; then
echo "cannot resolve ref '$GIT_REF' in $GIT_URL" >&2
exit 1
fi
echo "ref '$GIT_REF' resolves to $sha"

if [ -n "$GIT_COMMIT" ]; then
case "$sha" in
"$GIT_COMMIT"*) ;;
*) echo "ref resolves to $sha, packages.git.commit expects $GIT_COMMIT: keeping the active revision" >&2; exit 1 ;;
esac
fi

if [ "$sha" = "$(cat "$PK/.revision" 2>/dev/null || true)" ] && [ -d "$PK/current/templates" ]; then
echo "revision $sha is already active"
exit 0
fi

rm -rf "$PK/.work" "$PK/rev-$sha"
mkdir "$PK/.work"
cd "$PK/.work"
git init -q .
git remote add origin "$GIT_URL"
git lfs install --local >/dev/null
git fetch -q --depth 1 origin "$sha"
git checkout -q FETCH_HEAD
git lfs pull

src="$PK/.work${PKG_PATH:+/$PKG_PATH}"
for dir in templates configurations; do
if [ ! -d "$src/$dir" ]; then
echo "$src/$dir not found: set packages.git.path to the folder with templates/ and configurations/" >&2
exit 1
fi
done
mkdir "$PK/rev-$sha"
cp -R "$src/templates" "$src/configurations" "$PK/rev-$sha/"
cd "$PK"
rm -rf "$PK/.work"
chmod -R a+rX "$PK/rev-$sha"
printf '%s\n' "$sha" >"$PK/.pending"
chmod a+r "$PK/.pending"
9 changes: 9 additions & 0 deletions charts/outprint/files/packages-validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Check the pending revision with the same code the Gateway runs at start-up.
# With a service configuration the roots come from the environment.
set -eu

[ -f /packages/.pending ] || { echo "nothing to validate"; exit 0; }
sha="$(cat /packages/.pending)"
export OUTPRINT_TEMPLATES_ROOT="/packages/rev-$sha/templates"
export OUTPRINT_CONFIGURATIONS_ROOT="/packages/rev-$sha/configurations"
exec outprint validate --service-config /etc/outprint/service.yaml
85 changes: 85 additions & 0 deletions charts/outprint/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,88 @@ runAsGroup: {{ .uid }}
seccompProfile:
type: RuntimeDefault
{{- end -}}

{{/* The packages source in use: git, image, existingClaim or empty. At most one may be set. */}}
{{- define "outprint.packagesSource" -}}
{{- $sources := list -}}
{{- if .Values.packages.git.url }}{{ $sources = append $sources "git" }}{{ end -}}
{{- if .Values.packages.image.reference }}{{ $sources = append $sources "image" }}{{ end -}}
{{- if .Values.packages.existingClaim }}{{ $sources = append $sources "existingClaim" }}{{ end -}}
{{- if gt (len $sources) 1 -}}
{{- fail (printf "packages: set only one source, got %s" (join ", " $sources)) -}}
{{- end -}}
{{- first (append $sources "") -}}
{{- end -}}

{{/* Folder that holds templates/ and configurations/ inside the packages mount. */}}
{{- define "outprint.packagesRoot" -}}
{{- if eq (include "outprint.packagesSource" .) "git" -}}
/packages/current
{{- else -}}
{{- printf "/packages/%s" (trimAll "/" .Values.packages.path) | trimSuffix "/" -}}
{{- end -}}
{{- end -}}

{{- define "outprint.packagesClaimName" -}}
{{- default (printf "%s-packages" (include "outprint.fullname" .)) .Values.packages.existingClaim -}}
{{- end -}}

{{/* Env for Gateway and Worker; the env vars win over the service config file. */}}
{{- define "outprint.packagesEnv" -}}
{{- if include "outprint.packagesSource" . }}
- name: OUTPRINT_TEMPLATES_ROOT
value: {{ printf "%s/templates" (include "outprint.packagesRoot" .) | quote }}
- name: OUTPRINT_CONFIGURATIONS_ROOT
value: {{ printf "%s/configurations" (include "outprint.packagesRoot" .) | quote }}
{{- end }}
{{- end -}}

{{- define "outprint.packagesVolumeMount" -}}
{{- if include "outprint.packagesSource" . }}
- { name: packages, mountPath: /packages, readOnly: true }
{{- end }}
{{- end -}}

{{- define "outprint.packagesVolume" -}}
{{- $source := include "outprint.packagesSource" . -}}
{{- if eq $source "image" }}
- name: packages
image:
reference: {{ .Values.packages.image.reference | quote }}
pullPolicy: {{ .Values.packages.image.pullPolicy }}
{{- else if $source }}
- name: packages
persistentVolumeClaim:
claimName: {{ include "outprint.packagesClaimName" . }}
readOnly: true
{{- end }}
{{- end -}}

{{/* Gateway and Worker wait here until the first sync has produced packages. */}}
{{- define "outprint.packagesWait" -}}
{{- if eq (include "outprint.packagesSource" .) "git" }}
initContainers:
- name: wait-for-packages
image: {{ include "outprint.image" (dict "repository" .Values.image.repository "tag" .Values.image.tag "appVersion" .Chart.AppVersion) | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sh", "-c", "until [ -d /packages/current/templates ] && [ -d /packages/current/configurations ]; do echo waiting for packages; sleep 2; done"]
securityContext:
{{- include "outprint.containerSecurity" . | nindent 6 }}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 100m, memory: 32Mi}
volumeMounts:
- { name: packages, mountPath: /packages, readOnly: true }
{{- end }}
{{- end -}}

{{/* Minutes (5m) and hours (2h) become a cron expression; anything else is taken as one. */}}
{{- define "outprint.packagesSchedule" -}}
{{- if regexMatch "^[0-9]+m$" . -}}
*/{{ trimSuffix "m" . }} * * * *
{{- else if regexMatch "^[0-9]+h$" . -}}
0 */{{ trimSuffix "h" . }} * * *
{{- else -}}
{{ . }}
{{- end -}}
{{- end -}}
96 changes: 96 additions & 0 deletions charts/outprint/templates/_packages-sync.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{{- define "outprint.packagesSyncPod" }}
{{- $git := .Values.packages.git }}
{{- $name := printf "%s-packages" (include "outprint.fullname" .) }}
serviceAccountName: {{ $name }}
restartPolicy: Never
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 2 }}
{{- end }}
securityContext:
seccompProfile:
type: RuntimeDefault
initContainers:
- name: sync
image: {{ printf "%s:%s" $git.image.repository $git.image.tag | quote }}
imagePullPolicy: {{ $git.image.pullPolicy }}
command: ["sh", "/scripts/sync.sh"]
env:
- { name: GIT_URL, value: {{ $git.url | quote }} }
- { name: GIT_REF, value: {{ $git.ref | quote }} }
- { name: GIT_COMMIT, value: {{ $git.commit | quote }} }
- { name: PKG_PATH, value: {{ trimAll "/" $git.path | quote }} }
securityContext:
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID]
resources:
{{- toYaml $git.resources | nindent 6 }}
volumeMounts:
- { name: packages, mountPath: /packages }
- { name: scripts, mountPath: /scripts, readOnly: true }
{{- if $git.sshKeySecret }}
- { name: ssh, mountPath: /ssh, readOnly: true }
{{- end }}
- name: validate
image: {{ include "outprint.image" (dict "repository" .Values.image.repository "tag" .Values.image.tag "appVersion" .Chart.AppVersion) | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sh", "/scripts/validate.sh"]
securityContext:
{{- include "outprint.containerSecurity" . | nindent 6 }}
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {cpu: 500m, memory: 256Mi}
volumeMounts:
- { name: packages, mountPath: /packages, readOnly: true }
- { name: scripts, mountPath: /scripts, readOnly: true }
- { name: service-config, mountPath: /etc/outprint, readOnly: true }
- { name: tmp, mountPath: /tmp }
containers:
- name: activate
image: {{ printf "%s:%s" $git.image.repository $git.image.tag | quote }}
imagePullPolicy: {{ $git.image.pullPolicy }}
command: ["sh", "/scripts/activate.sh"]
env:
- name: NAMESPACE
valueFrom: { fieldRef: { fieldPath: metadata.namespace } }
- { name: DEPLOYMENTS, value: {{ printf "%s-gateway %s-worker" (include "outprint.fullname" .) (include "outprint.fullname" .) | quote }} }
- { name: KEEP_REVISIONS, value: {{ $git.keepRevisions | quote }} }
securityContext:
runAsUser: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
add: [CHOWN, DAC_OVERRIDE, FOWNER]
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 200m, memory: 128Mi}
volumeMounts:
- { name: packages, mountPath: /packages }
- { name: scripts, mountPath: /scripts, readOnly: true }
- { name: tmp, mountPath: /tmp }
volumes:
- name: packages
persistentVolumeClaim:
claimName: {{ $name }}
- name: scripts
configMap:
name: {{ $name }}-sync
- name: service-config
configMap:
name: {{ include "outprint.fullname" . }}-service
- name: tmp
emptyDir: { sizeLimit: 64Mi }
{{- if $git.sshKeySecret }}
- name: ssh
secret:
secretName: {{ $git.sshKeySecret }}
defaultMode: 0400
{{- end }}
{{- end }}
10 changes: 10 additions & 0 deletions charts/outprint/templates/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ spec:
{{- end }}
securityContext:
{{- include "outprint.podSecurity" (dict "uid" 10001) | nindent 8 }}
{{- include "outprint.packagesWait" . | nindent 6 }}
containers:
- name: gateway
image: {{ include "outprint.image" (dict "repository" .Values.image.repository "tag" .Values.image.tag "appVersion" .Chart.AppVersion) | quote }}
Expand All @@ -52,6 +53,7 @@ spec:
value: {{ . | quote }}
{{- end }}
{{- include "outprint.tokenEnv" . | nindent 12 }}
{{- include "outprint.packagesEnv" . | nindent 12 }}
{{- include "outprint.extraEnv" .Values.gateway.env | nindent 12 }}
ports:
- name: http
Expand All @@ -71,10 +73,18 @@ spec:
{{- toYaml .Values.gateway.resources | nindent 12 }}
volumeMounts:
- { name: service-config, mountPath: /etc/outprint, readOnly: true }
{{- include "outprint.packagesVolumeMount" . | nindent 12 }}
{{- with .Values.gateway.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: service-config
configMap:
name: {{ include "outprint.fullname" . }}-service
{{- include "outprint.packagesVolume" . | nindent 8 }}
{{- with .Values.gateway.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.gateway.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
Loading
Loading