Skip to content
Open
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
36 changes: 36 additions & 0 deletions applications/samples/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ paths:
operationId: ping
summary: test the application is up
x-openapi-router-controller: samples.controllers.test_controller

/write-file:
post:
tags:
- test
requestBody:
description: Optional content of the file to write.
required: false
content:
application/json:
schema:
type: object
properties:
content:
type: string
responses:
"200":
content:
application/json:
schema:
type: object
properties:
filename:
type: string
path:
type: string
hostname:
type: string
description: The file was written on the application volume
operationId: write_file
summary: writes a file on the application volume
description: >-
Writes a timestamped file on the application volume and returns the name of the pod
that handled the request. On a statefulset deployment, route this endpoint to the
leader service to have all writes land on pod 0.
x-openapi-router-controller: samples.controllers.test_controller
/valid:
get:
tags:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,43 @@ def ping(): # noqa: E501
return time.time()


def write_file(body=None): # noqa: E501
"""writes a file on the application volume

# noqa: E501

:param body: Optional content of the file to write.
:type body: dict | bytes

:rtype: dict
"""
import os
import socket
import time

from flask import has_request_context, request

if body is None and has_request_context() and request.is_json:
body = request.get_json()

try:
from cloudharness.applications import get_current_configuration
mountpath = get_current_configuration().harness.deployment.volume.mountpath
except Exception:
# not running inside a cloudharness deployment (e.g. unit tests)
mountpath = "/tmp/myvolume"
Comment on lines +51 to +56

hostname = socket.gethostname()
filename = f"{time.strftime('%Y%m%d-%H%M%S')}-{hostname}.txt"
path = os.path.join(mountpath, filename)
content = (body or {}).get("content") or f"written by {hostname}"

os.makedirs(mountpath, exist_ok=True)
with open(path, "w") as f:
f.write(content)

return {"filename": filename, "path": path, "hostname": hostname}


def serialization():
return User(last_name="Last", first_name="First")
18 changes: 18 additions & 0 deletions applications/samples/backend/samples/test/test_sample.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
def test_sample():
assert True


def test_write_file():
import socket

from samples.controllers import test_controller

# outside a deployment the controller falls back to writing under /tmp/myvolume
result = test_controller.write_file({"content": "hello"})

assert result["hostname"] == socket.gethostname()
assert result["hostname"] in result["filename"]
with open(result["path"]) as f:
assert f.read() == "hello"

result = test_controller.write_file()
with open(result["path"]) as f:
assert socket.gethostname() in f.read()
4 changes: 4 additions & 0 deletions applications/samples/deploy/values-statefulset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
harness:
deployment:
statefulset: true
replicas: 2
5 changes: 5 additions & 0 deletions applications/samples/deploy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ harness:
use_services:
- name: common
deployment:
replicas: 2
statefulset: true
volume:
name: my-shared-volume
mountpath: /tmp/myvolume
Expand Down Expand Up @@ -48,6 +50,9 @@ harness:
white-listed: true
- uri: /api/ping
white-listed: true
- uri: /api/write-file
methods: [POST]
white-listed: true
- uri: /api/openapi.json
white-listed: true
- uri: /icon.png
Expand Down
60 changes: 57 additions & 3 deletions deployment-configuration/helm/templates/auto-database.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,55 @@ spec:
{{- if and (eq .app.harness.database.type "postgres") (dig "postgres" "operator" false .app.harness.database) }}
{{- include "deploy_utils.database.postgres.operator" . }}
{{- else }}
{{- $isStatefulSet := .app.harness.database.statefulset | default false }}
{{- /* A statefulset database owns its volume through volumeClaimTemplates. A PVC named after the
database is the leftover of a previous Deployment: it is kept (never deleted by helm) and
its data is copied into the statefulset volume by a migration job (see
auto-volume-migration.yaml). Delete the legacy PVC once migrated. */}}
{{- $legacyClaim := false }}
{{- if $isStatefulSet }}
{{- if lookup "v1" "PersistentVolumeClaim" .root.Values.namespace .app.harness.database.name }}
{{- $legacyClaim = true }}
{{- end }}
{{- end }}
{{- if or (not $isStatefulSet) $legacyClaim }}
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ .app.harness.database.name | quote }}
namespace: {{ .root.Values.namespace }}
{{- if $legacyClaim }}
annotations:
helm.sh/resource-policy: keep
{{- end }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .app.harness.database.size }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
kind: {{ ternary "StatefulSet" "Deployment" $isStatefulSet }}
metadata:
name: {{ .app.harness.database.name | quote }}
namespace: {{ .root.Values.namespace }}
labels:
usesvolume: {{ .app.harness.database.name }}-{{ .root.Values.namespace }}
spec:
{{- if $isStatefulSet }}
# StatefulSet updates terminate the old pod before creating its replacement, so no
# Recreate strategy or node pinning is needed.
serviceName: {{ .app.harness.database.name | quote }}
{{- else }}
# The database's ReadWriteOnce volume attaches to a single node and the pod is
# pinned to it via podAffinity. Recreate terminates the old pod before starting
# the new one, avoiding an unschedulable pod when the node can't fit both.
strategy:
type: Recreate
{{- end }}
replicas: 1
selector:
matchLabels:
Expand All @@ -54,6 +77,7 @@ spec:
service: db
usesvolume: {{ .app.harness.database.name }}-{{ .root.Values.namespace }}
spec:
{{- if not $isStatefulSet }}
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
Expand All @@ -65,6 +89,18 @@ spec:
- {{ .app.harness.database.name }}-{{ .root.Values.namespace }}
- {{ .app.harness.database.name }} # Obsolete, kept for backwards compatability, to be removed at a later date
topologyKey: "kubernetes.io/hostname"
{{- end }}
{{- if $legacyClaim }}
initContainers:
# Holds the pod until the volume-migration job has copied the legacy volume data into
# the statefulset volume. Delete the legacy PVC after verifying the migration.
- name: volume-migration
image: busybox:1.36
command: ['sh', '-c', 'until [ -f /migration/target/.cloudharness-volume-migrated ]; do echo "waiting for the volume migration job"; sleep 5; done']
volumeMounts:
- name: {{ .app.harness.database.name | quote }}
mountPath: /migration/target
{{- end }}
containers:
- name: {{ .app.harness.database.name | quote }}
imagePullPolicy: IfNotPresent
Expand All @@ -82,15 +118,33 @@ spec:
- mountPath: /dev/shm
name: dshm
{{- end }}
{{- if or (not $isStatefulSet) (eq .app.harness.database.type "postgres") }}
volumes:
{{- if not $isStatefulSet }}
- name: {{ .app.harness.database.name | quote }}
persistentVolumeClaim:
claimName: {{ .app.harness.database.name | quote }}
{{- end }}
{{- if eq .app.harness.database.type "postgres" }}
- emptyDir:
medium: Memory
name: dshm
{{- end }}
name: dshm
{{- end }}
{{- end }}
{{- if $isStatefulSet }}
volumeClaimTemplates:
- metadata:
name: {{ .app.harness.database.name | quote }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .app.harness.database.size }}
{{- end }}
{{- if $legacyClaim }}
{{- include "deploy_utils.volumeMigration" (dict "root" .root "name" .app.harness.database.name "pvc" .app.harness.database.name) }}
{{- end }}
Comment on lines +144 to +147
---
{{- if and .root.Values.backup.active (ne .app.harness.database.type "neo4j") }}
{{- include (print "deploy_utils.database." .app.harness.database.type ".backup") . }}
Expand Down
75 changes: 62 additions & 13 deletions deployment-configuration/helm/templates/auto-deployments.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
{{- define "deploy_utils.deployment" }}
{{- $isStatefulSet := .app.harness.deployment.statefulset | default false }}
{{- $volume := .app.harness.deployment.volume }}
{{- /* $rwoVolume: a ReadWriteOnce (non-nfs) volume pins the pod to a single node — it drives both
the Recreate strategy and the podAffinity. $ownVolume additionally requires the PVC to be
managed here (auto), which a statefulset turns into a volumeClaimTemplate. The checks are
nested so hasKey/index never runs on a nil volume: helm < 3.10 (go < 1.18) does not
short-circuit 'and'/'or', so a flat expression would fail for volume-less apps. */}}
{{- $rwoVolume := false }}
{{- $ownVolume := false }}
{{- if $volume }}
{{- if or (not (hasKey $volume "usenfs")) (not $volume.usenfs) }}
{{- $rwoVolume = true }}
{{- if or (not (hasKey $volume "auto")) $volume.auto }}
{{- $ownVolume = true }}
{{- end }}
{{- end }}
{{- end }}
{{- $useClaimTemplate := and $isStatefulSet $ownVolume }}
{{- /* A PVC named after the volume is the leftover of a previous Deployment: a migration job
(see auto-volume-migration.yaml) copies its data into each statefulset volume, and an init
container holds each pod until its volume has been migrated. Delete the legacy PVC once
migrated to end the migration. */}}
{{- $legacyClaim := false }}
{{- if $useClaimTemplate }}
{{- if lookup "v1" "PersistentVolumeClaim" .root.Values.namespace $volume.name }}
{{- $legacyClaim = true }}
{{- end }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
kind: {{ ternary "StatefulSet" "Deployment" $isStatefulSet }}
metadata:
name: {{ .app.harness.deployment.name| quote }}
namespace: {{ .root.Values.namespace }}
Expand All @@ -11,18 +39,18 @@ metadata:
{{- end }}
{{- include "deploy_utils.labels" .root | indent 4 }}
spec:
{{- if .app.harness.deployment.volume }}
{{- if or (not (hasKey .app.harness.deployment.volume "usenfs")) (not .app.harness.deployment.volume.usenfs) }}
{{- if $isStatefulSet }}
# StatefulSet updates terminate the old pod before creating its replacement, so no
# Recreate strategy or node pinning is needed even for ReadWriteOnce volumes.
serviceName: {{ .app.harness.service.name | quote }}
{{- else if $rwoVolume }}
# A ReadWriteOnce volume attaches to a single node and the pod is pinned to the
# volume's node via podAffinity (see below). Recreate terminates the old pod
# before starting the new one, avoiding an unschedulable pod when the node can't
# fit both. NFS (ReadWriteMany) volumes have no pinning and can roll normally.
# Nested ifs (not a flat 'and'): helm < 3.10 (go < 1.18) does not short-circuit,
# so hasKey must not run on a nil volume.
strategy:
type: Recreate
{{- end }}
{{- end }}
replicas: {{ .app.harness.deployment.replicas | default 1 }}
selector:
matchLabels:
Expand All @@ -47,8 +75,7 @@ spec:
- name: {{ .root.Values.registry.secret.name }}
{{- end }}
{{- end }}
{{- if and .app.harness.deployment.volume }}
{{- if or (not (hasKey .app.harness.deployment.volume "usenfs")) (not .app.harness.deployment.volume.usenfs) }}
{{- if and $rwoVolume (not $isStatefulSet) }}
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
Expand All @@ -61,23 +88,30 @@ spec:
- {{ .app.harness.deployment.volume.name }} # Obsolete, kept for backwards compatability, to be removed at a later date
topologyKey: "kubernetes.io/hostname"
{{- end }}
{{- end }}
{{- if .app.harness.deployment.extraContainers }}
{{- $hasInit := false }}
{{- $hasInit := $legacyClaim }}
{{- range $name, $container := .app.harness.deployment.extraContainers }}
{{- if and $container.auto $container.initContainer }}
{{- $hasInit = true }}
{{- end }}
{{- end }}
{{- if $hasInit }}
initContainers:
{{- if $legacyClaim }}
# Holds the pod until the volume-migration job has copied the legacy volume data into
# this pod's volume. Delete the legacy PVC after verifying the migration to drop this gate.
- name: volume-migration
image: busybox:1.36
command: ['sh', '-c', 'until [ -f /migration/target/.cloudharness-volume-migrated ]; do echo "waiting for the volume migration job"; sleep 5; done']
volumeMounts:
- name: {{ $volume.name }}
mountPath: /migration/target
{{- end }}
{{- range $name, $container := .app.harness.deployment.extraContainers }}
{{- if and $container.auto $container.initContainer }}
{{- include "deploy_utils.extraContainerSpec" (dict "name" $name "container" $container "app" $.app "root" $.root) | nindent 6 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
containers:
- name: {{ .app.harness.deployment.name| default "cloudharness-docs" | quote }}
image: {{ .app.harness.deployment.image }}
Expand Down Expand Up @@ -152,7 +186,7 @@ spec:
secretName: {{ $dep }}
{{- end }}
{{- end }}
{{- if .app.harness.deployment.volume }}
{{- if and .app.harness.deployment.volume (not $useClaimTemplate) }}
- name: {{ .app.harness.deployment.volume.name }}
persistentVolumeClaim:
claimName: {{ .app.harness.deployment.volume.name }}
Expand All @@ -175,6 +209,21 @@ spec:
secretName: {{ printf "%s-db" .app.harness.deployment.name }}
{{- end }}
{{- end }}
{{- if $useClaimTemplate }}
volumeClaimTemplates:
- metadata:
name: {{ $volume.name }}
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: {{ $volume.size }}
{{- end }}
{{- if $legacyClaim }}
{{- include "deploy_utils.volumeMigration" (dict "root" .root "name" .app.harness.deployment.name "pvc" $volume.name) }}
{{- end }}
Comment on lines +223 to +226
---
{{- end }}

Expand Down
Loading
Loading