mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-13 22:18:27 +09:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d514501d57 | ||
|
|
83bb9d89c8 | ||
|
|
0117605572 | ||
|
|
72e14c17ba | ||
|
|
aa0ebdb97e | ||
|
|
3d4b2933e1 | ||
|
|
922ff5ea64 | ||
|
|
fce3200265 | ||
|
|
3b35db2681 | ||
|
|
c7da0cf33d | ||
|
|
78272521bb | ||
|
|
8e39380ee6 | ||
|
|
fb58447ba1 | ||
|
|
2ffe8f3387 | ||
|
|
4194525933 | ||
|
|
44799dafa2 | ||
|
|
30b32c9a35 | ||
|
|
d1b4b82895 | ||
|
|
9bc43c0c08 | ||
|
|
13960a0bb3 | ||
|
|
64cb159d65 | ||
|
|
d2173fe879 | ||
|
|
49eb8cdd81 | ||
|
|
b4fc1241ae | ||
|
|
54f1b37666 | ||
|
|
8bda151596 | ||
|
|
ab19c0d1ac |
No files matched your search
@@ -0,0 +1,183 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: Helm chart
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- deploy/helm/**
|
||||
- deploy/self-hosting/Caddyfile
|
||||
- .github/workflows/helm.yaml
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- deploy/helm/**
|
||||
- deploy/self-hosting/Caddyfile
|
||||
- .github/workflows/helm.yaml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
render:
|
||||
name: Lint and render with Helm ${{ matrix.helm }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- helm: v3.19.0
|
||||
sha256: a7f81ce08007091b86d8bd696eb4d86b8d0f2e1b9f6c714be62f82f96a594496
|
||||
- helm: v4.2.4
|
||||
sha256: c306b46f719b0a4da32d0f78ee21bf90ce8d602f15b22ab753f0674d1670a7f3
|
||||
env:
|
||||
CHART: deploy/helm/fluxer
|
||||
KUBECONFORM_VERSION: v0.7.0
|
||||
KUBECONFORM_SHA256: c31518ddd122663b3f3aa874cfe8178cb0988de944f29c74a0b9260920d115d3
|
||||
CRD_CATALOG: https://raw.githubusercontent.com/datreeio/CRDs-catalog/63669a570e231d4f1f8396d229a1de512bcf0a34/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
|
||||
steps:
|
||||
- name: Checkout fluxer
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Helm and kubeconform
|
||||
env:
|
||||
HELM_VERSION: ${{ matrix.helm }}
|
||||
HELM_SHA256: ${{ matrix.sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/bin"
|
||||
cd "$RUNNER_TEMP"
|
||||
curl -fsSLo helm.tgz "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
echo "${HELM_SHA256} helm.tgz" | sha256sum -c -
|
||||
tar -xzf helm.tgz linux-amd64/helm
|
||||
mv linux-amd64/helm bin/helm
|
||||
curl -fsSLo kubeconform.tgz "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz"
|
||||
echo "${KUBECONFORM_SHA256} kubeconform.tgz" | sha256sum -c -
|
||||
tar -xzf kubeconform.tgz kubeconform
|
||||
mv kubeconform bin/kubeconform
|
||||
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Lint, render and validate every CI values file
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v helm kubeconform python3
|
||||
python3 -c 'import yaml'
|
||||
helm version --short
|
||||
shopt -s nullglob
|
||||
files=("$CHART"/ci/*.yaml)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "::error::no values files under $CHART/ci"
|
||||
exit 1
|
||||
fi
|
||||
for values in "${files[@]}"; do
|
||||
echo "::group::$values"
|
||||
helm lint --strict "$CHART" -f "$values"
|
||||
helm template fluxer "$CHART" -n fluxer -f "$values" \
|
||||
--api-versions cert-manager.io/v1 --api-versions gateway.networking.k8s.io/v1 > rendered.yaml
|
||||
kubeconform -strict -summary -kubernetes-version 1.29.0 \
|
||||
-schema-location default -schema-location "$CRD_CATALOG" rendered.yaml
|
||||
if grep -qi 'example\.com' rendered.yaml; then
|
||||
echo "::error::$values renders example.com"
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eqi 'change_?me' rendered.yaml; then
|
||||
echo "::error::$values renders a CHANGE_ME placeholder"
|
||||
exit 1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
- name: Check the guards
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expect_failure() {
|
||||
local message=$1
|
||||
shift
|
||||
if output=$(helm template fluxer "$CHART" "$@" 2>&1); then
|
||||
echo "::error::rendered with $*"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qF "$message" <<<"$output"; then
|
||||
echo "::error::expected \"$message\" for $*, got: $output"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
d=domain=chat.example.test
|
||||
expect_failure "domain must be set"
|
||||
expect_failure "is a placeholder" --set $d --set secrets.values.FLUXER_SUDO_MODE_SECRET=CHANGE_ME
|
||||
expect_failure "is a placeholder" --set $d --set-string 'secrets.extra.FLUXER_STRIPE_SECRET_KEY=<change-me>'
|
||||
expect_failure "at least 32 characters" --set $d --set secrets.values.FLUXER_GATEWAY_RPC_AUTH_TOKEN=short
|
||||
expect_failure "letters and digits only" --set $d --set-string 'secrets.values.FLUXER_VALKEY_PASSWORD=0123456789abcdef0123456789abcd:@'
|
||||
expect_failure "worker.replicas must be 1" --set $d --set worker.replicas=2
|
||||
expect_failure "at least 32 bytes" --set $d --set secrets.values.FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=c2hvcnQ=
|
||||
expect_failure "set postgres.external.host" --set $d --set postgres.enabled=false
|
||||
expect_failure "set together" --set $d --set secrets.values.FLUXER_VAPID_PUBLIC_KEY=abc
|
||||
expect_failure "is a secret and would be stored in plain text" --set $d --set env.FLUXER_EMAIL_SMTP_PASSWORD=not-a-real-password
|
||||
expect_failure "is a secret and would be stored in plain text" --set $d --set api.env.FLUXER_STRIPE_SECRET_KEY=not-a-real-key
|
||||
expect_failure "holds a password" --set $d --set-string 'env.FLUXER_SEARCH_URL=https://user:[email protected]'
|
||||
expect_failure "Set livekit.nodeCIDRs" --set $d --set networkPolicy.ingressNamespaceSelector.matchLabels.name=ingress
|
||||
expect_failure "set livekit.external.publicUrl" --set $d --set livekit.enabled=false --set livekit.external.url=http://livekit.voice.svc:7880
|
||||
expect_failure "reserve a public address" --set $d --set livekit.expose=loadBalancer
|
||||
|
||||
- name: Check that a chart version bump restarts nothing
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp -R "$CHART" "$RUNNER_TEMP/bumped"
|
||||
sed -e 's/^version: .*/version: 99.0.0/' -e 's/^appVersion: .*/appVersion: "bumped"/' "$CHART/Chart.yaml" > "$RUNNER_TEMP/bumped/Chart.yaml"
|
||||
helm template fluxer "$CHART" -n fluxer -f "$CHART/ci/existing-secret-values.yaml" > before.yaml
|
||||
helm template fluxer "$RUNNER_TEMP/bumped" -n fluxer -f "$CHART/ci/existing-secret-values.yaml" > after.yaml
|
||||
python3 - before.yaml after.yaml <<'PY'
|
||||
import sys, yaml
|
||||
def pods(path):
|
||||
out = {}
|
||||
for doc in yaml.safe_load_all(open(path)):
|
||||
if doc and doc.get('kind') in ('Deployment', 'StatefulSet'):
|
||||
spec = doc['spec']
|
||||
out[f"{doc['kind']}/{doc['metadata']['name']}"] = (spec['template'], spec['selector'], spec.get('volumeClaimTemplates'))
|
||||
return out
|
||||
before, after = pods(sys.argv[1]), pods(sys.argv[2])
|
||||
if len(before) < 20:
|
||||
sys.exit(f'only {len(before)} workloads rendered')
|
||||
changed = sorted(name for name in before.keys() | after.keys() if before.get(name) != after.get(name))
|
||||
if changed:
|
||||
sys.exit('a chart version bump changes the pod template of: ' + ', '.join(changed))
|
||||
print(f'{len(before)} workloads keep their pod template across a chart version bump')
|
||||
PY
|
||||
|
||||
- name: Check the default resource requests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
helm template fluxer "$CHART" -n fluxer -f "$CHART/ci/default-values.yaml" > rendered.yaml
|
||||
python3 - rendered.yaml <<'PY'
|
||||
import sys, yaml
|
||||
def cpu(v):
|
||||
v = str(v)
|
||||
return float(v[:-1]) if v.endswith('m') else float(v) * 1000
|
||||
units = {'Ki': 2**10, 'Mi': 2**20, 'Gi': 2**30}
|
||||
def mem(v):
|
||||
v = str(v)
|
||||
for unit, factor in units.items():
|
||||
if v.endswith(unit):
|
||||
return float(v[:-len(unit)]) * factor
|
||||
return float(v)
|
||||
total_cpu = total_mem = 0
|
||||
for doc in yaml.safe_load_all(open(sys.argv[1])):
|
||||
if not doc or doc.get('kind') not in ('Deployment', 'StatefulSet'):
|
||||
continue
|
||||
replicas = doc['spec'].get('replicas', 1)
|
||||
for container in doc['spec']['template']['spec']['containers']:
|
||||
requests = (container.get('resources') or {}).get('requests') or {}
|
||||
if 'cpu' not in requests or 'memory' not in requests:
|
||||
sys.exit(f"{doc['kind']}/{doc['metadata']['name']} has a container without cpu and memory requests")
|
||||
total_cpu += replicas * cpu(requests['cpu'])
|
||||
total_mem += replicas * mem(requests['memory'])
|
||||
print(f"requests: {total_cpu:.0f}m cpu, {total_mem / 2**30:.2f}Gi memory")
|
||||
if total_cpu > 1200 or total_mem > 4.5 * 2**30:
|
||||
sys.exit('the default install requests more than 1200m cpu or 4.5Gi memory')
|
||||
PY
|
||||
Generated
+3
-3
@@ -49,9 +49,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "ammonia"
|
||||
version = "4.2.0"
|
||||
version = "4.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f7ed3ac4252be4d3e36fb589ed5659356842587b21a31596a9dc89d3b2f5e2a"
|
||||
checksum = "061e83b03c2681c18a6787d956e355c74e0b98ba7ba3d69b0822ade1e6f1d716"
|
||||
dependencies = [
|
||||
"cssparser 0.38.0",
|
||||
"html5ever 0.40.1",
|
||||
@@ -1266,7 +1266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-api
|
||||
description: Fluxer HTTP API and background job workers
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,244 +0,0 @@
|
||||
{{- define "fluxer-api.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.labels" -}}
|
||||
{{ include "fluxer-api.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-api.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.image" -}}
|
||||
{{- $g := .root.Values.image | default dict -}}
|
||||
{{- $i := .w.image | default dict -}}
|
||||
{{- $repo := $i.repository -}}
|
||||
{{- if not $repo -}}
|
||||
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default "fluxer-api") -}}
|
||||
{{- end -}}
|
||||
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
|
||||
{{- if $i.digest -}}
|
||||
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $repo $tag | quote -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.pick" -}}
|
||||
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
|
||||
{{- if $v }}
|
||||
{{- toYaml $v }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.str" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
|
||||
{{- int64 . | toString | quote -}}
|
||||
{{- else -}}
|
||||
{{- toString . | quote -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.env" -}}
|
||||
{{- $env := dict -}}
|
||||
{{- range $k, $val := .root.Values.env | default dict }}
|
||||
{{- $_ := set $env $k $val }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := .w.env | default dict }}
|
||||
{{- $_ := set $env $k $val }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := $env }}
|
||||
{{- if not (kindIs "invalid" $val) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "fluxer-api.str" $val }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .w.buildVersion }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "fluxer-api.str" . }}
|
||||
{{- end }}
|
||||
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.topologySpread" -}}
|
||||
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
|
||||
{{- range $tscs }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not $c.labelSelector }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-api.selectorLabels" $ | fromYaml)) }}
|
||||
{{- end }}
|
||||
- {{- toYaml $c | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.pdb" -}}
|
||||
{{- with .w.pdb }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $.name }}-pdb
|
||||
namespace: {{ $.root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-api.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-api.selectorLabels" $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.hpa" -}}
|
||||
{{- with .w.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $.name }}
|
||||
namespace: {{ $.root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-api.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $.name }}
|
||||
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
|
||||
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
|
||||
{{- with .targetCPUUtilizationPercentage }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-api.deployment" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $v := $root.Values -}}
|
||||
{{- $w := .w -}}
|
||||
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) -}}
|
||||
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) -}}
|
||||
{{- $wProbes := $w.probes | default dict -}}
|
||||
{{- $gProbes := .probes | default dict -}}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .name }}
|
||||
namespace: {{ $root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-api.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $w.hpa }}
|
||||
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ int $w.minReadySeconds }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-api.selectorLabels" . | nindent 6 }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "strategy") }}
|
||||
strategy:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer-api.labels" . | nindent 8 }}
|
||||
{{- with $podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
|
||||
imagePullSecrets:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
|
||||
nodeSelector:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "affinity") }}
|
||||
affinity:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "tolerations") }}
|
||||
tolerations:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.topologySpread" . | trim }}
|
||||
topologySpreadConstraints:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .name }}
|
||||
image: {{ include "fluxer-api.image" . }}
|
||||
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
|
||||
{{- with .command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.env" . | trim }}
|
||||
env:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
|
||||
{{ $probe }}Probe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "securityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,24 +0,0 @@
|
||||
{{- range $name, $w := .Values.api }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "api" "probes" ($.Values.probes | default dict) }}
|
||||
{{ include "fluxer-api.deployment" $ctx }}
|
||||
{{ include "fluxer-api.hpa" $ctx }}
|
||||
{{ include "fluxer-api.pdb" $ctx }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-api.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer-api.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,8 +0,0 @@
|
||||
{{- range $name, $w := .Values.workers }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "worker" "command" (list "node" "dist/WorkerEntrypoint.js") "probes" (dict) }}
|
||||
{{ include "fluxer-api.deployment" $ctx }}
|
||||
{{ include "fluxer-api.hpa" $ctx }}
|
||||
{{ include "fluxer-api.pdb" $ctx }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,86 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
env:
|
||||
NODE_ENV: production
|
||||
FLUXER_ENV: production
|
||||
FLUXER_PUBLIC_ORIGIN: https://web.example.com
|
||||
FLUXER_API_ENDPOINT: https://api.example.com
|
||||
FLUXER_GATEWAY_ENDPOINT: wss://gateway.example.com
|
||||
FLUXER_MEDIA_ENDPOINT: https://media.example.com
|
||||
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: https://uploads.example.com
|
||||
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
|
||||
FLUXER_KV_URL: redis://valkey:6379/0
|
||||
FLUXER_NATS_URL: nats://nats:4222
|
||||
FLUXER_NATS_JETSTREAM_URL: nats://nats:4222
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 30
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
api:
|
||||
api:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
memory: 2560Mi
|
||||
|
||||
workers:
|
||||
worker:
|
||||
replicas: 1
|
||||
env:
|
||||
FLUXER_API_WORKER_MODE: all_lanes
|
||||
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
memory: 2560Mi
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-gateway
|
||||
description: A Helm chart for the Fluxer realtime gateway.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,280 +0,0 @@
|
||||
{{- define "gateway.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.labels" -}}
|
||||
{{ include "gateway.selectorLabels" . }}
|
||||
{{- with .component }}
|
||||
app.kubernetes.io/component: {{ . }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.headlessName" -}}
|
||||
{{ printf "%s-headless" .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.pick" -}}
|
||||
{{- $v := get .root.Values .key }}
|
||||
{{- if hasKey .w .key }}
|
||||
{{- $v = get .w .key }}
|
||||
{{- end }}
|
||||
{{- with $v }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.string" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
|
||||
{{- int64 . | toString }}
|
||||
{{- else }}
|
||||
{{- toString . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.envList" -}}
|
||||
{{- $env := deepCopy (.root.Values.env | default dict) }}
|
||||
{{- range $k, $v := .w.env | default dict }}
|
||||
{{- if kindIs "invalid" $v }}
|
||||
{{- $_ := unset $env $k }}
|
||||
{{- else }}
|
||||
{{- $_ := set $env $k $v }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k, $v := $env }}
|
||||
{{- if not (kindIs "invalid" $v) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "gateway.string" $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.envFrom" -}}
|
||||
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.podAnnotations" -}}
|
||||
{{- with merge (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.probes" -}}
|
||||
{{- $global := .root.Values.probes | default dict }}
|
||||
{{- $own := .w.probes | default dict }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- $p := get $global $probe }}
|
||||
{{- if hasKey $own $probe }}
|
||||
{{- $p = get $own $probe }}
|
||||
{{- end }}
|
||||
{{- with $p }}
|
||||
{{ $probe }}Probe:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.topologySpreadConstraints" -}}
|
||||
{{- $out := list }}
|
||||
{{- range include "gateway.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not (hasKey $c "labelSelector") }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "gateway.selectorLabels" $ | fromYaml)) }}
|
||||
{{- end }}
|
||||
{{- $out = append $out $c }}
|
||||
{{- end }}
|
||||
{{- with $out }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.image" -}}
|
||||
{{- $img := .w.image | default dict }}
|
||||
{{- $v := .root.Values.image }}
|
||||
{{- $repo := $img.repository | default (printf "%s/%s" $v.registry ($img.name | default "fluxer-gateway")) }}
|
||||
{{- $ref := printf "%s:%s" $repo ($img.tag | default $v.tag) }}
|
||||
{{- with $img.digest }}
|
||||
{{- $ref = printf "%s@%s" $ref . }}
|
||||
{{- end }}
|
||||
{{- $ref | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.replicas" -}}
|
||||
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.env" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w -}}
|
||||
{{- with $w.role }}
|
||||
- name: FLUXER_GATEWAY_ROLE
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.buildVersion) }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "gateway.string" $w.buildVersion | quote }}
|
||||
{{- end }}
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: status.podIP
|
||||
- name: FLUXER_ERLANG_NODE_NAME
|
||||
value: fluxer_gateway@$(POD_IP)
|
||||
- name: FLUXER_ERLANG_DIST_PORT
|
||||
value: "8081"
|
||||
- name: FLUXER_GATEWAY_CLUSTER_ENABLED
|
||||
value: "true"
|
||||
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME
|
||||
value: {{ printf "%s.%s.svc.%s" (include "gateway.headlessName" $root) $root.Release.Namespace $root.Values.clusterDomain | quote }}
|
||||
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME
|
||||
value: fluxer_gateway
|
||||
{{- include "gateway.envList" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.pod" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w -}}
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
{{- with include "gateway.podAnnotations" . }}
|
||||
annotations:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "affinity") }}
|
||||
affinity:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
|
||||
imagePullSecrets:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
|
||||
nodeSelector:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "tolerations") }}
|
||||
tolerations:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.topologySpreadConstraints" . }}
|
||||
topologySpreadConstraints:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: gateway
|
||||
image: {{ include "gateway.image" . }}
|
||||
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $root.Values.image.pullPolicy }}
|
||||
env:
|
||||
{{- include "gateway.env" . | trim | nindent 6 }}
|
||||
{{- with include "gateway.envFrom" . }}
|
||||
envFrom:
|
||||
{{- . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: epmd
|
||||
containerPort: 4369
|
||||
protocol: TCP
|
||||
- name: erl-dist
|
||||
containerPort: 8081
|
||||
protocol: TCP
|
||||
{{- with include "gateway.probes" . | trim }}
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "securityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.pdb" -}}
|
||||
{{- with .w.pdb }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $.name }}-pdb
|
||||
namespace: {{ $.root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
{{- if not (kindIs "invalid" .minAvailable) }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" .maxUnavailable) }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gateway.selectorLabels" $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "gateway.hpa" -}}
|
||||
{{- with .w.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $.name }}
|
||||
namespace: {{ $.root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $.name }}
|
||||
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
|
||||
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
|
||||
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,48 +0,0 @@
|
||||
{{- range $name, $w := .Values.deployments }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $w.hpa }}
|
||||
replicas: {{ include "gateway.replicas" $ctx }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ $w.minReadySeconds }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "strategy") }}
|
||||
strategy:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
{{- include "gateway.pod" $ctx | nindent 4 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
selector:
|
||||
{{- include "gateway.selectorLabels" $ctx | nindent 4 }}
|
||||
{{- include "gateway.hpa" $ctx }}
|
||||
{{- include "gateway.pdb" $ctx }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,26 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gateway.headlessName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" (dict "root" . "name" "gateway" "component" "discovery") | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
- name: epmd
|
||||
port: 4369
|
||||
protocol: TCP
|
||||
targetPort: epmd
|
||||
- name: erl-dist
|
||||
port: 8081
|
||||
protocol: TCP
|
||||
targetPort: erl-dist
|
||||
selector:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
@@ -1,53 +0,0 @@
|
||||
{{- $np := .Values.networkPolicy | default dict }}
|
||||
{{- if $np.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: gateway
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" (dict "root" . "name" "gateway") | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
egress:
|
||||
- {}
|
||||
ingress:
|
||||
{{- with $np.ingressNamespace }}
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ . }}
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- with $np.clients }}
|
||||
- from:
|
||||
{{- range . }}
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
- port: 4369
|
||||
protocol: TCP
|
||||
- port: 8081
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
@@ -1,29 +0,0 @@
|
||||
{{- range $name, $w := .Values.statefulsets }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ include "gateway.replicas" $ctx }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ $w.minReadySeconds }}
|
||||
{{- end }}
|
||||
serviceName: {{ include "gateway.headlessName" $ }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "updateStrategy") }}
|
||||
updateStrategy:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
{{- include "gateway.pod" $ctx | nindent 4 }}
|
||||
{{- include "gateway.pdb" $ctx }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,86 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
clusterDomain: cluster.local
|
||||
|
||||
env:
|
||||
FLUXER_ENV: production
|
||||
FLUXER_GATEWAY_PORT: "8080"
|
||||
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: https://media.example.com
|
||||
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
failureThreshold: 30
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
readiness:
|
||||
exec:
|
||||
command:
|
||||
- curl
|
||||
- -fsS
|
||||
- -o
|
||||
- /dev/null
|
||||
- --max-time
|
||||
- "2"
|
||||
- http://127.0.0.1:8080/_health/ready
|
||||
timeoutSeconds: 3
|
||||
|
||||
strategy: {}
|
||||
updateStrategy: {}
|
||||
|
||||
topologySpreadConstraints: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
ingressNamespace: ingress-nginx
|
||||
clients:
|
||||
- app.kubernetes.io/part-of: fluxer
|
||||
|
||||
deployments:
|
||||
gateway:
|
||||
role: all
|
||||
replicas: 1
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8080/_health/drain; sleep 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
|
||||
statefulsets: {}
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-infra
|
||||
description: NATS and Valkey for a Fluxer installation.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,282 +0,0 @@
|
||||
{{- define "fluxer-infra.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.labels" -}}
|
||||
{{ include "fluxer-infra.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-infra.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.pick" -}}
|
||||
{{- $v := get .root.Values .key }}
|
||||
{{- if hasKey .w .key }}
|
||||
{{- $v = get .w .key }}
|
||||
{{- end }}
|
||||
{{- with $v }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.string" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
|
||||
{{- int64 . | toString }}
|
||||
{{- else }}
|
||||
{{- toString . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.envList" -}}
|
||||
{{- $env := deepCopy (.root.Values.env | default dict) }}
|
||||
{{- range $k, $v := .w.env | default dict }}
|
||||
{{- if kindIs "invalid" $v }}
|
||||
{{- $_ := unset $env $k }}
|
||||
{{- else }}
|
||||
{{- $_ := set $env $k $v }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k, $v := $env }}
|
||||
{{- if not (kindIs "invalid" $v) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "fluxer-infra.string" $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.envFrom" -}}
|
||||
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.probes" -}}
|
||||
{{- $global := .root.Values.probes | default dict }}
|
||||
{{- $own := .w.probes | default dict }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- $p := get $global $probe }}
|
||||
{{- if hasKey $own $probe }}
|
||||
{{- $p = get $own $probe }}
|
||||
{{- end }}
|
||||
{{- with $p }}
|
||||
{{ $probe }}Probe:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.topologySpreadConstraints" -}}
|
||||
{{- $out := list }}
|
||||
{{- range include "fluxer-infra.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not (hasKey $c "labelSelector") }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-infra.selectorLabels" $ | fromYaml)) }}
|
||||
{{- end }}
|
||||
{{- $out = append $out $c }}
|
||||
{{- end }}
|
||||
{{- with $out }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.replicas" -}}
|
||||
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.image" -}}
|
||||
{{- $ref := printf "%s:%s" .repository .tag }}
|
||||
{{- with .digest }}
|
||||
{{- $ref = printf "%s@%s" $ref . }}
|
||||
{{- end }}
|
||||
{{- $ref | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.podAnnotations" -}}
|
||||
{{- with merge (deepCopy (.extra | default dict)) (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.podSpec" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "affinity") }}
|
||||
affinity:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
|
||||
imagePullSecrets:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
|
||||
nodeSelector:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "tolerations") }}
|
||||
tolerations:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.topologySpreadConstraints" . }}
|
||||
topologySpreadConstraints:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.containerCommon" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w }}
|
||||
{{- $img := $w.image | default dict }}
|
||||
image: {{ include "fluxer-infra.image" $img }}
|
||||
imagePullPolicy: {{ $img.pullPolicy }}
|
||||
{{- $env := include "fluxer-infra.envList" . | trim }}
|
||||
{{- if or .env $env }}
|
||||
env:
|
||||
{{- with .env }}
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with $env }}
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.envFrom" . }}
|
||||
envFrom:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- include "fluxer-infra.probes" . }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "securityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with concat .mounts ($w.extraVolumeMounts | default list) }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.statefulSetSpec" -}}
|
||||
{{- $w := .w }}
|
||||
{{- with include "fluxer-infra.pick" (dict "root" .root "w" $w "key" "updateStrategy") }}
|
||||
updateStrategy:
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ $w.minReadySeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.volumeClaim" -}}
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- with .storageClassName }}
|
||||
storageClassName: {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .size }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.pdb" -}}
|
||||
{{- with .w.pdb }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $.name }}-pdb
|
||||
namespace: {{ $.root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
{{- if not (kindIs "invalid" .minAvailable) }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" .maxUnavailable) }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-infra.selectorLabels" $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.service" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .svcName }}
|
||||
namespace: {{ .root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if .headless }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
{{- if .publishNotReady }}
|
||||
publishNotReadyAddresses: true
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "fluxer-infra.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- range .ports }}
|
||||
- name: {{ index . 0 }}
|
||||
port: {{ index . 1 }}
|
||||
targetPort: {{ index . 0 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-infra.natsConf" -}}
|
||||
{{- $w := .Values.nats -}}
|
||||
{{- with $w.config -}}
|
||||
listen: 0.0.0.0:4222
|
||||
http: 0.0.0.0:8222
|
||||
max_payload: {{ .maxPayload }}
|
||||
max_pending: {{ .maxPending }}
|
||||
max_connections: {{ .maxConnections }}
|
||||
{{- if $w.jetstream.enabled }}
|
||||
server_name: $POD_NAME
|
||||
|
||||
jetstream {
|
||||
store_dir: /data
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
cluster {
|
||||
name: {{ .clusterName }}
|
||||
listen: 0.0.0.0:6222
|
||||
|
||||
routes = [
|
||||
{{- range $i := until (int (include "fluxer-infra.replicas" (dict "w" $w))) }}
|
||||
nats-route://nats-{{ $i }}.nats-headless.{{ $.Release.Namespace }}.svc.{{ $.Values.clusterDomain }}:6222
|
||||
{{- end }}
|
||||
]
|
||||
}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
@@ -1,71 +0,0 @@
|
||||
{{- with .Values.nats }}
|
||||
{{- $ctx := dict "root" $ "w" . "name" "nats" "component" "messaging" }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: nats-config
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
|
||||
data:
|
||||
nats.conf: {{ include "fluxer-infra.natsConf" $ | toJson }}
|
||||
{{- include "fluxer-infra.pdb" $ctx }}
|
||||
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats" "ports" (list (list "client" 4222))) $ctx) }}
|
||||
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats-headless" "headless" true "ports" (list (list "client" 4222) (list "cluster" 6222) (list "monitor" 8222))) $ctx) }}
|
||||
{{- $mounts := list (dict "name" "config" "mountPath" "/etc/nats") }}
|
||||
{{- $env := list }}
|
||||
{{- if .jetstream.enabled }}
|
||||
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
|
||||
{{- $env = append $env (dict "name" "POD_NAME" "valueFrom" (dict "fieldRef" (dict "fieldPath" "metadata.name"))) }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: nats
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ include "fluxer-infra.replicas" $ctx }}
|
||||
serviceName: nats-headless
|
||||
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
|
||||
{{- with include "fluxer-infra.podAnnotations" (merge (dict "extra" (dict "checksum/config" (include "fluxer-infra.natsConf" $ | sha256sum))) $ctx) | trim }}
|
||||
{{- . | nindent 6 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
|
||||
containers:
|
||||
- name: nats
|
||||
{{- include "fluxer-infra.containerCommon" (merge (dict "env" $env "mounts" $mounts) $ctx) | trim | nindent 10 }}
|
||||
args:
|
||||
- -c
|
||||
- /etc/nats/nats.conf
|
||||
ports:
|
||||
- name: client
|
||||
containerPort: 4222
|
||||
- name: cluster
|
||||
containerPort: 6222
|
||||
- name: monitor
|
||||
containerPort: 8222
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: nats-config
|
||||
{{- with .extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .jetstream.enabled }}
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer-infra.volumeClaim" .jetstream.storage | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,67 +0,0 @@
|
||||
{{- with .Values.valkey }}
|
||||
{{- $ctx := dict "root" $ "w" . "name" "valkey" "component" "cache" }}
|
||||
{{- include "fluxer-infra.pdb" $ctx }}
|
||||
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey" "ports" (list (list "valkey" 6379))) $ctx) }}
|
||||
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey-headless" "headless" true "publishNotReady" true "ports" (list (list "valkey" 6379))) $ctx) }}
|
||||
{{- $mounts := list }}
|
||||
{{- if .persistence.enabled }}
|
||||
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: valkey
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: valkey-headless
|
||||
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
|
||||
{{- . | nindent 2 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
|
||||
{{- with include "fluxer-infra.podAnnotations" $ctx | trim }}
|
||||
{{- . | nindent 6 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
|
||||
containers:
|
||||
- name: valkey
|
||||
{{- include "fluxer-infra.containerCommon" (merge (dict "env" list "mounts" $mounts) $ctx) | trim | nindent 10 }}
|
||||
command:
|
||||
- valkey-server
|
||||
{{- if .persistence.enabled }}
|
||||
- --appendonly
|
||||
- "yes"
|
||||
- --dir
|
||||
- /data
|
||||
{{- else }}
|
||||
- --save
|
||||
- ""
|
||||
- --appendonly
|
||||
- "no"
|
||||
{{- end }}
|
||||
- --maxmemory
|
||||
- {{ .maxmemory | quote }}
|
||||
- --maxmemory-policy
|
||||
- {{ .maxmemoryPolicy | quote }}
|
||||
ports:
|
||||
- name: valkey
|
||||
containerPort: 6379
|
||||
{{- with .extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer-infra.volumeClaim" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,108 +0,0 @@
|
||||
imagePullSecrets: []
|
||||
|
||||
clusterDomain: cluster.local
|
||||
|
||||
env: {}
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom: []
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
probes: {}
|
||||
|
||||
updateStrategy: {}
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
nats:
|
||||
image:
|
||||
repository: nats
|
||||
tag: 2.14-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 3
|
||||
config:
|
||||
clusterName: nats
|
||||
maxPayload: 1MB
|
||||
maxPending: 64MB
|
||||
maxConnections: 65536
|
||||
jetstream:
|
||||
enabled: true
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClassName: ""
|
||||
podSecurityContext:
|
||||
fsGroup: 65534
|
||||
runAsGroup: 65534
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
probes:
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: monitor
|
||||
initialDelaySeconds: 10
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /healthz?js-enabled-only=true
|
||||
port: monitor
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
|
||||
valkey:
|
||||
image:
|
||||
repository: valkey/valkey
|
||||
tag: 9.1-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
maxmemory: 192mb
|
||||
maxmemoryPolicy: noeviction
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 1Gi
|
||||
storageClassName: ""
|
||||
podSecurityContext:
|
||||
fsGroup: 999
|
||||
runAsGroup: 999
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
probes:
|
||||
liveness:
|
||||
exec:
|
||||
command:
|
||||
- valkey-cli
|
||||
- ping
|
||||
initialDelaySeconds: 10
|
||||
readiness:
|
||||
exec:
|
||||
command:
|
||||
- valkey-cli
|
||||
- ping
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-ingress
|
||||
description: Ingress routing for the public Fluxer endpoints.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,27 +0,0 @@
|
||||
{{- define "fluxer-ingress.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-ingress.labels" -}}
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-ingress.chart" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-ingress.annotationKey" -}}
|
||||
{{- if or (contains "/" .key) (not .prefix) -}}
|
||||
{{- .key -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s/%s" .prefix .key -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-ingress.string" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
|
||||
{{- . | int64 | toString -}}
|
||||
{{- else -}}
|
||||
{{- . | toString -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- with .Values.clusterIssuer }}
|
||||
{{- if .enabled }}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: {{ required "clusterIssuer.name is required" .name }}
|
||||
labels:
|
||||
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
acme:
|
||||
email: {{ required "clusterIssuer.email is required" .email | quote }}
|
||||
privateKeySecretRef:
|
||||
name: {{ required "clusterIssuer.privateKeySecretName is required" .privateKeySecretName }}
|
||||
server: {{ required "clusterIssuer.server is required" .server }}
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: {{ required "clusterIssuer.solverIngressClass is required" .solverIngressClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,58 +0,0 @@
|
||||
{{- $v := .Values }}
|
||||
{{- $presets := $v.annotationPresets | default dict }}
|
||||
{{- $issuer := $v.clusterIssuer | default dict }}
|
||||
{{- range $name, $spec := ($v.ingresses | default dict) }}
|
||||
{{- if not (kindIs "invalid" $spec) }}
|
||||
{{- $ann := deepCopy ($v.commonAnnotations | default dict) }}
|
||||
{{- range ($spec.presets | default list) }}
|
||||
{{- $ann = mergeOverwrite $ann (deepCopy (required (printf "unknown annotation preset %s" .) (index $presets .))) }}
|
||||
{{- end }}
|
||||
{{- if and $spec.tls $issuer.enabled }}
|
||||
{{- $_ := set $ann "cert-manager.io/cluster-issuer" (required "clusterIssuer.name is required" $issuer.name) }}
|
||||
{{- end }}
|
||||
{{- $ann = mergeOverwrite $ann (deepCopy ($spec.annotations | default dict)) }}
|
||||
{{- range $k, $val := $ann }}
|
||||
{{- if kindIs "invalid" $val }}
|
||||
{{- $_ := unset $ann $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
|
||||
{{- with $ann }}
|
||||
annotations:
|
||||
{{- range $k, $val := . }}
|
||||
{{ include "fluxer-ingress.annotationKey" (dict "key" $k "prefix" $v.annotationPrefix) }}: {{ include "fluxer-ingress.string" $val | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with $spec.ingressClassName | default $v.ingressClassName }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with $spec.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range $rule := required (printf "ingress %s needs rules" $name) $spec.rules }}
|
||||
- host: {{ required (printf "ingress %s has a rule without a host" $name) $rule.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range $p := $rule.paths | default (list dict) }}
|
||||
{{- $p = $p | default dict }}
|
||||
- path: {{ $p.path | default "/" | quote }}
|
||||
pathType: {{ $p.pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ required (printf "ingress %s host %s needs a service" $name $rule.host) ($p.service | default $rule.service) }}
|
||||
port:
|
||||
number: {{ required (printf "ingress %s host %s needs a port or servicePort" $name $rule.host) ($p.port | default $rule.port | default $v.servicePort) | int64 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,53 +0,0 @@
|
||||
ingressClassName: nginx
|
||||
annotationPrefix: nginx.ingress.kubernetes.io
|
||||
servicePort: 8080
|
||||
|
||||
commonAnnotations: {}
|
||||
|
||||
annotationPresets:
|
||||
websocket:
|
||||
proxy-read-timeout: "3600"
|
||||
proxy-send-timeout: "3600"
|
||||
stripPrefix:
|
||||
use-regex: "true"
|
||||
rewrite-target: /$2
|
||||
|
||||
ingresses:
|
||||
fluxer:
|
||||
rules:
|
||||
- host: web.example.com
|
||||
service: app-proxy
|
||||
- host: api.example.com
|
||||
service: api
|
||||
- host: admin.example.com
|
||||
service: admin
|
||||
- host: media.example.com
|
||||
service: media-proxy
|
||||
fluxer-web-api:
|
||||
presets: [stripPrefix]
|
||||
rules:
|
||||
- host: web.example.com
|
||||
service: api
|
||||
paths:
|
||||
- path: /api(/(.*))?$
|
||||
pathType: ImplementationSpecific
|
||||
fluxer-gateway:
|
||||
presets: [websocket]
|
||||
rules:
|
||||
- host: gateway.example.com
|
||||
service: gateway
|
||||
fluxer-uploads:
|
||||
annotations:
|
||||
proxy-body-size: 100m
|
||||
proxy-request-buffering: "off"
|
||||
rules:
|
||||
- host: uploads.example.com
|
||||
service: uploads
|
||||
|
||||
clusterIssuer:
|
||||
enabled: false
|
||||
name: letsencrypt
|
||||
email: ""
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretName: letsencrypt-account-key
|
||||
solverIngressClass: nginx
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-media-proxy
|
||||
description: Fluxer media proxy and upload relay workloads.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,87 +0,0 @@
|
||||
{{- define "fluxer-media-proxy.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.labels" -}}
|
||||
{{ include "fluxer-media-proxy.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ include "fluxer-media-proxy.mode" . }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-media-proxy.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.image" -}}
|
||||
{{- $g := .root.Values.image -}}
|
||||
{{- $i := .w.image | default dict -}}
|
||||
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default "fluxer-media-proxy")) -}}
|
||||
{{- $tag := $i.tag | default $g.tag -}}
|
||||
{{- if $i.digest -}}
|
||||
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $repo $tag | quote -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.pick" -}}
|
||||
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
|
||||
{{- if $v }}
|
||||
{{- toYaml $v }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.mode" -}}
|
||||
{{- $mode := required (printf "workloads.%s.mode is required" .name) .w.mode -}}
|
||||
{{- if not (has $mode (list "mp" "static" "upload" "relay")) -}}
|
||||
{{- fail (printf "workloads.%s.mode must be mp, static, upload or relay" .name) -}}
|
||||
{{- end -}}
|
||||
{{- $mode -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.envValue" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
|
||||
{{- int64 . | toString -}}
|
||||
{{- else -}}
|
||||
{{- toString . -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.mergeEnv" -}}
|
||||
{{- $out := dict -}}
|
||||
{{- range $layer := . -}}
|
||||
{{- range $k, $v := ($layer | default dict) -}}
|
||||
{{- if kindIs "invalid" $v -}}
|
||||
{{- $_ := unset $out $k -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $k $v -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.topologySpreadConstraints" -}}
|
||||
{{- $out := list -}}
|
||||
{{- range .constraints -}}
|
||||
{{- if .labelSelector -}}
|
||||
{{- $out = append $out . -}}
|
||||
{{- else -}}
|
||||
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-media-proxy.pdb" -}}
|
||||
{{- $out := dict -}}
|
||||
{{- range $k := list "minAvailable" "maxUnavailable" -}}
|
||||
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
|
||||
{{- $_ := set $out $k (index $ $k) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
@@ -1,191 +0,0 @@
|
||||
{{- range $name, $w := .Values.workloads }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "w" $w }}
|
||||
{{- $mode := include "fluxer-media-proxy.mode" $ctx }}
|
||||
{{- $sel := include "fluxer-media-proxy.selectorLabels" $ctx | fromYaml }}
|
||||
{{- $env := include "fluxer-media-proxy.mergeEnv" (list $.Values.env $w.env) | fromYaml }}
|
||||
{{- $extraEnv := concat ($.Values.extraEnv | default list) ($w.extraEnv | default list) }}
|
||||
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
|
||||
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($.Values.podAnnotations | default dict) }}
|
||||
{{- $probes := dict }}
|
||||
{{- range $k, $v := ($.Values.probes | default dict) }}
|
||||
{{- $_ := set $probes $k $v }}
|
||||
{{- end }}
|
||||
{{- range $k, $v := ($w.probes | default dict) }}
|
||||
{{- $_ := set $probes $k $v }}
|
||||
{{- end }}
|
||||
{{- $pick := dict "root" $ "w" $w }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $w.hpa }}
|
||||
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int64 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ $w.minReadySeconds | int64 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- toYaml $sel | nindent 6 }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "strategy") }}
|
||||
strategy:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with $podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "fluxer-media-proxy.labels" $ctx | nindent 8 }}
|
||||
spec:
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "imagePullSecrets") }}
|
||||
imagePullSecrets:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "podSecurityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int64 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "nodeSelector") }}
|
||||
nodeSelector:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "tolerations") }}
|
||||
tolerations:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "affinity") }}
|
||||
affinity:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "topologySpreadConstraints") | fromYamlArray }}
|
||||
topologySpreadConstraints:
|
||||
{{- include "fluxer-media-proxy.topologySpreadConstraints" (dict "constraints" . "selector" $sel) | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ $name }}
|
||||
image: {{ include "fluxer-media-proxy.image" $ctx }}
|
||||
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
|
||||
env:
|
||||
{{- if not (kindIs "invalid" $w.buildVersion) }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "fluxer-media-proxy.envValue" $w.buildVersion | quote }}
|
||||
{{- end }}
|
||||
- name: FLUXER_MEDIA_PROXY_MODE
|
||||
value: {{ $mode | quote }}
|
||||
{{- range $k, $v := $env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "fluxer-media-proxy.envValue" $v | quote }}
|
||||
{{- end }}
|
||||
{{- with $extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- range $k := list "startup" "liveness" "readiness" }}
|
||||
{{- with get $probes $k }}
|
||||
{{ $k }}Probe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "securityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- toYaml $sel | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- with include "fluxer-media-proxy.pdb" ($w.pdb | default dict) | fromYaml }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $name }}-pdb
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- toYaml $sel | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with $w.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $name }}
|
||||
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int64 }}
|
||||
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int64 }}
|
||||
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,72 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
env: {}
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
probes:
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
workloads:
|
||||
media-proxy:
|
||||
mode: mp
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
|
||||
uploads:
|
||||
mode: relay
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-push
|
||||
description: Fluxer push notification delivery service
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,71 +0,0 @@
|
||||
{{- define "fluxer-push.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.labels" -}}
|
||||
{{ include "fluxer-push.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ include "fluxer-push.mode" . }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.mode" -}}
|
||||
{{- $mode := .w.mode | default "delivery" -}}
|
||||
{{- if not (has $mode (list "delivery" "relay")) -}}
|
||||
{{- fail (printf "workloads.%s.mode must be delivery or relay" .name) -}}
|
||||
{{- end -}}
|
||||
{{- $mode -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.port" -}}
|
||||
{{- .w.port | default (ternary 8127 8126 (eq (include "fluxer-push.mode" .) "relay")) -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.image" -}}
|
||||
{{- $global := .root.Values.image | default dict -}}
|
||||
{{- $img := .w.image | default dict -}}
|
||||
{{- $repo := $img.repository -}}
|
||||
{{- if not $repo -}}
|
||||
{{- $repo = printf "%s/%s" (required "image.registry is required" $global.registry) ($img.name | default "fluxer-push") -}}
|
||||
{{- end -}}
|
||||
{{- $ref := printf "%s:%s" $repo (include "fluxer-push.string" (required "image.tag is required" ($img.tag | default $global.tag))) -}}
|
||||
{{- with $img.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
|
||||
{{- $ref -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.string" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
|
||||
{{- . | int64 | toString -}}
|
||||
{{- else -}}
|
||||
{{- . | toString -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-push.env" -}}
|
||||
{{- $env := deepCopy (.root.Values.env | default dict) -}}
|
||||
{{- range $k, $v := (.w.env | default dict) -}}
|
||||
{{- if kindIs "invalid" $v -}}
|
||||
{{- $_ := unset $env $k -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $env $k $v -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "invalid" .w.port) -}}
|
||||
{{- $_ := set $env "FLUXER_PUSH_SERVICE_PORT" .w.port -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "invalid" .w.buildVersion) }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "fluxer-push.string" .w.buildVersion | quote }}
|
||||
{{- end }}
|
||||
{{- range $k, $v := $env }}
|
||||
{{- if not (kindIs "invalid" $v) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "fluxer-push.string" $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,205 +0,0 @@
|
||||
{{- range $name, $w := .Values.workloads }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "w" $w }}
|
||||
{{- $mode := include "fluxer-push.mode" $ctx }}
|
||||
{{- $port := include "fluxer-push.port" $ctx | int }}
|
||||
{{- $globalProbes := $.Values.probes | default dict }}
|
||||
{{- $workloadProbes := $w.probes | default dict }}
|
||||
{{- $probes := dict }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- $_ := set $probes $probe (ternary (index $workloadProbes $probe) (index $globalProbes $probe) (hasKey $workloadProbes $probe)) }}
|
||||
{{- end }}
|
||||
{{- $annotations := mergeOverwrite (deepCopy ($.Values.podAnnotations | default dict)) (deepCopy ($w.podAnnotations | default dict)) }}
|
||||
{{- $pullSecrets := ternary $w.imagePullSecrets $.Values.imagePullSecrets (hasKey $w "imagePullSecrets") }}
|
||||
{{- $podSecurityContext := ternary $w.podSecurityContext $.Values.podSecurityContext (hasKey $w "podSecurityContext") }}
|
||||
{{- $securityContext := ternary $w.securityContext $.Values.securityContext (hasKey $w "securityContext") }}
|
||||
{{- $strategy := ternary $w.strategy $.Values.strategy (hasKey $w "strategy") }}
|
||||
{{- $tsc := ternary $w.topologySpreadConstraints $.Values.topologySpreadConstraints (hasKey $w "topologySpreadConstraints") }}
|
||||
{{- $nodeSelector := ternary $w.nodeSelector $.Values.nodeSelector (hasKey $w "nodeSelector") }}
|
||||
{{- $tolerations := ternary $w.tolerations $.Values.tolerations (hasKey $w "tolerations") }}
|
||||
{{- $affinity := ternary $w.affinity $.Values.affinity (hasKey $w "affinity") }}
|
||||
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
|
||||
{{- $env := include "fluxer-push.env" $ctx }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $w.hpa }}
|
||||
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int }}
|
||||
{{- end }}
|
||||
{{- if hasKey $w "minReadySeconds" }}
|
||||
minReadySeconds: {{ $w.minReadySeconds | int }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- with $strategy }}
|
||||
strategy:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with $annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "fluxer-push.labels" $ctx | nindent 8 }}
|
||||
spec:
|
||||
{{- with $pullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if hasKey $w "terminationGracePeriodSeconds" }}
|
||||
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int }}
|
||||
{{- end }}
|
||||
{{- with $nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $tsc }}
|
||||
topologySpreadConstraints:
|
||||
{{- range . }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not $c.labelSelector }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-push.selectorLabels" $ctx | fromYaml)) }}
|
||||
{{- end }}
|
||||
{{- toYaml (list $c) | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ $name }}
|
||||
image: {{ include "fluxer-push.image" $ctx | quote }}
|
||||
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($.Values.image | default dict).pullPolicy | default "IfNotPresent" }}
|
||||
command:
|
||||
- /usr/local/bin/fluxer-push
|
||||
{{- if eq $mode "relay" }}
|
||||
args:
|
||||
- --mode
|
||||
- relay
|
||||
{{- end }}
|
||||
{{- with trim $env }}
|
||||
env:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $port }}
|
||||
protocol: TCP
|
||||
{{- with $probes.startup }}
|
||||
startupProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $probes.liveness }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $probes.readiness }}
|
||||
readinessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer-push.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $port }}
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
{{- with $w.pdb }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $name }}-pdb
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with $w.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $name }}
|
||||
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int }}
|
||||
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int }}
|
||||
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .targetCPUUtilizationPercentage | int }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,65 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
env: {}
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
probes:
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_healthz
|
||||
port: http
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /_healthz
|
||||
port: http
|
||||
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
workloads:
|
||||
push:
|
||||
mode: delivery
|
||||
replicas: 1
|
||||
env:
|
||||
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
|
||||
FLUXER_SVC_NATS_URL: nats://nats:4222
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-svc
|
||||
description: Fluxer internal services, each a router Deployment and a shard StatefulSet
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: v1
|
||||
@@ -1,203 +0,0 @@
|
||||
{{- define "fluxer-svc.chart" -}}
|
||||
{{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.labels" -}}
|
||||
{{ include "fluxer-svc.selectorLabels" . }}
|
||||
app.kubernetes.io/component: {{ .mode }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-svc.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.envValue" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
|
||||
{{- int64 . | toString -}}
|
||||
{{- else -}}
|
||||
{{- toString . -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.mergeEnv" -}}
|
||||
{{- $out := dict -}}
|
||||
{{- range $layer := . -}}
|
||||
{{- range $k, $v := ($layer | default dict) -}}
|
||||
{{- if kindIs "invalid" $v -}}
|
||||
{{- $_ := unset $out $k -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $k $v -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.topologySpreadConstraints" -}}
|
||||
{{- $out := list -}}
|
||||
{{- range .constraints -}}
|
||||
{{- if .labelSelector -}}
|
||||
{{- $out = append $out . -}}
|
||||
{{- else -}}
|
||||
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.pdb" -}}
|
||||
{{- $out := dict -}}
|
||||
{{- range $k := list "minAvailable" "maxUnavailable" -}}
|
||||
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
|
||||
{{- $_ := set $out $k (index $ $k) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.config" -}}
|
||||
{{- $v := .root.Values -}}
|
||||
{{- $levels := list (index $v .mode) (index .svc .mode) -}}
|
||||
{{- $c := dict "extraEnv" ($v.extraEnv | default list) "envFrom" ($v.envFrom | default list) "podAnnotations" (deepCopy ($v.podAnnotations | default dict)) "probes" (deepCopy ($v.probes | default dict)) "image" (deepCopy (.svc.image | default dict)) -}}
|
||||
{{- range $k := list "imagePullSecrets" "podSecurityContext" "securityContext" "topologySpreadConstraints" "nodeSelector" "tolerations" "affinity" (ternary "updateStrategy" "strategy" (eq .mode "shard")) -}}
|
||||
{{- $_ := set $c $k (index $v $k) -}}
|
||||
{{- end -}}
|
||||
{{- $envLayers := list $v.env -}}
|
||||
{{- range $level := $levels -}}
|
||||
{{- range $k, $x := ($level | default dict) -}}
|
||||
{{- if eq $k "env" -}}
|
||||
{{- $envLayers = append $envLayers $x -}}
|
||||
{{- else if has $k (list "podAnnotations" "image") -}}
|
||||
{{- $_ := set $c $k (mergeOverwrite (index $c $k) (deepCopy ($x | default dict))) -}}
|
||||
{{- else if has $k (list "extraEnv" "envFrom") -}}
|
||||
{{- $_ := set $c $k (concat (index $c $k) ($x | default list)) -}}
|
||||
{{- else if eq $k "probes" -}}
|
||||
{{- range $name, $p := ($x | default dict) -}}
|
||||
{{- $_ := set $c.probes $name $p -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $c $k $x -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $c "env" (include "fluxer-svc.mergeEnv" $envLayers | fromYaml) -}}
|
||||
{{- toYaml $c }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.image" -}}
|
||||
{{- $g := .root.Values.image -}}
|
||||
{{- $i := .c.image -}}
|
||||
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default (printf "fluxer-%s" .service))) -}}
|
||||
{{- $ref := printf "%s:%s" $repo ($i.tag | default $g.tag) -}}
|
||||
{{- with $i.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
|
||||
{{- $ref -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-svc.pod" -}}
|
||||
{{- $v := .root.Values -}}
|
||||
{{- $c := .c -}}
|
||||
metadata:
|
||||
{{- with $c.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- with $c.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $c.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $c.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ $c.terminationGracePeriodSeconds | int64 }}
|
||||
{{- end }}
|
||||
{{- with $c.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $c.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $c.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $c.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- include "fluxer-svc.topologySpreadConstraints" (dict "constraints" . "selector" (include "fluxer-svc.selectorLabels" $ | fromYaml)) | nindent 4 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .mode }}
|
||||
image: {{ include "fluxer-svc.image" . | quote }}
|
||||
imagePullPolicy: {{ $c.image.pullPolicy | default $v.image.pullPolicy }}
|
||||
env:
|
||||
- name: FLUXER_SVC_MODE
|
||||
value: {{ .mode | quote }}
|
||||
- name: FLUXER_SVC_NAME
|
||||
value: {{ .service | quote }}
|
||||
- name: FLUXER_SVC_SHARD_COUNT
|
||||
value: {{ .shardCount | quote }}
|
||||
- name: FLUXER_SVC_PORT
|
||||
value: {{ include "fluxer-svc.envValue" $v.port | quote }}
|
||||
{{- if not (kindIs "invalid" $c.buildVersion) }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "fluxer-svc.envValue" $c.buildVersion | quote }}
|
||||
{{- end }}
|
||||
{{- if eq .mode "shard" }}
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
{{- end }}
|
||||
{{- range $name, $value := $c.env }}
|
||||
- name: {{ $name }}
|
||||
value: {{ include "fluxer-svc.envValue" $value | quote }}
|
||||
{{- end }}
|
||||
{{- with $c.extraEnv }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $c.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $v.port }}
|
||||
protocol: TCP
|
||||
{{- with $c.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- range $name := list "startup" "liveness" "readiness" }}
|
||||
{{- with index $c.probes $name }}
|
||||
{{ $name }}Probe:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $c.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $c.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $c.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $c.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,145 +0,0 @@
|
||||
{{- range $service, $svc := .Values.services }}
|
||||
{{- if not (kindIs "invalid" $svc) }}
|
||||
{{- $svc = $svc | default dict }}
|
||||
{{- $rc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "router")) }}
|
||||
{{- $sc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "shard")) }}
|
||||
{{- $routerReplicas := ternary $rc.replicas 1 (hasKey $rc "replicas") | int64 }}
|
||||
{{- $shardCount := ternary $sc.replicas 1 (hasKey $sc "replicas") | int64 }}
|
||||
{{- if lt $shardCount 1 }}
|
||||
{{- fail (printf "services.%s shard replicas must be at least 1" $service) }}
|
||||
{{- end }}
|
||||
{{- $router := dict "root" $ "service" $service "svc" $svc "mode" "router" "name" $service "c" $rc "shardCount" (toString $shardCount) }}
|
||||
{{- $shard := dict "root" $ "service" $service "svc" $svc "mode" "shard" "name" (printf "%s-shard" $service) "c" $sc "shardCount" (toString $shardCount) }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $service }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $router | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $rc.hpa }}
|
||||
replicas: {{ $routerReplicas }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $rc.minReadySeconds) }}
|
||||
minReadySeconds: {{ $rc.minReadySeconds | int64 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-svc.selectorLabels" $router | nindent 6 }}
|
||||
{{- with $rc.strategy }}
|
||||
strategy:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
{{- include "fluxer-svc.pod" $router | nindent 4 }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ $service }}-shard
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $shardCount }}
|
||||
{{- if not (kindIs "invalid" $sc.minReadySeconds) }}
|
||||
minReadySeconds: {{ $sc.minReadySeconds | int64 }}
|
||||
{{- end }}
|
||||
podManagementPolicy: Parallel
|
||||
serviceName: {{ $service }}-shard-headless
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-svc.selectorLabels" $shard | nindent 6 }}
|
||||
{{- with $sc.updateStrategy }}
|
||||
updateStrategy:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
{{- include "fluxer-svc.pod" $shard | nindent 4 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $service }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $router | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer-svc.selectorLabels" $router | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $.Values.port }}
|
||||
targetPort: {{ $.Values.port }}
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $service }}-shard-headless
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
publishNotReadyAddresses: true
|
||||
selector:
|
||||
{{- include "fluxer-svc.selectorLabels" $shard | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $.Values.port }}
|
||||
targetPort: {{ $.Values.port }}
|
||||
protocol: TCP
|
||||
{{- with $rc.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $service }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $router | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $service }}
|
||||
minReplicas: {{ required (printf "services.%s router hpa.minReplicas is required" $service) .minReplicas | int64 }}
|
||||
maxReplicas: {{ required (printf "services.%s router hpa.maxReplicas is required" $service) .maxReplicas | int64 }}
|
||||
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $ctx := list $router $shard }}
|
||||
{{- with include "fluxer-svc.pdb" ($ctx.c.pdb | default dict) | fromYaml }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $ctx.name }}-pdb
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-svc.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-svc.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,89 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
env:
|
||||
FLUXER_SVC_NATS_URL: nats://nats:4222
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
probes:
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_healthz
|
||||
port: http
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
|
||||
topologySpreadConstraints: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
port: 8090
|
||||
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 192Mi
|
||||
|
||||
shard:
|
||||
replicas: 2
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /_healthz
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
memory: 384Mi
|
||||
|
||||
services:
|
||||
gifs:
|
||||
shard:
|
||||
env:
|
||||
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: https://media.example.com
|
||||
messages: {}
|
||||
snowflakes: {}
|
||||
unfurl:
|
||||
shard:
|
||||
env:
|
||||
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
|
||||
users: {}
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-web
|
||||
description: Fluxer web app proxy and admin dashboard.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -1,80 +0,0 @@
|
||||
{{- define "fluxer-web.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.labels" -}}
|
||||
{{ include "fluxer-web.selectorLabels" . }}
|
||||
app.kubernetes.io/component: web
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
helm.sh/chart: {{ include "fluxer-web.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.image" -}}
|
||||
{{- $g := .root.Values.image | default dict -}}
|
||||
{{- $i := .w.image | default dict -}}
|
||||
{{- $repo := $i.repository -}}
|
||||
{{- if not $repo -}}
|
||||
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default (printf "fluxer-%s" .name)) -}}
|
||||
{{- end -}}
|
||||
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
|
||||
{{- if $i.digest -}}
|
||||
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $repo $tag | quote -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.pick" -}}
|
||||
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
|
||||
{{- if $v }}
|
||||
{{- toYaml $v }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.str" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
|
||||
{{- int64 . | toString | quote -}}
|
||||
{{- else -}}
|
||||
{{- toString . | quote -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.env" -}}
|
||||
{{- $env := dict -}}
|
||||
{{- range $k, $val := .root.Values.env | default dict }}
|
||||
{{- $_ := set $env $k $val }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := .w.env | default dict }}
|
||||
{{- $_ := set $env $k $val }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := $env }}
|
||||
{{- if not (kindIs "invalid" $val) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ include "fluxer-web.str" $val }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .w.buildVersion }}
|
||||
- name: BUILD_VERSION
|
||||
value: {{ include "fluxer-web.str" . }}
|
||||
{{- end }}
|
||||
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer-web.topologySpread" -}}
|
||||
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
|
||||
{{- range $tscs }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not $c.labelSelector }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-web.selectorLabels" $ | fromYaml)) }}
|
||||
{{- end }}
|
||||
- {{- toYaml $c | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,172 +0,0 @@
|
||||
{{- $v := .Values }}
|
||||
{{- range $name, $w := .Values.workloads }}
|
||||
{{- if not (kindIs "invalid" $w) }}
|
||||
{{- $ctx := dict "root" $ "name" $name "w" $w }}
|
||||
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) }}
|
||||
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) }}
|
||||
{{- $wProbes := $w.probes | default dict }}
|
||||
{{- $gProbes := $v.probes | default dict }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- if not $w.hpa }}
|
||||
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
|
||||
minReadySeconds: {{ int $w.minReadySeconds }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "strategy") }}
|
||||
strategy:
|
||||
{{- . | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer-web.labels" $ctx | nindent 8 }}
|
||||
{{- with $podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "imagePullSecrets") }}
|
||||
imagePullSecrets:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "podSecurityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
|
||||
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "nodeSelector") }}
|
||||
nodeSelector:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "affinity") }}
|
||||
affinity:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "tolerations") }}
|
||||
tolerations:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.topologySpread" $ctx | trim }}
|
||||
topologySpreadConstraints:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ $name }}
|
||||
image: {{ include "fluxer-web.image" $ctx }}
|
||||
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
|
||||
{{- with include "fluxer-web.env" $ctx | trim }}
|
||||
env:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
{{- with $w.lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
|
||||
{{ $probe }}Probe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "securityContext") }}
|
||||
securityContext:
|
||||
{{- . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $w.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer-web.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- with $w.hpa }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ $name }}
|
||||
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $name) .minReplicas }}
|
||||
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $name) .maxReplicas }}
|
||||
{{- with .targetCPUUtilizationPercentage }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .behavior }}
|
||||
behavior:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $w.pdb }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $name }}-pdb
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,78 +0,0 @@
|
||||
image:
|
||||
registry: ghcr.io/fluxerapp
|
||||
tag: v1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
env: {}
|
||||
|
||||
extraEnv: []
|
||||
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: fluxer-env
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
failureThreshold: 30
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: http
|
||||
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
workloads:
|
||||
admin:
|
||||
image:
|
||||
name: fluxer-admin
|
||||
replicas: 1
|
||||
env:
|
||||
FLUXER_API_ENDPOINT: https://api.example.com
|
||||
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
|
||||
FLUXER_MEDIA_ENDPOINT: https://media.example.com
|
||||
FLUXER_APP_ENDPOINT: https://web.example.com
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
memory: 384Mi
|
||||
app-proxy:
|
||||
image:
|
||||
name: fluxer-app-proxy-self-hosted
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
memory: 384Mi
|
||||
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
ci/
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v2
|
||||
name: fluxer
|
||||
description: A complete Fluxer instance on Kubernetes, at parity with the self-hosted Docker Compose stack.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
kubeVersion: ">=1.29.0-0"
|
||||
home: https://docs.fluxer.app/operator/kubernetes/
|
||||
sources:
|
||||
- https://github.com/fluxerapp/fluxer/tree/main/deploy/helm/fluxer
|
||||
keywords:
|
||||
- fluxer
|
||||
- chat
|
||||
@@ -0,0 +1 @@
|
||||
domain: chat.example.test
|
||||
@@ -0,0 +1,3 @@
|
||||
domain: chat.example.test
|
||||
secrets:
|
||||
existingSecret: fluxer-ci-secret
|
||||
@@ -0,0 +1,38 @@
|
||||
domain: chat.example.test
|
||||
postgres:
|
||||
enabled: false
|
||||
external:
|
||||
host: db.example.test
|
||||
ssl: "true"
|
||||
valkey:
|
||||
enabled: false
|
||||
nats:
|
||||
enabled: false
|
||||
external:
|
||||
url: nats://mq.example.test:4222
|
||||
meilisearch:
|
||||
enabled: false
|
||||
external:
|
||||
url: https://search.example.test
|
||||
seaweedfs:
|
||||
enabled: false
|
||||
s3:
|
||||
endpoint: https://s3.example.test
|
||||
publicEndpoint: https://cdn.example.test
|
||||
region: eu-central-1
|
||||
forcePathStyle: false
|
||||
livekit:
|
||||
enabled: false
|
||||
external:
|
||||
url: http://livekit.voice.svc:7880
|
||||
publicUrl: wss://livekit.example.test
|
||||
secrets:
|
||||
values:
|
||||
FLUXER_POSTGRES_PASSWORD: ci-postgres-password
|
||||
FLUXER_SEARCH_API_KEY: ci-search-key
|
||||
FLUXER_S3_SECRET_ACCESS_KEY: ci-s3-secret
|
||||
FLUXER_NATS_AUTH_TOKEN: ci-nats-token
|
||||
FLUXER_LIVEKIT_API_KEY: ci-livekit-key
|
||||
FLUXER_LIVEKIT_API_SECRET: ci-livekit-secret-of-at-least-32-characters
|
||||
extra:
|
||||
FLUXER_KV_URL: redis://:[email protected]:6379/0
|
||||
@@ -0,0 +1,11 @@
|
||||
domain: chat.example.test
|
||||
edge:
|
||||
expose: gateway
|
||||
gateway:
|
||||
parentRefs:
|
||||
- name: shared
|
||||
namespace: gateway-system
|
||||
timeouts:
|
||||
request: 0s
|
||||
livekit:
|
||||
expose: nodePort
|
||||
@@ -0,0 +1,10 @@
|
||||
domain: chat.example.test
|
||||
email: [email protected]
|
||||
edge:
|
||||
expose: loadBalancer
|
||||
loadBalancer:
|
||||
http3: true
|
||||
livekit:
|
||||
expose: loadBalancer
|
||||
nodeIP: 203.0.113.10
|
||||
loadBalancerIP: 203.0.113.10
|
||||
@@ -0,0 +1,15 @@
|
||||
domain: chat.example.test
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
livekit:
|
||||
enabled: false
|
||||
nats:
|
||||
replicas: 3
|
||||
initImage:
|
||||
repository: registry.example.test/busybox
|
||||
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
|
||||
postgres:
|
||||
image:
|
||||
repository: registry.example.test/postgres
|
||||
tag: "16"
|
||||
runAsUser: 999
|
||||
@@ -0,0 +1,13 @@
|
||||
domain: chat.example.test
|
||||
networkPolicy:
|
||||
ingressNamespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: ingress-nginx
|
||||
edge:
|
||||
ingress:
|
||||
className: nginx
|
||||
livekit:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: voice-1
|
||||
nodeCIDRs:
|
||||
- 10.0.0.12/32
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
servers {
|
||||
trusted_proxies static {$FLUXER_EDGE_TRUSTED_PROXIES:private_ranges}
|
||||
trusted_proxies_strict
|
||||
}
|
||||
{$FLUXER_EDGE_GLOBAL_OPTIONS}
|
||||
}
|
||||
|
||||
{$FLUXER_EDGE_SITE_ADDRESS} {
|
||||
encode {$FLUXER_EDGE_ENCODE:zstd gzip}
|
||||
|
||||
handle /_health {
|
||||
respond "OK" 200
|
||||
}
|
||||
|
||||
handle_path /api/* {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
|
||||
handle /gateway {
|
||||
rewrite * /
|
||||
reverse_proxy gateway:8080
|
||||
}
|
||||
|
||||
handle_path /gateway/* {
|
||||
reverse_proxy gateway:8080
|
||||
}
|
||||
|
||||
handle_path /media/* {
|
||||
reverse_proxy media-proxy:8080
|
||||
}
|
||||
|
||||
handle_path /livekit/* {
|
||||
reverse_proxy livekit:7880
|
||||
}
|
||||
|
||||
handle {$FLUXER_ADMIN_BASE_PATH:/admin} {
|
||||
rewrite * /
|
||||
reverse_proxy admin:8080
|
||||
}
|
||||
|
||||
handle_path {$FLUXER_ADMIN_BASE_PATH:/admin}/* {
|
||||
reverse_proxy admin:8080
|
||||
}
|
||||
|
||||
@staticAssets path /web/* /emoji/* /libs/* /avatars/* /badges/* /desktop/*
|
||||
handle @staticAssets {
|
||||
reverse_proxy static-proxy:8080
|
||||
}
|
||||
|
||||
handle /.well-known/fluxer {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy app-proxy:8080
|
||||
}
|
||||
}
|
||||
|
||||
:8088 {
|
||||
handle /.well-known/fluxer {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
trap 'exit 0' TERM INT
|
||||
buckets="$FLUXER_S3_BUCKET_CDN $FLUXER_S3_BUCKET_UPLOADS $FLUXER_S3_BUCKET_REPORTS $FLUXER_S3_BUCKET_HARVESTS"
|
||||
missing="$buckets"
|
||||
for attempt in $(seq 1 "$FLUXER_SEAWEEDFS_INIT_ATTEMPTS"); do
|
||||
if ! nc -z 127.0.0.1 9333 2>/dev/null; then
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
listed=$(echo "s3.bucket.list" | timeout 10 weed shell -master=127.0.0.1:9333 2>&1)
|
||||
missing=""
|
||||
for b in $buckets; do
|
||||
echo "$listed" | grep -q "^[[:space:]]*$b[[:space:]]" || missing="${missing:+$missing }$b"
|
||||
done
|
||||
if [ -z "$missing" ]; then
|
||||
if ! echo "s3.configure -user=fluxer -access_key=$FLUXER_S3_ACCESS_KEY -secret_key=$FLUXER_S3_SECRET_KEY -actions=Admin,Read,Write,List,Tagging -apply" | timeout 10 weed shell -master=127.0.0.1:9333 >/dev/null 2>&1; then
|
||||
echo "seaweedfs-init could not configure the S3 identity" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "buckets ready"
|
||||
touch /tmp/fluxer-buckets-ready
|
||||
while :; do
|
||||
sleep 86400 &
|
||||
wait $!
|
||||
done
|
||||
fi
|
||||
for b in $missing; do
|
||||
echo "s3.bucket.create -name $b" | timeout 10 weed shell -master=127.0.0.1:9333 >/dev/null 2>&1
|
||||
done
|
||||
sleep 2
|
||||
done
|
||||
echo "seaweedfs-init could not verify buckets: $missing" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,72 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- $v := .Values }}
|
||||
{{- $o := include "fluxer.origin" . }}
|
||||
Fluxer is installing in namespace {{ .Release.Namespace }}. Run one release per namespace.
|
||||
|
||||
Open {{ $o }} once every pod is ready:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get pods
|
||||
|
||||
The admin panel is at {{ $o }}{{ $v.adminBasePath }}.
|
||||
|
||||
The setup wizard asks for the setup token before it creates the admin account:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get secret {{ include "fluxer.secretName" . }} -o jsonpath='{.data.FLUXER_SETUP_TOKEN}' | base64 -d
|
||||
|
||||
A Secret with no FLUXER_SETUP_TOKEN has a token the API wrote to its log once:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs deployment/api | grep -i 'setup token'
|
||||
{{- if eq $v.edge.expose "loadBalancer" }}
|
||||
|
||||
Point DNS for {{ $v.domain }} at the external address of the edge-public Service:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get service edge-public
|
||||
{{- else if eq $v.edge.expose "none" }}
|
||||
|
||||
Nothing exposes the instance yet. Send your proxy or tunnel to the Service edge
|
||||
on port 8080, with the Host header {{ $v.domain }}.
|
||||
{{- else }}
|
||||
|
||||
Point DNS for {{ $v.domain }} at your {{ ternary "ingress controller" "gateway" (eq $v.edge.expose "ingress") }}. Its Service needs
|
||||
externalTrafficPolicy Local or the PROXY protocol, or every client shares one IP
|
||||
and registration hits its per-IP limit.
|
||||
{{- end }}
|
||||
{{- if $v.livekit.external.url }}
|
||||
|
||||
Voice uses your LiveKit server at {{ $v.livekit.external.publicUrl }}. Point its webhook at
|
||||
{{ $o }}/api/webhooks/livekit, signed with the key in FLUXER_LIVEKIT_API_KEY.
|
||||
{{- end }}
|
||||
{{- if $v.livekit.enabled }}
|
||||
|
||||
Voice media does not pass through the edge. Open these ports to the internet:
|
||||
{{- if eq $v.livekit.expose "hostNetwork" }}
|
||||
{{ $v.livekit.tcpPort }}/tcp and {{ $v.livekit.udpPort }}/udp on the node that runs the livekit pod.
|
||||
hostNetwork needs a namespace that allows privileged pods. Keep 7880 closed.
|
||||
{{- else if eq $v.livekit.expose "nodePort" }}
|
||||
{{ $v.livekit.nodePorts.tcp }}/tcp and {{ $v.livekit.nodePorts.udp }}/udp on the node that runs the livekit pod.
|
||||
{{- else }}
|
||||
{{ $v.livekit.tcpPort }}/tcp and {{ $v.livekit.udpPort }}/udp on the load balancer of the Service livekit-rtc,
|
||||
whose public IP must be {{ $v.livekit.nodeIP }}.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
Back up the Secret {{ include "fluxer.secretName" . }}{{ if $v.postgres.enabled }}, the database in postgres-0{{ end }}{{ if $v.seaweedfs.enabled }} and the uploads on the volume data-seaweedfs-0{{ end }}.
|
||||
{{- if $v.secrets.existingSecret }}
|
||||
After editing the Secret {{ $v.secrets.existingSecret }}, restart the release:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart deployment,statefulset -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||
{{- end }}
|
||||
{{- if and $v.secrets.generate (not $v.secrets.existingSecret) }}
|
||||
|
||||
Argo CD and helm template cannot read the existing Secret and would generate
|
||||
new secrets on every render. Under them, set secrets.generate=false and use
|
||||
secrets.existingSecret or give every key in secrets.values.
|
||||
{{- end }}
|
||||
|
||||
To move to the latest v1 images, pull the repository and upgrade with your values file:
|
||||
|
||||
helm upgrade {{ .Release.Name }} <chart> -n {{ .Release.Namespace }} -f <your values file> --set-string image.revision=$(date +%s)
|
||||
|
||||
Leave out --reuse-values, which keeps the defaults of the old chart version.
|
||||
|
||||
Documentation: https://docs.fluxer.app/operator/kubernetes/
|
||||
@@ -0,0 +1,109 @@
|
||||
{{- define "fluxer.s3Endpoint" -}}
|
||||
{{- .Values.s3.endpoint | default (ternary "http://seaweedfs:8333" "" .Values.seaweedfs.enabled) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.configData" -}}
|
||||
{{- $v := .Values }}
|
||||
{{- $o := include "fluxer.origin" . }}
|
||||
{{- $hostOrigin := include "fluxer.hostOrigin" . }}
|
||||
{{- $media := printf "%s/media" $o }}
|
||||
{{- $s3 := include "fluxer.s3Endpoint" . }}
|
||||
{{- $c := dict }}
|
||||
{{- $_ := set $c "FLUXER_ENV" "production" }}
|
||||
{{- $_ := set $c "FLUXER_SELF_HOSTED" "true" }}
|
||||
{{- $_ := set $c "FLUXER_DATABASE_BACKEND" "postgres" }}
|
||||
{{- $_ := set $c "FLUXER_TRUST_CLIENT_IP_HEADER" "true" }}
|
||||
{{- $_ := set $c "FLUXER_BASE_DOMAIN" $v.domain }}
|
||||
{{- $_ := set $c "FLUXER_PUBLIC_SCHEME" $v.scheme }}
|
||||
{{- $_ := set $c "FLUXER_PUBLIC_PORT" (include "fluxer.string" $v.port) }}
|
||||
{{- $_ := set $c "FLUXER_PUBLIC_ORIGIN" (trimSuffix "/" ($v.publicOrigin | default "")) }}
|
||||
{{- $_ := set $c "FLUXER_APP_ENDPOINT" $o }}
|
||||
{{- $_ := set $c "FLUXER_MARKETING_ENDPOINT" $o }}
|
||||
{{- $_ := set $c "FLUXER_MEDIA_ENDPOINT" $media }}
|
||||
{{- $_ := set $c "FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT" $media }}
|
||||
{{- $_ := set $c "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT" $media }}
|
||||
{{- $_ := set $c "FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT" $media }}
|
||||
{{- $_ := set $c "FLUXER_ADMIN_ENDPOINT" (printf "%s%s" $o $v.adminBasePath) }}
|
||||
{{- $_ := set $c "FLUXER_GATEWAY_STATIC_CDN_ENDPOINT" $o }}
|
||||
{{- $_ := set $c "FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS" $hostOrigin }}
|
||||
{{- $_ := set $c "FLUXER_PASSKEY_RP_ID" $v.domain }}
|
||||
{{- $_ := set $c "FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS" $o }}
|
||||
{{- if $v.postgres.enabled }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_HOST" "postgres" }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_PORT" "5432" }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_DATABASE" $v.postgres.database }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_USERNAME" $v.postgres.username }}
|
||||
{{- else }}
|
||||
{{- with $v.postgres.external }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_HOST" (.host | default "") }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_PORT" (include "fluxer.string" (.port | default "")) }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_DATABASE" (.database | default "") }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_USERNAME" (.username | default "") }}
|
||||
{{- $_ := set $c "FLUXER_POSTGRES_SSL" (include "fluxer.string" (.ssl | default "")) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not $v.valkey.enabled }}
|
||||
{{- $_ := set $c "FLUXER_KV_URL" ($v.valkey.external.url | default "") }}
|
||||
{{- end }}
|
||||
{{- $nats := ternary "nats://nats:4222" ($v.nats.external.url | default "") $v.nats.enabled }}
|
||||
{{- $_ := set $c "FLUXER_NATS_URL" $nats }}
|
||||
{{- $_ := set $c "FLUXER_NATS_JETSTREAM_URL" $nats }}
|
||||
{{- $_ := set $c "FLUXER_SVC_NATS_URL" $nats }}
|
||||
{{- $_ := set $c "FLUXER_SEARCH_ENGINE" "meilisearch" }}
|
||||
{{- $_ := set $c "FLUXER_SEARCH_URL" (ternary "http://meilisearch:7700" ($v.meilisearch.external.url | default "") $v.meilisearch.enabled) }}
|
||||
{{- $_ := set $c "FLUXER_S3_ENDPOINT" $s3 }}
|
||||
{{- $_ := set $c "FLUXER_S3_PUBLIC_ENDPOINT" ($v.s3.publicEndpoint | default $s3) }}
|
||||
{{- $_ := set $c "FLUXER_S3_REGION" $v.s3.region }}
|
||||
{{- $_ := set $c "FLUXER_S3_FORCE_PATH_STYLE" (toString $v.s3.forcePathStyle) }}
|
||||
{{- $_ := set $c "FLUXER_S3_BUCKET_CDN" $v.s3.buckets.cdn }}
|
||||
{{- $_ := set $c "FLUXER_S3_BUCKET_UPLOADS" $v.s3.buckets.uploads }}
|
||||
{{- $_ := set $c "FLUXER_S3_BUCKET_REPORTS" $v.s3.buckets.reports }}
|
||||
{{- $_ := set $c "FLUXER_S3_BUCKET_HARVESTS" $v.s3.buckets.harvests }}
|
||||
{{- $livekit := include "fluxer.livekitMode" . }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_ENABLED" (toString (ne $livekit "off")) }}
|
||||
{{- if eq $livekit "bundled" }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_INTERNAL_URL" "http://livekit:7880" }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_DEFAULT_REGION" $v.livekit.defaultRegion }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_URL" (printf "%s/livekit" $hostOrigin) }}
|
||||
{{- else if eq $livekit "external" }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_INTERNAL_URL" $v.livekit.external.url }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_DEFAULT_REGION" $v.livekit.defaultRegion }}
|
||||
{{- $_ := set $c "FLUXER_LIVEKIT_URL" $v.livekit.external.publicUrl }}
|
||||
{{- end }}
|
||||
{{- $_ := set $c "FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED" "false" }}
|
||||
{{- $_ := set $c "FLUXER_EMAIL_FROM_EMAIL" "noreply@localhost" }}
|
||||
{{- $_ := set $c "FLUXER_VAPID_EMAIL" ($v.email | default (printf "admin@%s" $v.domain)) }}
|
||||
{{- $_ := set $c "FLUXER_INTERNAL_API_ENDPOINT" "http://api:8080" }}
|
||||
{{- $_ := set $c "FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT" "http://media-proxy:8080" }}
|
||||
{{- range $k, $val := $v.env | default dict }}
|
||||
{{- $_ := set $c $k (include "fluxer.string" ($val | default "")) }}
|
||||
{{- end }}
|
||||
{{- $out := dict }}
|
||||
{{- range $k, $val := $c }}
|
||||
{{- if $val }}
|
||||
{{- $_ := set $out $k $val }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- toYaml $out }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.appProxyConfigData" -}}
|
||||
{{- $v := .Values }}
|
||||
{{- $s3 := include "fluxer.s3Endpoint" . }}
|
||||
{{- $c := dict }}
|
||||
{{- $_ := set $c "FLUXER_APP_PROXY_PORT" "8080" }}
|
||||
{{- $_ := set $c "DISCOVERY_UPSTREAM_URL" "http://api:8080/.well-known/fluxer" }}
|
||||
{{- $_ := set $c "FLUXER_S3_PUBLIC_ENDPOINT" ($v.s3.publicEndpoint | default $s3) }}
|
||||
{{- $_ := set $c "FLUXER_S3_BUCKET_UPLOADS" $v.s3.buckets.uploads }}
|
||||
{{- $_ := set $c "FLUXER_MEDIA_ENDPOINT" (printf "%s/media" (include "fluxer.origin" .)) }}
|
||||
{{- range $k, $val := $v.appProxy.config | default dict }}
|
||||
{{- $_ := set $c $k (include "fluxer.string" ($val | default "")) }}
|
||||
{{- end }}
|
||||
{{- $out := dict }}
|
||||
{{- range $k, $val := $c }}
|
||||
{{- if $val }}
|
||||
{{- $_ := set $out $k $val }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- toYaml $out }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,375 @@
|
||||
{{- define "fluxer.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ .name }}
|
||||
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.podLabels" -}}
|
||||
{{ include "fluxer.selectorLabels" . }}
|
||||
{{- with .component }}
|
||||
app.kubernetes.io/component: {{ . }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/part-of: fluxer
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.labels" -}}
|
||||
{{ include "fluxer.podLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
|
||||
app.kubernetes.io/version: {{ .root.Chart.AppVersion | quote }}
|
||||
helm.sh/chart: {{ include "fluxer.chart" .root }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.string" -}}
|
||||
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
|
||||
{{- int64 . | toString }}
|
||||
{{- else }}
|
||||
{{- toString . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.origin" -}}
|
||||
{{- $v := .Values }}
|
||||
{{- if $v.publicOrigin }}
|
||||
{{- trimSuffix "/" $v.publicOrigin }}
|
||||
{{- else }}
|
||||
{{- $default := ternary 443 80 (eq $v.scheme "https") }}
|
||||
{{- if eq (int $v.port) $default }}
|
||||
{{- printf "%s://%s" $v.scheme $v.domain }}
|
||||
{{- else }}
|
||||
{{- printf "%s://%s:%d" $v.scheme $v.domain (int $v.port) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.hostOrigin" -}}
|
||||
{{- $v := .Values }}
|
||||
{{- if $v.publicOrigin }}
|
||||
{{- trimSuffix "/" $v.publicOrigin }}
|
||||
{{- else }}
|
||||
{{- printf "%s://%s:%d" $v.scheme $v.domain (int $v.port) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.secretName" -}}
|
||||
{{- .Values.secrets.existingSecret | default "fluxer-env" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.thirdPartyImage" -}}
|
||||
{{- $ref := printf "%s:%s" .repository (include "fluxer.string" .tag) }}
|
||||
{{- with .digest }}
|
||||
{{- $ref = printf "%s@%s" $ref . }}
|
||||
{{- end }}
|
||||
{{- $ref | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.image" -}}
|
||||
{{- $v := .root.Values.image }}
|
||||
{{- $img := (.w | default dict).image | default dict }}
|
||||
{{- $registry := $img.registry | default $v.registry }}
|
||||
{{- $tag := $img.tag | default (get ($v.tags | default dict) .image) | default $v.tag }}
|
||||
{{- $ref := printf "%s/%s:%s" $registry .image (include "fluxer.string" $tag) }}
|
||||
{{- with $img.digest | default (get ($v.digests | default dict) .image) }}
|
||||
{{- $ref = printf "%s@%s" $ref . }}
|
||||
{{- end }}
|
||||
{{- $ref | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.pullPolicy" -}}
|
||||
{{- (((.w | default dict).image | default dict).pullPolicy) | default .root.Values.image.pullPolicy }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.pullSecrets" -}}
|
||||
{{- with .Values.image.pullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.podSecurityContext" -}}
|
||||
runAsNonRoot: true
|
||||
{{- with .uid }}
|
||||
runAsUser: {{ . }}
|
||||
runAsGroup: {{ $.gid | default . }}
|
||||
fsGroup: {{ $.gid | default . }}
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
{{- end }}
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.containerSecurityContext" -}}
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.scheduling" -}}
|
||||
{{- $w := .w | default dict }}
|
||||
{{- with $w.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with $w.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- with $w.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- $spread := list }}
|
||||
{{- range $w.topologySpreadConstraints }}
|
||||
{{- $c := deepCopy . }}
|
||||
{{- if not (hasKey $c "labelSelector") }}
|
||||
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer.selectorLabels" $ | fromYaml)) }}
|
||||
{{- end }}
|
||||
{{- $spread = append $spread $c }}
|
||||
{{- end }}
|
||||
{{- with $spread }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.pvc" -}}
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- with .storageClassName }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .size }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.vapidPair" -}}
|
||||
{{- $der := genPrivateKey "ecdsa" | replace "-----BEGIN EC PRIVATE KEY-----" "" | replace "-----END EC PRIVATE KEY-----" "" | nospace | b64dec -}}
|
||||
private: {{ substr 7 39 $der | b64enc | replace "+" "-" | replace "/" "_" | trimAll "=" }}
|
||||
public: {{ substr 56 121 $der | b64enc | replace "+" "-" | replace "/" "_" | trimAll "=" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.livekitMode" -}}
|
||||
{{- if .Values.livekit.enabled }}bundled{{ else if .Values.livekit.external.url }}external{{ else }}off{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.noGenerateKeys" -}}
|
||||
{{- $keys := list }}
|
||||
{{- $extra := .Values.secrets.extra | default dict }}
|
||||
{{- if and (not .Values.postgres.enabled) (not (get $extra "FLUXER_POSTGRES_URL")) }}
|
||||
{{- $keys = append $keys "FLUXER_POSTGRES_PASSWORD" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.meilisearch.enabled }}
|
||||
{{- $keys = append $keys "FLUXER_SEARCH_API_KEY" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.seaweedfs.enabled }}
|
||||
{{- $keys = append $keys "FLUXER_S3_SECRET_ACCESS_KEY" }}
|
||||
{{- end }}
|
||||
{{- if eq (include "fluxer.livekitMode" .) "external" }}
|
||||
{{- $keys = append $keys "FLUXER_LIVEKIT_API_SECRET" }}
|
||||
{{- end }}
|
||||
{{- toJson $keys }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.requiredSecretKeys" -}}
|
||||
{{- $keys := list }}
|
||||
{{- $extra := .Values.secrets.extra | default dict }}
|
||||
{{- range $k, $_ := .Values.secrets.values }}
|
||||
{{- if and (eq $k "FLUXER_POSTGRES_PASSWORD") (get $extra "FLUXER_POSTGRES_URL") }}
|
||||
{{- else if and (hasPrefix "FLUXER_LIVEKIT_" $k) (eq (include "fluxer.livekitMode" $) "off") }}
|
||||
{{- else if and (eq $k "FLUXER_VALKEY_PASSWORD") (not $.Values.valkey.enabled) }}
|
||||
{{- else if and (eq $k "FLUXER_NATS_AUTH_TOKEN") (not $.Values.nats.enabled) }}
|
||||
{{- else if and (eq $k "FLUXER_SETUP_TOKEN") (or $.Values.secrets.existingSecret (and (not $.Values.secrets.generate) (not (include "fluxer.string" (get $.Values.secrets.values $k | default ""))))) }}
|
||||
{{- else }}
|
||||
{{- $keys = append $keys $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- toJson $keys }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.resolveSecrets" -}}
|
||||
{{- $v := .Values }}
|
||||
{{- $existing := dict }}
|
||||
{{- $found := lookup "v1" "Secret" .Release.Namespace (include "fluxer.secretName" .) }}
|
||||
{{- range $k, $val := ($found.data | default dict) }}
|
||||
{{- $_ := set $existing $k (b64dec $val) }}
|
||||
{{- end }}
|
||||
{{- $out := dict }}
|
||||
{{- if $v.secrets.existingSecret }}
|
||||
{{- $out = $existing }}
|
||||
{{- else }}
|
||||
{{- $noGenerate := include "fluxer.noGenerateKeys" . | fromJsonArray }}
|
||||
{{- $explicit := $v.secrets.values | default dict }}
|
||||
{{- $vapidPublic := include "fluxer.string" (get $explicit "FLUXER_VAPID_PUBLIC_KEY" | default "") }}
|
||||
{{- $vapidPrivate := include "fluxer.string" (get $explicit "FLUXER_VAPID_PRIVATE_KEY" | default "") }}
|
||||
{{- if and $vapidPublic $vapidPrivate }}
|
||||
{{- else if or $vapidPublic $vapidPrivate }}
|
||||
{{- fail "secrets.values.FLUXER_VAPID_PUBLIC_KEY and secrets.values.FLUXER_VAPID_PRIVATE_KEY must be set together, or both left empty to generate a pair" }}
|
||||
{{- else if and (get $existing "FLUXER_VAPID_PUBLIC_KEY") (get $existing "FLUXER_VAPID_PRIVATE_KEY") }}
|
||||
{{- $vapidPublic = get $existing "FLUXER_VAPID_PUBLIC_KEY" }}
|
||||
{{- $vapidPrivate = get $existing "FLUXER_VAPID_PRIVATE_KEY" }}
|
||||
{{- else if $v.secrets.generate }}
|
||||
{{- $pair := include "fluxer.vapidPair" . | fromYaml }}
|
||||
{{- $vapidPublic = $pair.public }}
|
||||
{{- $vapidPrivate = $pair.private }}
|
||||
{{- else }}
|
||||
{{- fail "secrets.values.FLUXER_VAPID_PUBLIC_KEY and secrets.values.FLUXER_VAPID_PRIVATE_KEY are empty and secrets.generate is false" }}
|
||||
{{- end }}
|
||||
{{- $_ := set $out "FLUXER_VAPID_PUBLIC_KEY" $vapidPublic }}
|
||||
{{- $_ := set $out "FLUXER_VAPID_PRIVATE_KEY" $vapidPrivate }}
|
||||
{{- $required := include "fluxer.requiredSecretKeys" . | fromJsonArray }}
|
||||
{{- $outsideToken := include "fluxer.string" (get $explicit "FLUXER_NATS_AUTH_TOKEN" | default "") }}
|
||||
{{- if and $outsideToken (not $v.nats.enabled) }}
|
||||
{{- $_ := set $out "FLUXER_NATS_AUTH_TOKEN" $outsideToken }}
|
||||
{{- end }}
|
||||
{{- range $k := $required }}
|
||||
{{- if not (hasPrefix "FLUXER_VAPID_" $k) }}
|
||||
{{- $given := include "fluxer.string" (get $explicit $k | default "") }}
|
||||
{{- if $given }}
|
||||
{{- $_ := set $out $k $given }}
|
||||
{{- else if has $k $noGenerate }}
|
||||
{{- fail (printf "secrets.values.%s is required when the matching service is not bundled" $k) }}
|
||||
{{- else if get $existing $k }}
|
||||
{{- $_ := set $out $k (get $existing $k) }}
|
||||
{{- else if $v.secrets.generate }}
|
||||
{{- if eq $k "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64" }}
|
||||
{{- $_ := set $out $k (randBytes 32) }}
|
||||
{{- else }}
|
||||
{{- $_ := set $out $k (randAlphaNum 64) }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- fail (printf "secrets.values.%s is empty and secrets.generate is false" $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := $v.secrets.extra | default dict }}
|
||||
{{- $s := include "fluxer.string" ($val | default "") }}
|
||||
{{- if $s }}
|
||||
{{- $_ := set $out $k $s }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- toJson $out }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.secretData" -}}
|
||||
{{- if not (hasKey .Values "fluxerResolvedSecrets") }}
|
||||
{{- $_ := set .Values "fluxerResolvedSecrets" (include "fluxer.resolveSecrets" . | fromJson) }}
|
||||
{{- end }}
|
||||
{{- toJson (get .Values "fluxerResolvedSecrets") }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.secretEnv" -}}
|
||||
- name: {{ .name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "fluxer.secretName" .root }}
|
||||
key: {{ .key }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.instanceChecksum" -}}
|
||||
{{- $parts := list (include "fluxer.configData" .) (include "fluxer.appProxyConfigData" .) (toJson .Values.secrets.values) (toJson (.Values.secrets.extra | default dict)) (include "fluxer.string" .Values.image.revision) (include "fluxer.secretData" .) }}
|
||||
{{- join "\n" $parts | sha256sum }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.envList" -}}
|
||||
{{- $w := .w | default dict }}
|
||||
{{- $own := $w.env | default dict }}
|
||||
{{- $base := .env | default list }}
|
||||
{{- $names := list }}
|
||||
{{- range $base }}
|
||||
{{- $names = append $names .name }}
|
||||
{{- end }}
|
||||
{{- $out := list }}
|
||||
{{- range $base }}
|
||||
{{- if hasKey $own .name }}
|
||||
{{- $val := get $own .name }}
|
||||
{{- if not (kindIs "invalid" $val) }}
|
||||
{{- $out = append $out (dict "name" .name "value" (include "fluxer.string" $val)) }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- $out = append $out . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k := keys $own | sortAlpha }}
|
||||
{{- $val := get $own $k }}
|
||||
{{- if and (not (has $k $names)) (not (kindIs "invalid" $val)) }}
|
||||
{{- $out = append $out (dict "name" $k "value" (include "fluxer.string" $val)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $out = concat $out ($w.extraEnv | default list) }}
|
||||
{{- with $out }}
|
||||
{{- toYaml . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.probes" -}}
|
||||
{{- $own := (.w | default dict).probes | default dict }}
|
||||
{{- range $probe := list "startup" "liveness" "readiness" }}
|
||||
{{- $p := get ($.probes | default dict) $probe }}
|
||||
{{- if hasKey $own $probe }}
|
||||
{{- $p = get $own $probe }}
|
||||
{{- end }}
|
||||
{{- with $p }}
|
||||
{{ $probe }}Probe:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.waitFor" -}}
|
||||
{{- with .waitFor }}
|
||||
initContainers:
|
||||
- name: wait-for-dependencies
|
||||
image: {{ include "fluxer.thirdPartyImage" $.root.Values.initImage }}
|
||||
imagePullPolicy: {{ $.root.Values.initImage.pullPolicy }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- {{ printf "for t in %s; do until nc -z -w 2 ${t%%:*} ${t##*:}; do echo waiting for $t; sleep 2; done; done" (join " " .) | quote }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 32Mi
|
||||
securityContext:
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.appWaitFor" -}}
|
||||
{{- $out := list }}
|
||||
{{- if and .usesNats .root.Values.nats.enabled }}
|
||||
{{- $out = append $out "nats:4222" }}
|
||||
{{- end }}
|
||||
{{- if and .usesPostgres .root.Values.postgres.enabled }}
|
||||
{{- $out = append $out "postgres:5432" }}
|
||||
{{- end }}
|
||||
{{- if and .usesS3 .root.Values.seaweedfs.enabled }}
|
||||
{{- $out = append $out "seaweedfs:8333" }}
|
||||
{{- end }}
|
||||
{{- if and .usesKv .root.Values.valkey.enabled }}
|
||||
{{- $out = append $out "valkey:6379" }}
|
||||
{{- end }}
|
||||
{{- if .usesSvc }}
|
||||
{{- range $name := list "gifs" "messages" "snowflakes" "unfurl" "users" }}
|
||||
{{- $svc := get $.root.Values.svc $name | default dict }}
|
||||
{{- if gt (int (include "fluxer.replicas" (dict "w" ($svc.router | default dict)))) 0 }}
|
||||
{{- $out = append $out (printf "%s:8090" $name) }}
|
||||
{{- end }}
|
||||
{{- if gt (int (include "fluxer.replicas" (dict "w" ($svc.shard | default dict)))) 0 }}
|
||||
{{- $out = append $out (printf "%s-shard:8090" $name) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- toJson $out }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,245 @@
|
||||
{{- define "fluxer.validate" -}}
|
||||
{{- if not (hasKey .Values "fluxerValidated") }}
|
||||
{{- $_ := set .Values "fluxerValidated" true }}
|
||||
{{- $v := .Values }}
|
||||
{{- if not (regexMatch "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$" ($v.domain | default "")) }}
|
||||
{{- fail "domain must be set to the lowercase public hostname of the instance, for example --set domain=chat.example.com" }}
|
||||
{{- end }}
|
||||
{{- if not (has $v.scheme (list "http" "https")) }}
|
||||
{{- fail "scheme must be http or https" }}
|
||||
{{- end }}
|
||||
{{- if and $v.publicOrigin (not (regexMatch "^https?://[^/]+$" (trimSuffix "/" $v.publicOrigin))) }}
|
||||
{{- fail "publicOrigin must be a scheme, host and optional port, for example https://chat.example.com" }}
|
||||
{{- end }}
|
||||
{{- if not (regexMatch "^/[^/].*[^/]$|^/[^/]$" $v.adminBasePath) }}
|
||||
{{- fail "adminBasePath must start with a slash and must not end with one, for example /admin" }}
|
||||
{{- end }}
|
||||
{{- $extra := $v.secrets.extra | default dict }}
|
||||
{{- if and (not $v.postgres.enabled) (not $v.postgres.external.host) (not (get $extra "FLUXER_POSTGRES_URL")) (not $v.secrets.existingSecret) }}
|
||||
{{- fail "postgres.enabled is false, so set postgres.external.host or secrets.extra.FLUXER_POSTGRES_URL" }}
|
||||
{{- end }}
|
||||
{{- if and (not $v.valkey.enabled) (not $v.valkey.external.url) (not (get $extra "FLUXER_KV_URL")) (not $v.secrets.existingSecret) }}
|
||||
{{- fail "valkey.enabled is false, so set valkey.external.url or secrets.extra.FLUXER_KV_URL" }}
|
||||
{{- end }}
|
||||
{{- if and (not $v.nats.enabled) (not $v.nats.external.url) }}
|
||||
{{- fail "nats.enabled is false, so set nats.external.url" }}
|
||||
{{- end }}
|
||||
{{- if and (not $v.meilisearch.enabled) (not $v.meilisearch.external.url) }}
|
||||
{{- fail "meilisearch.enabled is false, so set meilisearch.external.url" }}
|
||||
{{- end }}
|
||||
{{- if and (not $v.seaweedfs.enabled) (not $v.s3.endpoint) }}
|
||||
{{- fail "seaweedfs.enabled is false, so set s3.endpoint to your S3 store" }}
|
||||
{{- end }}
|
||||
{{- if gt (int $v.nats.replicas) 1 }}
|
||||
{{- if lt (int $v.nats.replicas) 3 }}
|
||||
{{- fail "nats.replicas must be 1, or 3 or more for a JetStream cluster" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if gt (int (include "fluxer.replicas" (dict "w" $v.worker))) 1 }}
|
||||
{{- fail "worker.replicas must be 1, because the worker runs the cron scheduler" }}
|
||||
{{- end }}
|
||||
{{- if not (has $v.livekit.expose (list "hostNetwork" "loadBalancer" "nodePort")) }}
|
||||
{{- fail "livekit.expose must be hostNetwork, loadBalancer or nodePort" }}
|
||||
{{- end }}
|
||||
{{- if and $v.livekit.enabled $v.livekit.external.url }}
|
||||
{{- fail "livekit.external.url names a LiveKit server of your own, so set livekit.enabled to false" }}
|
||||
{{- end }}
|
||||
{{- if and $v.livekit.external.url (not (regexMatch "^(wss?|https?)://[^/]+" ($v.livekit.external.publicUrl | default ""))) }}
|
||||
{{- fail "livekit.external.url is set, so set livekit.external.publicUrl to the address browsers use to reach that LiveKit server, for example wss://livekit.example.com" }}
|
||||
{{- end }}
|
||||
{{- if and $v.livekit.enabled (eq $v.livekit.expose "loadBalancer") (not $v.livekit.nodeIP) }}
|
||||
{{- fail "livekit.expose is loadBalancer, so reserve a public address before installing, set livekit.nodeIP to it, and pin the Service livekit-rtc to it with livekit.loadBalancerIP or livekit.serviceAnnotations. Use hostNetwork or nodePort where a load balancer cannot serve TCP and UDP on one address" }}
|
||||
{{- end }}
|
||||
{{- if not (has $v.edge.expose (list "ingress" "gateway" "loadBalancer" "none")) }}
|
||||
{{- fail "edge.expose must be ingress, gateway, loadBalancer or none" }}
|
||||
{{- end }}
|
||||
{{- if not (has $v.edge.ingress.tls.mode (list "certManager" "existingSecret" "none")) }}
|
||||
{{- fail "edge.ingress.tls.mode must be certManager, existingSecret or none" }}
|
||||
{{- end }}
|
||||
{{- if and (eq $v.edge.expose "gateway") (not $v.edge.gateway.parentRefs) }}
|
||||
{{- fail "edge.expose is gateway, so set edge.gateway.parentRefs to the Gateway the route attaches to" }}
|
||||
{{- end }}
|
||||
{{- if and (eq $v.edge.expose "loadBalancer") (ne $v.scheme "https") }}
|
||||
{{- fail "edge.expose loadBalancer serves HTTPS itself, so scheme must be https" }}
|
||||
{{- end }}
|
||||
{{- if and $v.networkPolicy.enabled $v.networkPolicy.ingressNamespaceSelector $v.livekit.enabled (eq $v.livekit.expose "hostNetwork") (not $v.livekit.nodeCIDRs) }}
|
||||
{{- fail "networkPolicy.ingressNamespaceSelector limits who reaches the edge, and LiveKit on the host network sends its webhook to the edge from a node address. Set livekit.nodeCIDRs to the address of the LiveKit node, for example 10.0.0.12/32" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $placeholder := "(?i)change[_-]?me" }}
|
||||
{{- $secretName := "_(PASSWORD|SECRET|TOKEN|COOKIE|KEY|KEYS|KEY_BASE|SECRETS?_BASE64)$" }}
|
||||
{{- $credentialUrl := "://[^/@ ]*:[^/@ ]*@" }}
|
||||
{{- $plain := dict "env" ($v.env | default dict) "appProxy.config" ($v.appProxy.config | default dict) "edge.env" ($v.edge.env | default dict) }}
|
||||
{{- range $name := list "api" "worker" "gateway" "mediaProxy" "push" "admin" "appProxy" "staticProxy" }}
|
||||
{{- $_ := set $plain (printf "%s.env" $name) ((get $v $name).env | default dict) }}
|
||||
{{- end }}
|
||||
{{- range $name, $svc := $v.svc }}
|
||||
{{- range $part := list "router" "shard" }}
|
||||
{{- $_ := set $plain (printf "svc.%s.%s.env" $name $part) ((get ($svc | default dict) $part | default dict).env | default dict) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $where, $map := $plain }}
|
||||
{{- range $k, $val := $map }}
|
||||
{{- $s := include "fluxer.string" ($val | default "") }}
|
||||
{{- if regexMatch $placeholder $s }}
|
||||
{{- fail (printf "%s.%s is a placeholder. Remove it or set a real value" $where $k) }}
|
||||
{{- end }}
|
||||
{{- if or (hasKey $v.secrets.values $k) (regexMatch $secretName $k) }}
|
||||
{{- fail (printf "%s.%s is a secret and would be stored in plain text. Set it in secrets.extra instead, or reference a Secret of your own from extraEnv" $where $k) }}
|
||||
{{- end }}
|
||||
{{- if regexMatch $credentialUrl $s }}
|
||||
{{- fail (printf "%s.%s is a URL that holds a password and would be stored in plain text. Set it in secrets.extra instead" $where $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := $v.secrets.values }}
|
||||
{{- if regexMatch $placeholder (include "fluxer.string" ($val | default "")) }}
|
||||
{{- fail (printf "secrets.values.%s is a placeholder. Leave it empty to generate a value, or set a real one" $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $k, $val := $extra }}
|
||||
{{- if regexMatch $placeholder (include "fluxer.string" ($val | default "")) }}
|
||||
{{- fail (printf "secrets.extra.%s is a placeholder. Remove it or set a real value" $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- $connected := false }}
|
||||
{{- if $v.clusterChecks }}
|
||||
{{- $connected = or (lookup "v1" "Namespace" "" .Release.Namespace) (lookup "v1" "Namespace" "" "kube-system") | not | not }}
|
||||
{{- end }}
|
||||
{{- $data := include "fluxer.secretData" . | fromJson }}
|
||||
{{- $secret := include "fluxer.secretName" . }}
|
||||
{{- if $v.secrets.existingSecret }}
|
||||
{{- if and $connected (not $data) }}
|
||||
{{- fail (printf "secrets.existingSecret names the Secret %s, which does not exist in namespace %s. Create it first with every key listed under secrets.values" $secret .Release.Namespace) }}
|
||||
{{- end }}
|
||||
{{- if $data }}
|
||||
{{- $missing := list }}
|
||||
{{- range $k := include "fluxer.requiredSecretKeys" . | fromJsonArray }}
|
||||
{{- if not (get $data $k) }}
|
||||
{{- $missing = append $missing $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $missing }}
|
||||
{{- fail (printf "Secret %s is missing these keys: %s" $secret (join ", " .)) }}
|
||||
{{- end }}
|
||||
{{- $config := include "fluxer.configData" . | fromYaml }}
|
||||
{{- if $v.valkey.enabled }}
|
||||
{{- $_ := set $config "FLUXER_KV_URL" "the bundled Valkey" }}
|
||||
{{- end }}
|
||||
{{- $shadowed := list }}
|
||||
{{- range $k := keys $config | sortAlpha }}
|
||||
{{- if and (hasKey $data $k) (ne (get $data $k) (get $config $k)) }}
|
||||
{{- $shadowed = append $shadowed $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $shadowed }}
|
||||
{{- fail (printf "Secret %s holds settings the chart sets itself, and the two disagree: %s. Remove them from the Secret. To keep an outside service, turn the bundled one off, as the External services section of the Kubernetes page describes" $secret (join ", " .)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $data }}
|
||||
{{- range $k, $val := $data }}
|
||||
{{- if regexMatch $placeholder $val }}
|
||||
{{- fail (printf "%s in the Secret %s is a placeholder. Set a real value" $k $secret) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with get $data "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64" }}
|
||||
{{- if or (not (regexMatch "^[A-Za-z0-9+/]+={0,2}$" .)) (lt (len (b64dec .)) 32) }}
|
||||
{{- fail "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must be standard base64 of at least 32 bytes, for example the output of openssl rand -base64 32" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $long := list "FLUXER_SUDO_MODE_SECRET" "FLUXER_CONNECTION_INITIATION_SECRET" "FLUXER_PROFILE_PSEUDONYM_SECRET" "FLUXER_GATEWAY_RPC_AUTH_TOKEN" "FLUXER_ERLANG_COOKIE" "FLUXER_MEDIA_PROXY_SECRET_KEY" "FLUXER_ADMIN_SECRET_KEY_BASE" "FLUXER_ADMIN_OAUTH_CLIENT_SECRET" "FLUXER_SETUP_TOKEN" }}
|
||||
{{- if ne (include "fluxer.livekitMode" .) "off" }}
|
||||
{{- $long = append $long "FLUXER_LIVEKIT_API_SECRET" }}
|
||||
{{- end }}
|
||||
{{- if $v.valkey.enabled }}
|
||||
{{- $long = append $long "FLUXER_VALKEY_PASSWORD" }}
|
||||
{{- end }}
|
||||
{{- if $v.nats.enabled }}
|
||||
{{- $long = append $long "FLUXER_NATS_AUTH_TOKEN" }}
|
||||
{{- end }}
|
||||
{{- range $k := $long }}
|
||||
{{- if and (hasKey $data $k) (lt (len (get $data $k)) 32) }}
|
||||
{{- fail (printf "%s must be at least 32 characters, for example the output of openssl rand -hex 32" $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and $v.meilisearch.enabled (hasKey $data "FLUXER_SEARCH_API_KEY") (lt (len (get $data "FLUXER_SEARCH_API_KEY")) 16) }}
|
||||
{{- fail "FLUXER_SEARCH_API_KEY must be at least 16 characters, or the bundled Meilisearch refuses to start" }}
|
||||
{{- end }}
|
||||
{{- if eq (get $data "FLUXER_POSTGRES_PASSWORD" | default "") "fluxer" }}
|
||||
{{- fail "FLUXER_POSTGRES_PASSWORD must not be fluxer, which the services refuse in production" }}
|
||||
{{- end }}
|
||||
{{- $plainOnly := list }}
|
||||
{{- if $v.seaweedfs.enabled }}
|
||||
{{- $plainOnly = concat $plainOnly (list "FLUXER_S3_ACCESS_KEY_ID" "FLUXER_S3_SECRET_ACCESS_KEY") }}
|
||||
{{- end }}
|
||||
{{- if $v.valkey.enabled }}
|
||||
{{- $plainOnly = append $plainOnly "FLUXER_VALKEY_PASSWORD" }}
|
||||
{{- end }}
|
||||
{{- if $v.nats.enabled }}
|
||||
{{- $plainOnly = append $plainOnly "FLUXER_NATS_AUTH_TOKEN" }}
|
||||
{{- end }}
|
||||
{{- range $k := $plainOnly }}
|
||||
{{- if and (hasKey $data $k) (not (regexMatch "^[A-Za-z0-9]+$" (get $data $k))) }}
|
||||
{{- fail (printf "%s must contain letters and digits only, because the bundled service takes it on a command line or in a URL" $k) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $pub := get $data "FLUXER_VAPID_PUBLIC_KEY" | default "" }}
|
||||
{{- $priv := get $data "FLUXER_VAPID_PRIVATE_KEY" | default "" }}
|
||||
{{- if or $pub $priv }}
|
||||
{{- if or (not (regexMatch "^[A-Za-z0-9_-]{87}$" $pub)) (not (regexMatch "^[A-Za-z0-9_-]{43}$" $priv)) }}
|
||||
{{- fail "FLUXER_VAPID_PUBLIC_KEY and FLUXER_VAPID_PRIVATE_KEY must be an unpadded base64url P-256 pair of 87 and 43 characters" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if $connected }}
|
||||
{{- if and (eq $v.edge.expose "ingress") (eq $v.edge.ingress.tls.mode "certManager") (not (.Capabilities.APIVersions.Has "cert-manager.io/v1")) }}
|
||||
{{- fail "edge.ingress.tls.mode is certManager, but cert-manager is not installed. Install cert-manager, or set edge.ingress.tls.mode to existingSecret or none, or set edge.expose to loadBalancer so the edge obtains its own certificate" }}
|
||||
{{- end }}
|
||||
{{- if and (eq $v.edge.expose "gateway") (not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1")) }}
|
||||
{{- fail "edge.expose is gateway, but the Gateway API is not installed in the cluster" }}
|
||||
{{- end }}
|
||||
{{- $emptyClass := and (eq $v.edge.expose "loadBalancer") (not $v.edge.loadBalancer.persistence.storageClassName) }}
|
||||
{{- range $c := list $v.postgres $v.valkey $v.nats $v.meilisearch $v.seaweedfs }}
|
||||
{{- if and $c.enabled (not $c.persistence.storageClassName) }}
|
||||
{{- $emptyClass = true }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $emptyClass }}
|
||||
{{- $default := false }}
|
||||
{{- range (lookup "storage.k8s.io/v1" "StorageClass" "" "").items | default list }}
|
||||
{{- if eq (get (.metadata.annotations | default dict) "storageclass.kubernetes.io/is-default-class") "true" }}
|
||||
{{- $default = true }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not $default }}
|
||||
{{- fail "The cluster has no default StorageClass, so the volumes would never bind. Install a storage provisioner and mark its class as default, or set persistence.storageClassName on postgres, valkey, nats, meilisearch and seaweedfs" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and (eq $v.edge.expose "ingress") (not $v.edge.ingress.className) }}
|
||||
{{- $classes := (lookup "networking.k8s.io/v1" "IngressClass" "" "").items | default list }}
|
||||
{{- if not $classes }}
|
||||
{{- fail "The cluster has no IngressClass, so nothing would serve the Ingress. Install an ingress controller, or set edge.expose to loadBalancer, gateway or none. Set clusterChecks to false if your controller serves an Ingress without a class" }}
|
||||
{{- end }}
|
||||
{{- $defaults := list }}
|
||||
{{- range $classes }}
|
||||
{{- if eq (get (.metadata.annotations | default dict) "ingressclass.kubernetes.io/is-default-class") "true" }}
|
||||
{{- $defaults = append $defaults .metadata.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not $defaults }}
|
||||
{{- $names := list }}
|
||||
{{- range $classes }}
|
||||
{{- $names = append $names .metadata.name }}
|
||||
{{- end }}
|
||||
{{- fail (printf "The cluster has no default IngressClass. Set edge.ingress.className to one of: %s" (join ", " $names)) }}
|
||||
{{- end }}
|
||||
{{- if gt (len $defaults) 1 }}
|
||||
{{- fail (printf "The cluster marks more than one IngressClass as default (%s), so the Ingress may land on the wrong controller. Set edge.ingress.className" (join ", " $defaults)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,188 @@
|
||||
{{- define "fluxer.replicas" -}}
|
||||
{{- $w := .w | default dict }}
|
||||
{{- if kindIs "invalid" $w.replicas }}1{{ else }}{{ $w.replicas | int }}{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.appPod" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w | default dict }}
|
||||
{{- $readsSecret := and (not .noEnvFrom) (not .configMapOnly) }}
|
||||
{{- $env := .env | default list }}
|
||||
{{- if and $readsSecret $root.Values.valkey.enabled }}
|
||||
{{- $env = prepend $env (dict "name" "FLUXER_KV_URL" "value" "redis://:$(FLUXER_VALKEY_PASSWORD)@valkey:6379/0") }}
|
||||
{{- end }}
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" . | nindent 4 }}
|
||||
annotations:
|
||||
checksum/instance: {{ include "fluxer.instanceChecksum" $root }}
|
||||
{{- with $w.podAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" $root | nindent 2 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict) | nindent 4 }}
|
||||
{{- include "fluxer.scheduling" . | nindent 2 }}
|
||||
{{- with $w.terminationGracePeriodSeconds }}
|
||||
terminationGracePeriodSeconds: {{ . }}
|
||||
{{- end }}
|
||||
{{- include "fluxer.waitFor" . | nindent 2 }}
|
||||
containers:
|
||||
- name: {{ .containerName | default .name }}
|
||||
image: {{ include "fluxer.image" . }}
|
||||
imagePullPolicy: {{ include "fluxer.pullPolicy" . }}
|
||||
{{- with .command }}
|
||||
command:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with include "fluxer.envList" (dict "w" $w "env" $env) }}
|
||||
env:
|
||||
{{- . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if not .noEnvFrom }}
|
||||
envFrom:
|
||||
{{- if .configMapOnly }}
|
||||
- configMapRef:
|
||||
name: {{ .configMapOnly }}
|
||||
{{- else }}
|
||||
- configMapRef:
|
||||
name: fluxer-config
|
||||
- secretRef:
|
||||
name: {{ include "fluxer.secretName" $root }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .port }}
|
||||
protocol: TCP
|
||||
{{- range .extraPorts }}
|
||||
- name: {{ .name }}
|
||||
containerPort: {{ .port }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- with include "fluxer.probes" . | trim }}
|
||||
{{- . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .lifecycle }}
|
||||
lifecycle:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $w.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 8 }}
|
||||
{{- with concat (.volumeMounts | default list) ($w.extraVolumeMounts | default list) }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with concat (.volumes | default list) ($w.extraVolumes | default list) }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.app" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $w := .w | default dict }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: {{ .kind | default "Deployment" }}
|
||||
metadata:
|
||||
name: {{ .name }}
|
||||
namespace: {{ $root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not (and $w.hpa $w.hpa.maxReplicas) }}
|
||||
replicas: {{ include "fluxer.replicas" . }}
|
||||
{{- end }}
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" . | nindent 6 }}
|
||||
{{- if eq (.kind | default "Deployment") "StatefulSet" }}
|
||||
serviceName: {{ .serviceName }}
|
||||
podManagementPolicy: Parallel
|
||||
{{- else if .strategy }}
|
||||
strategy:
|
||||
{{- toYaml .strategy | nindent 4 }}
|
||||
{{- end }}
|
||||
template:
|
||||
{{- include "fluxer.appPod" . | nindent 4 }}
|
||||
{{- if not .noService }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .name }}
|
||||
namespace: {{ $root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
{{- if .headless }}
|
||||
clusterIP: None
|
||||
publishNotReadyAddresses: true
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .servicePort | default .port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
{{- with $w.pdb }}
|
||||
{{- if or (not (kindIs "invalid" .minAvailable)) (not (kindIs "invalid" .maxUnavailable)) }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $.name }}
|
||||
namespace: {{ $root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
{{- if not (kindIs "invalid" .minAvailable) }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if not (kindIs "invalid" .maxUnavailable) }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $w.hpa }}
|
||||
{{- if .maxReplicas }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ $.name }}
|
||||
namespace: {{ $root.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: {{ $.kind | default "Deployment" }}
|
||||
name: {{ $.name }}
|
||||
minReplicas: {{ .minReplicas | default 1 }}
|
||||
maxReplicas: {{ .maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .targetCPUUtilizationPercentage | default 80 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,90 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- $v := .Values }}
|
||||
{{- $o := include "fluxer.origin" . }}
|
||||
{{- $http := dict "port" "http" }}
|
||||
{{- $start := dict "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 30 }}
|
||||
{{- $check := dict "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3 }}
|
||||
{{- $health := merge (dict "path" "/_health") $http }}
|
||||
{{- $healthProbes := dict "startup" (merge (dict "httpGet" $health) $start) "liveness" (merge (dict "httpGet" $health) $check) "readiness" (merge (dict "httpGet" $health) $check) }}
|
||||
{{- $nodeEnv := list (dict "name" "NODE_ENV" "value" "production") (dict "name" "NODE_OPTIONS" "value" "--enable-source-maps") (dict "name" "NODE_EXTRA_CA_CERTS" "value" "/etc/ssl/certs/ca-certificates.crt") (dict "name" "FLUXER_POSTGRES_MAX_CONNECTIONS" "value" "25") }}
|
||||
|
||||
{{- include "fluxer.app" (dict "root" . "name" "api" "component" "api" "w" $v.api "image" "fluxer-api" "port" 8080
|
||||
"env" (concat (list (dict "name" "FLUXER_API_PORT" "value" "8080") (dict "name" "FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED" "value" "true")) $nodeEnv)
|
||||
"probes" $healthProbes
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" . "usesNats" true "usesPostgres" true "usesS3" true "usesKv" true "usesSvc" true) | fromJsonArray)) }}
|
||||
|
||||
{{- $heartbeat := dict "exec" (dict "command" (list "node" "-e" "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}")) "initialDelaySeconds" 90 "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3 }}
|
||||
{{- include "fluxer.app" (dict "root" . "name" "worker" "component" "worker" "w" $v.worker "image" "fluxer-api" "port" 8080 "noService" true
|
||||
"command" (list "node" "dist/WorkerEntrypoint.js")
|
||||
"env" (concat (list (dict "name" "FLUXER_API_WORKER_MODE" "value" "all_lanes") (dict "name" "FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER" "value" "true")) $nodeEnv)
|
||||
"probes" (dict "liveness" $heartbeat)
|
||||
"strategy" (dict "type" "Recreate")
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" . "usesNats" true "usesPostgres" true "usesS3" true "usesKv" true "usesSvc" true) | fromJsonArray)) }}
|
||||
|
||||
{{- $gatewayEnv := list
|
||||
(dict "name" "FLUXER_GATEWAY_PORT" "value" "8080")
|
||||
(dict "name" "FLUXER_GATEWAY_ROLE" "value" "all")
|
||||
(dict "name" "POD_IP" "valueFrom" (dict "fieldRef" (dict "apiVersion" "v1" "fieldPath" "status.podIP")))
|
||||
(dict "name" "FLUXER_ERLANG_NODE_NAME" "value" "fluxer_gateway@$(POD_IP)")
|
||||
(dict "name" "FLUXER_ERLANG_DIST_PORT" "value" "8081")
|
||||
(dict "name" "FLUXER_GATEWAY_CLUSTER_ENABLED" "value" "true")
|
||||
(dict "name" "FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME" "value" (printf "gateway-headless.%s.svc.%s" .Release.Namespace $v.clusterDomain))
|
||||
(dict "name" "FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME" "value" "fluxer_gateway") }}
|
||||
{{- $gatewayReady := dict "exec" (dict "command" (list "curl" "-fsS" "-o" "/dev/null" "--max-time" "4" "http://127.0.0.1:8080/_health/ready")) "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3 }}
|
||||
{{- $gatewayCtx := dict "root" . "name" "gateway" "component" "gateway" "w" $v.gateway "image" "fluxer-gateway" "port" 8080
|
||||
"extraPorts" (list (dict "name" "epmd" "port" 4369) (dict "name" "erl-dist" "port" 8081))
|
||||
"env" $gatewayEnv
|
||||
"probes" (dict "startup" (merge (dict "httpGet" $health) $start) "liveness" (merge (dict "httpGet" $health) $check) "readiness" $gatewayReady)
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" . "usesNats" true) | fromJsonArray)
|
||||
"lifecycle" (dict "preStop" (dict "exec" (dict "command" (list "/bin/sh" "-c" "curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8080/_health/drain; sleep 5")))) }}
|
||||
{{- include "fluxer.app" $gatewayCtx }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gateway-headless
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $gatewayCtx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
publishNotReadyAddresses: true
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $gatewayCtx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
- name: epmd
|
||||
port: 4369
|
||||
targetPort: epmd
|
||||
protocol: TCP
|
||||
- name: erl-dist
|
||||
port: 8081
|
||||
targetPort: erl-dist
|
||||
protocol: TCP
|
||||
|
||||
{{- include "fluxer.app" (dict "root" . "name" "media-proxy" "component" "media" "w" $v.mediaProxy "image" "fluxer-media-proxy" "port" 8080
|
||||
"env" (list (dict "name" "FLUXER_MEDIA_PROXY_PORT" "value" "8080") (dict "name" "FLUXER_MEDIA_PROXY_MODE" "value" "upload") (dict "name" "FLUXER_MEDIA_PROXY_STORAGE_BACKEND" "value" "s3") (dict "name" "FLUXER_S3_READ_SIGNED" "value" "true"))
|
||||
"probes" $healthProbes
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" . "usesNats" true "usesS3" true) | fromJsonArray)) }}
|
||||
|
||||
{{- $pushCheck := dict "exec" (dict "command" (list "/usr/local/bin/fluxer-push" "healthcheck")) }}
|
||||
{{- include "fluxer.app" (dict "root" . "name" "push" "component" "push" "w" $v.push "image" "fluxer-push" "port" 8126
|
||||
"env" (list (dict "name" "FLUXER_PUSH_SERVICE_PORT" "value" "8126"))
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" . "usesNats" true) | fromJsonArray)
|
||||
"probes" (dict "startup" (merge (deepCopy $pushCheck) (dict "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 12)) "liveness" (merge (deepCopy $pushCheck) $check) "readiness" (merge (deepCopy $pushCheck) $check))) }}
|
||||
|
||||
{{- include "fluxer.app" (dict "root" . "name" "admin" "component" "admin" "w" $v.admin "image" "fluxer-admin" "port" 8080
|
||||
"env" (list (dict "name" "FLUXER_ADMIN_PORT" "value" "8080") (dict "name" "FLUXER_ADMIN_BASE_PATH" "value" $v.adminBasePath) (dict "name" "FLUXER_API_ENDPOINT" "value" "http://api:8080") (dict "name" "FLUXER_STATIC_CDN_ENDPOINT" "value" $o))
|
||||
"probes" $healthProbes) }}
|
||||
|
||||
{{- $ready := merge (dict "path" "/_ready") $http }}
|
||||
{{- include "fluxer.app" (dict "root" . "name" "app-proxy" "component" "web" "w" $v.appProxy "image" "fluxer-app-proxy-self-hosted" "port" 8080 "configMapOnly" "fluxer-app-proxy"
|
||||
"probes" (dict "startup" (merge (dict "httpGet" $health) $start) "liveness" (merge (dict "httpGet" $health) $check) "readiness" (merge (dict "httpGet" $ready) $check))) }}
|
||||
|
||||
{{- $avatar := merge (dict "path" "/avatars/0.png") $http }}
|
||||
{{- include "fluxer.app" (dict "root" . "name" "static-proxy" "component" "web" "w" $v.staticProxy "image" "fluxer-static" "port" 8080 "noEnvFrom" true
|
||||
"probes" (dict "startup" (merge (dict "httpGet" $avatar) $start) "liveness" (merge (dict "httpGet" $avatar) $check) "readiness" (merge (dict "httpGet" $avatar) $check))) }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fluxer-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-config" "component" "config") | nindent 4 }}
|
||||
data:
|
||||
{{- include "fluxer.configData" . | nindent 2 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fluxer-app-proxy
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-app-proxy" "component" "config") | nindent 4 }}
|
||||
data:
|
||||
{{- include "fluxer.appProxyConfigData" . | nindent 2 }}
|
||||
@@ -0,0 +1,296 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- $v := .Values }}
|
||||
{{- $e := $v.edge }}
|
||||
{{- $ctx := dict "root" . "name" "edge" "component" "edge" "w" $e }}
|
||||
{{- $lb := eq $e.expose "loadBalancer" }}
|
||||
{{- $caddyfile := .Files.Get "files/Caddyfile" }}
|
||||
{{- $globalOptions := ternary (printf "email %s" $v.email) "" (and $lb (ne $v.email "")) }}
|
||||
{{- $siteAddress := ternary (printf "https://%s, :8080" $v.domain) ":8080" $lb }}
|
||||
{{- $trusted := ternary "127.0.0.1/32" "private_ranges" $lb }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: edge-caddyfile
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
data:
|
||||
Caddyfile: {{ $caddyfile | quote }}
|
||||
{{- if $lb }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: edge-data
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- with $e.loadBalancer.persistence.storageClassName }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ $e.loadBalancer.persistence.size }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: edge
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ ternary 1 ($e.replicas | int) $lb }}
|
||||
revisionHistoryLimit: 3
|
||||
{{- if $lb }}
|
||||
strategy:
|
||||
type: Recreate
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ printf "%s\n%s\n%s\n%s" $caddyfile $siteAddress $globalOptions (toJson (list $e.env $e.extraEnv)) | sha256sum }}
|
||||
{{- with $e.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" . | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 65532) | nindent 8 }}
|
||||
{{- if $lb }}
|
||||
sysctls:
|
||||
- name: net.ipv4.ip_unprivileged_port_start
|
||||
value: "0"
|
||||
{{- end }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: edge
|
||||
image: {{ include "fluxer.thirdPartyImage" $e.image }}
|
||||
imagePullPolicy: {{ $e.image.pullPolicy }}
|
||||
{{- $edgeEnv := list (dict "name" "FLUXER_EDGE_SITE_ADDRESS" "value" $siteAddress) (dict "name" "FLUXER_EDGE_TRUSTED_PROXIES" "value" $trusted) (dict "name" "FLUXER_EDGE_GLOBAL_OPTIONS" "value" $globalOptions) (dict "name" "FLUXER_ADMIN_BASE_PATH" "value" $v.adminBasePath) }}
|
||||
env:
|
||||
{{- include "fluxer.envList" (dict "w" $e "env" $edgeEnv) | nindent 12 }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
{{- if $lb }}
|
||||
- name: public-http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
- name: public-https
|
||||
containerPort: 443
|
||||
protocol: TCP
|
||||
- name: public-quic
|
||||
containerPort: 443
|
||||
protocol: UDP
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
|
||||
periodSeconds: 5
|
||||
failureThreshold: 24
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
{{- with $e.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
volumeMounts:
|
||||
- name: caddyfile
|
||||
mountPath: /etc/caddy
|
||||
readOnly: true
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: caddyfile
|
||||
configMap:
|
||||
name: edge-caddyfile
|
||||
- name: config
|
||||
emptyDir: {}
|
||||
- name: data
|
||||
{{- if $lb }}
|
||||
persistentVolumeClaim:
|
||||
claimName: edge-data
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: edge
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- if $lb }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: edge-public
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
{{- with $e.loadBalancer.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
externalTrafficPolicy: Local
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: public-http
|
||||
protocol: TCP
|
||||
- name: https
|
||||
port: 443
|
||||
targetPort: public-https
|
||||
protocol: TCP
|
||||
{{- if $e.loadBalancer.http3 }}
|
||||
- name: quic
|
||||
port: 443
|
||||
targetPort: public-quic
|
||||
protocol: UDP
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if eq $e.expose "ingress" }}
|
||||
{{- $tls := $e.ingress.tls }}
|
||||
{{- $annotations := dict
|
||||
"nginx.ingress.kubernetes.io/proxy-body-size" "512m"
|
||||
"nginx.ingress.kubernetes.io/proxy-read-timeout" "3600"
|
||||
"nginx.ingress.kubernetes.io/proxy-send-timeout" "3600" }}
|
||||
{{- if eq $tls.mode "certManager" }}
|
||||
{{- $_ := set $annotations "cert-manager.io/issuer" "fluxer-acme" }}
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: fluxer-acme
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
acme:
|
||||
server: {{ $tls.acmeServer }}
|
||||
{{- with $v.email }}
|
||||
email: {{ . }}
|
||||
{{- end }}
|
||||
privateKeySecretRef:
|
||||
name: fluxer-acme-account
|
||||
solvers:
|
||||
- http01:
|
||||
{{- with $e.ingress.className }}
|
||||
ingress:
|
||||
ingressClassName: {{ . }}
|
||||
{{- else }}
|
||||
ingress: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $annotations = merge (deepCopy ($e.ingress.annotations | default dict)) $annotations }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: fluxer
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
{{- with $annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with $e.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if ne $tls.mode "none" }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $v.domain }}
|
||||
secretName: {{ $tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ $v.domain }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: edge
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- if eq $e.expose "gateway" }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: fluxer
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- toYaml $e.gateway.parentRefs | nindent 4 }}
|
||||
hostnames:
|
||||
- {{ $v.domain }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: edge
|
||||
port: 8080
|
||||
{{- with $e.gateway.timeouts }}
|
||||
timeouts:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,156 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.livekit }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "livekit" "component" "voice" "w" . }}
|
||||
{{- $tcp := .tcpPort | int }}
|
||||
{{- $udp := .udpPort | int }}
|
||||
{{- if eq .expose "nodePort" }}
|
||||
{{- $tcp = .nodePorts.tcp | int }}
|
||||
{{- $udp = .nodePorts.udp | int }}
|
||||
{{- end }}
|
||||
{{- $useExternal := and .useExternalIP (ne .expose "loadBalancer") }}
|
||||
{{- $secrets := include "fluxer.secretData" $ | fromJson }}
|
||||
{{- $config := dict
|
||||
"port" 7880
|
||||
"log_level" .logLevel
|
||||
"rtc" (dict "tcp_port" $tcp "udp_port" $udp "use_external_ip" $useExternal "node_ip" (.nodeIP | default "") "stun_servers" .stunServers) }}
|
||||
{{- $config = mergeOverwrite $config (deepCopy (.extraConfig | default dict)) }}
|
||||
{{- $configYaml := toYaml $config }}
|
||||
{{- $root := dict "path" "/" "port" "http" }}
|
||||
{{- $webhook := ternary "http://edge:8080/api/webhooks/livekit" "http://api:8080/webhooks/livekit" (eq .expose "hostNetwork") }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: livekit
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 7880
|
||||
targetPort: 7880
|
||||
protocol: TCP
|
||||
{{- if ne .expose "hostNetwork" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: livekit-rtc
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
{{- with .serviceAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "NodePort" (eq .expose "loadBalancer") }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if and (eq .expose "loadBalancer") .loadBalancerIP }}
|
||||
loadBalancerIP: {{ .loadBalancerIP }}
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: rtc-tcp
|
||||
port: {{ $tcp }}
|
||||
targetPort: rtc-tcp
|
||||
protocol: TCP
|
||||
{{- if eq .expose "nodePort" }}
|
||||
nodePort: {{ $tcp }}
|
||||
{{- end }}
|
||||
- name: rtc-udp
|
||||
port: {{ $udp }}
|
||||
targetPort: rtc-udp
|
||||
protocol: UDP
|
||||
{{- if eq .expose "nodePort" }}
|
||||
nodePort: {{ $udp }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: livekit
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ list $configYaml $webhook (get $secrets "FLUXER_LIVEKIT_API_KEY" | default "") (get $secrets "FLUXER_LIVEKIT_API_SECRET" | default "") | toJson | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- if eq .expose "hostNetwork" }}
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
{{- end }}
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 65532) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: livekit
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
export LIVEKIT_KEYS="${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
|
||||
export LIVEKIT_CONFIG="$(printf '%s\nwebhook:\n api_key: %s\n urls:\n - %s\n' "$LIVEKIT_CONFIG" "$LIVEKIT_API_KEY" "$LIVEKIT_WEBHOOK_URL")"
|
||||
exec /livekit-server
|
||||
env:
|
||||
- name: LIVEKIT_CONFIG
|
||||
value: {{ $configYaml | quote }}
|
||||
- name: LIVEKIT_WEBHOOK_URL
|
||||
value: {{ $webhook }}
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "LIVEKIT_API_KEY" "key" "FLUXER_LIVEKIT_API_KEY") | nindent 12 }}
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "LIVEKIT_API_SECRET" "key" "FLUXER_LIVEKIT_API_SECRET") | nindent 12 }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 7880
|
||||
protocol: TCP
|
||||
- name: rtc-tcp
|
||||
containerPort: {{ $tcp }}
|
||||
protocol: TCP
|
||||
- name: rtc-udp
|
||||
containerPort: {{ $udp }}
|
||||
protocol: UDP
|
||||
startupProbe:
|
||||
httpGet: {{ toJson $root }}
|
||||
periodSeconds: 5
|
||||
failureThreshold: 60
|
||||
livenessProbe:
|
||||
httpGet: {{ toJson $root }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet: {{ toJson $root }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,97 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.meilisearch }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "meilisearch" "component" "search" "w" . }}
|
||||
{{- $health := dict "path" "/health" "port" "http" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: meilisearch
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 7700
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: meilisearch
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: meilisearch
|
||||
podManagementPolicy: Parallel
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 65532) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: meilisearch
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
env:
|
||||
- name: MEILI_ENV
|
||||
value: production
|
||||
- name: MEILI_NO_ANALYTICS
|
||||
value: "true"
|
||||
- name: MEILI_UPGRADE_DB
|
||||
value: "true"
|
||||
- name: MEILI_MAX_INDEXING_MEMORY
|
||||
value: {{ .maxIndexingMemory | quote }}
|
||||
- name: MEILI_MAX_INDEXING_THREADS
|
||||
value: {{ include "fluxer.string" .maxIndexingThreads | quote }}
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "MEILI_MASTER_KEY" "key" "FLUXER_SEARCH_API_KEY") | nindent 12 }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 7700
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
httpGet: {{ toJson $health }}
|
||||
periodSeconds: 5
|
||||
failureThreshold: 120
|
||||
livenessProbe:
|
||||
httpGet: {{ toJson $health }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet: {{ toJson $health }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /meili_data
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer.pvc" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,169 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.nats }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "nats" "component" "messaging" "w" . }}
|
||||
{{- $replicas := .replicas | int }}
|
||||
{{- $conf := include "fluxer.natsConf" (dict "root" $ "replicas" $replicas) }}
|
||||
{{- $secrets := include "fluxer.secretData" $ | fromJson }}
|
||||
{{- $healthz := dict "exec" (dict "command" (list "wget" "-q" "-O" "/dev/null" "http://127.0.0.1:8222/healthz")) }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: nats-config
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
data:
|
||||
nats.conf: {{ $conf | quote }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nats
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: client
|
||||
port: 4222
|
||||
targetPort: client
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nats-headless
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
publishNotReadyAddresses: true
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: client
|
||||
port: 4222
|
||||
targetPort: client
|
||||
protocol: TCP
|
||||
- name: cluster
|
||||
port: 6222
|
||||
targetPort: cluster
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: nats
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $replicas }}
|
||||
serviceName: nats-headless
|
||||
podManagementPolicy: Parallel
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ printf "%s\n%s" $conf (get $secrets "FLUXER_NATS_AUTH_TOKEN" | default "") | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 65534) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: nats
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- sed "s/__FLUXER_NATS_AUTH_TOKEN__/$FLUXER_NATS_AUTH_TOKEN/g" /etc/nats/nats.conf > /run/nats/nats.conf && exec nats-server --config /run/nats/nats.conf
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.name
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "FLUXER_NATS_AUTH_TOKEN" "key" "FLUXER_NATS_AUTH_TOKEN") | nindent 12 }}
|
||||
ports:
|
||||
- name: client
|
||||
containerPort: 4222
|
||||
protocol: TCP
|
||||
- name: cluster
|
||||
containerPort: 6222
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
{{- toYaml (merge (dict "periodSeconds" 5 "timeoutSeconds" 5 "failureThreshold" 60) $healthz) | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- toYaml (merge (dict "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 6) $healthz) | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml (merge (dict "periodSeconds" 10 "timeoutSeconds" 5) $healthz) | nindent 12 }}
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/nats
|
||||
readOnly: true
|
||||
- name: rendered
|
||||
mountPath: /run/nats
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: nats-config
|
||||
- name: rendered
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer.pvc" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "fluxer.natsConf" -}}
|
||||
server_name: $POD_NAME
|
||||
listen: 0.0.0.0:4222
|
||||
http: 127.0.0.1:8222
|
||||
authorization {
|
||||
token: "__FLUXER_NATS_AUTH_TOKEN__"
|
||||
}
|
||||
jetstream {
|
||||
store_dir: /data
|
||||
}
|
||||
{{- if gt .replicas 1 }}
|
||||
cluster {
|
||||
name: fluxer
|
||||
listen: 0.0.0.0:6222
|
||||
authorization {
|
||||
user: fluxer
|
||||
password: "__FLUXER_NATS_AUTH_TOKEN__"
|
||||
}
|
||||
routes: [
|
||||
{{- range $i := until .replicas }}
|
||||
nats://fluxer:__FLUXER_NATS_AUTH_TOKEN__@nats-{{ $i }}.nats-headless.{{ $.root.Release.Namespace }}.svc.{{ $.root.Values.clusterDomain }}:6222
|
||||
{{- end }}
|
||||
]
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,83 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
{{- $v := .Values }}
|
||||
{{- $release := dict "app.kubernetes.io/instance" .Release.Name }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fluxer
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer" "component" "network") | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- toYaml $release | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
{{- toYaml $release | nindent 14 }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fluxer-edge
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-edge" "component" "network") | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" (dict "root" . "name" "edge") | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
{{- with $v.networkPolicy.ingressNamespaceSelector }}
|
||||
from:
|
||||
- namespaceSelector:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- if and $v.livekit.enabled (eq $v.livekit.expose "hostNetwork") }}
|
||||
{{- range $v.livekit.nodeCIDRs }}
|
||||
- ipBlock:
|
||||
cidr: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if eq $v.edge.expose "loadBalancer" }}
|
||||
- ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- port: 443
|
||||
protocol: UDP
|
||||
{{- end }}
|
||||
{{- if and $v.livekit.enabled (ne $v.livekit.expose "hostNetwork") }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: fluxer-livekit
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-livekit" "component" "network") | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" (dict "root" . "name" "livekit") | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- ports:
|
||||
- port: {{ ternary $v.livekit.nodePorts.tcp $v.livekit.tcpPort (eq $v.livekit.expose "nodePort") }}
|
||||
protocol: TCP
|
||||
- port: {{ ternary $v.livekit.nodePorts.udp $v.livekit.udpPort (eq $v.livekit.expose "nodePort") }}
|
||||
protocol: UDP
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,147 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.postgres }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "postgres" "component" "database" "w" . }}
|
||||
{{- $args := list }}
|
||||
{{- range $k := keys .parameters | sortAlpha }}
|
||||
{{- $args = concat $args (list "-c" (printf "%s=%s" $k (include "fluxer.string" (get $.Values.postgres.parameters $k)))) }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: postgres
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: postgres
|
||||
podManagementPolicy: Parallel
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ toJson $args | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
terminationGracePeriodSeconds: 60
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" (.runAsUser | int)) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
initContainers:
|
||||
- name: version-guard
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
want=$(postgres --version | sed -E 's/[^0-9]*([0-9]+).*/\1/')
|
||||
if [ -s "$PGDATA/PG_VERSION" ]; then
|
||||
have=$(cat "$PGDATA/PG_VERSION")
|
||||
if [ "$have" != "$want" ]; then
|
||||
echo "The data directory was created by Postgres $have and this image runs Postgres $want. Set postgres.image.tag back to the $have image, or dump the database and restore it into a fresh volume. See the Kubernetes page of the operator docs." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
env:
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
containers:
|
||||
- name: postgres
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml $args | nindent 12 }}
|
||||
env:
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
- name: POSTGRES_DB
|
||||
value: {{ .database | quote }}
|
||||
- name: POSTGRES_USER
|
||||
value: {{ .username | quote }}
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "POSTGRES_PASSWORD" "key" "FLUXER_POSTGRES_PASSWORD") | nindent 12 }}
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: shm
|
||||
mountPath: /dev/shm
|
||||
- name: run
|
||||
mountPath: /var/run/postgresql
|
||||
volumes:
|
||||
- name: shm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: {{ .shmSize }}
|
||||
- name: run
|
||||
emptyDir: {}
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer.pvc" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,136 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.seaweedfs }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "seaweedfs" "component" "storage" "w" . }}
|
||||
{{- $healthz := dict "path" "/healthz" "port" "s3" }}
|
||||
{{- $secrets := include "fluxer.secretData" $ | fromJson }}
|
||||
{{- $init := $.Files.Get "files/seaweedfs-init.sh" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: seaweedfs
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: s3
|
||||
port: 8333
|
||||
targetPort: s3
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: seaweedfs
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: seaweedfs
|
||||
podManagementPolicy: Parallel
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/init: {{ list $init (get $secrets "FLUXER_S3_ACCESS_KEY_ID" | default "") (get $secrets "FLUXER_S3_SECRET_ACCESS_KEY" | default "") $.Values.s3.buckets | toJson | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 1000) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: seaweedfs
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
args:
|
||||
- server
|
||||
- -s3
|
||||
- -dir=/data
|
||||
- -master.telemetry=false
|
||||
- -ip=127.0.0.1
|
||||
- -ip.bind=127.0.0.1
|
||||
- -s3.ip.bind=0.0.0.0
|
||||
env:
|
||||
- name: GOMEMLIMIT
|
||||
value: {{ .gomemlimit | quote }}
|
||||
- name: WEED_MASTER_VOLUME_GROWTH_COPY_1
|
||||
value: {{ include "fluxer.string" .volumeGrowth | quote }}
|
||||
ports:
|
||||
- name: s3
|
||||
containerPort: 8333
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
httpGet: {{ toJson $healthz }}
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 24
|
||||
livenessProbe:
|
||||
httpGet: {{ toJson $healthz }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet: {{ toJson $healthz }}
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: seaweedfs-init
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- {{ $init | quote }}
|
||||
env:
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "FLUXER_S3_ACCESS_KEY" "key" "FLUXER_S3_ACCESS_KEY_ID") | nindent 12 }}
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "FLUXER_S3_SECRET_KEY" "key" "FLUXER_S3_SECRET_ACCESS_KEY") | nindent 12 }}
|
||||
- name: FLUXER_S3_BUCKET_CDN
|
||||
value: {{ $.Values.s3.buckets.cdn | quote }}
|
||||
- name: FLUXER_S3_BUCKET_UPLOADS
|
||||
value: {{ $.Values.s3.buckets.uploads | quote }}
|
||||
- name: FLUXER_S3_BUCKET_REPORTS
|
||||
value: {{ $.Values.s3.buckets.reports | quote }}
|
||||
- name: FLUXER_S3_BUCKET_HARVESTS
|
||||
value: {{ $.Values.s3.buckets.harvests | quote }}
|
||||
- name: FLUXER_SEAWEEDFS_INIT_ATTEMPTS
|
||||
value: {{ include "fluxer.string" .initAttempts | quote }}
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["test", "-f", "/tmp/fluxer-buckets-ready"]
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer.pvc" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- if not .Values.secrets.existingSecret }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: fluxer-env
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-env" "component" "config") | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $k, $val := include "fluxer.secretData" . | fromJson }}
|
||||
{{ $k }}: {{ $val | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- $healthz := dict "path" "/_healthz" "port" "http" }}
|
||||
{{- $health := dict "path" "/_health" "port" "http" }}
|
||||
{{- $probes := dict
|
||||
"startup" (dict "httpGet" $healthz "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 12)
|
||||
"liveness" (dict "httpGet" $healthz "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3)
|
||||
"readiness" (dict "httpGet" $health "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3) }}
|
||||
{{- range $name := list "gifs" "messages" "snowflakes" "unfurl" "users" }}
|
||||
{{- $svc := get $.Values.svc $name | default dict }}
|
||||
{{- $router := $svc.router | default dict }}
|
||||
{{- $shard := $svc.shard | default dict }}
|
||||
{{- $count := include "fluxer.replicas" (dict "w" $shard) }}
|
||||
{{- $common := list (dict "name" "FLUXER_SVC_NAME" "value" $name) (dict "name" "FLUXER_SVC_PORT" "value" "8090") (dict "name" "FLUXER_SVC_SHARD_COUNT" "value" $count) }}
|
||||
{{- $shardEnv := concat $common (list (dict "name" "FLUXER_SVC_MODE" "value" "shard") (dict "name" "FLUXER_SVC_SHARD_ID" "valueFrom" (dict "fieldRef" (dict "apiVersion" "v1" "fieldPath" "metadata.labels['apps.kubernetes.io/pod-index']"))) (dict "name" "POD_NAME" "valueFrom" (dict "fieldRef" (dict "apiVersion" "v1" "fieldPath" "metadata.name")))) }}
|
||||
{{- $usesPostgres := has $name (list "users" "messages") }}
|
||||
{{- if $usesPostgres }}
|
||||
{{- $shardEnv = append $shardEnv (dict "name" "FLUXER_POSTGRES_MAX_CONNECTIONS" "value" "20") }}
|
||||
{{- end }}
|
||||
{{- if eq $name "unfurl" }}
|
||||
{{- $shardEnv = append $shardEnv (dict "name" "FLUXER_MEDIA_PROXY_ENDPOINT" "value" "http://media-proxy:8080") }}
|
||||
{{- end }}
|
||||
{{- include "fluxer.app" (dict "root" $ "name" $name "component" "svc" "w" $router "image" (printf "fluxer-%s" $name) "port" 8090 "containerName" "router"
|
||||
"env" (append $common (dict "name" "FLUXER_SVC_MODE" "value" "router"))
|
||||
"probes" $probes
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" $ "usesNats" true) | fromJsonArray)) }}
|
||||
{{- include "fluxer.app" (dict "root" $ "name" (printf "%s-shard" $name) "component" "svc" "w" $shard "image" (printf "fluxer-%s" $name) "port" 8090 "containerName" "shard"
|
||||
"kind" "StatefulSet" "serviceName" (printf "%s-shard" $name) "headless" true
|
||||
"env" $shardEnv
|
||||
"probes" $probes
|
||||
"waitFor" (include "fluxer.appWaitFor" (dict "root" $ "usesNats" true "usesPostgres" $usesPostgres) | fromJsonArray)) }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,46 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: fluxer-test-edge
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" (dict "root" . "name" "fluxer-test" "component" "test") | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/hook: test
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" . | nindent 2 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 65532) | nindent 4 }}
|
||||
containers:
|
||||
- name: check
|
||||
image: {{ include "fluxer.thirdPartyImage" .Values.edge.image }}
|
||||
imagePullPolicy: {{ .Values.edge.image.pullPolicy }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
for i in $(seq 1 30); do
|
||||
if wget -q -O /dev/null http://edge:8080/_health && wget -q -O - --header "Host: $DOMAIN" http://edge:8080/.well-known/fluxer > /tmp/discovery && grep -q "$DOMAIN" /tmp/discovery; then
|
||||
echo "edge and discovery answer for $DOMAIN"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "edge or discovery did not answer for $DOMAIN within 60 seconds" >&2
|
||||
exit 1
|
||||
env:
|
||||
- name: DOMAIN
|
||||
value: {{ .Values.domain | quote }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 8 }}
|
||||
@@ -0,0 +1,84 @@
|
||||
{{- include "fluxer.validate" . }}
|
||||
{{- with .Values.valkey }}
|
||||
{{- if .enabled }}
|
||||
{{- $ctx := dict "root" $ "name" "valkey" "component" "cache" "w" . }}
|
||||
{{- $args := list "valkey-server" "--appendonly" "yes" "--appendfsync" .appendfsync "--dir" "/data" "--maxmemory" .maxmemory "--maxmemory-policy" .maxmemoryPolicy "--requirepass" "$(REDISCLI_AUTH)" }}
|
||||
{{- $ping := dict "exec" (dict "command" (list "sh" "-c" "valkey-cli ping | grep -q PONG")) "periodSeconds" 10 "timeoutSeconds" 5 }}
|
||||
{{- $secrets := include "fluxer.secretData" $ | fromJson }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: valkey
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 4 }}
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: redis
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: valkey
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "fluxer.labels" $ctx | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: valkey
|
||||
podManagementPolicy: Parallel
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "fluxer.selectorLabels" $ctx | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "fluxer.podLabels" $ctx | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ printf "%s\n%s" (toJson $args) (get $secrets "FLUXER_VALKEY_PASSWORD" | default "") | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
{{- include "fluxer.pullSecrets" $ | nindent 6 }}
|
||||
securityContext:
|
||||
{{- include "fluxer.podSecurityContext" (dict "uid" 999 "gid" 1000) | nindent 8 }}
|
||||
{{- include "fluxer.scheduling" $ctx | nindent 6 }}
|
||||
containers:
|
||||
- name: valkey
|
||||
image: {{ include "fluxer.thirdPartyImage" .image }}
|
||||
imagePullPolicy: {{ .image.pullPolicy }}
|
||||
command:
|
||||
{{- toYaml $args | nindent 12 }}
|
||||
env:
|
||||
{{- include "fluxer.secretEnv" (dict "root" $ "name" "REDISCLI_AUTH" "key" "FLUXER_VALKEY_PASSWORD") | nindent 12 }}
|
||||
ports:
|
||||
- name: redis
|
||||
containerPort: 6379
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml (merge (dict "failureThreshold" 6 "initialDelaySeconds" 10) $ping) | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml (merge (dict "failureThreshold" 3) $ping) | nindent 12 }}
|
||||
{{- with .resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- include "fluxer.containerSecurityContext" . | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumeClaimTemplates:
|
||||
{{- include "fluxer.pvc" .persistence | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,975 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Fluxer chart values",
|
||||
"definitions": {
|
||||
"workload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"registry": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["Always", "IfNotPresent", "Never"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array"
|
||||
},
|
||||
"probes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"startup": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"liveness": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"readiness": {
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"podAnnotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"topologySpreadConstraints": {
|
||||
"type": "array"
|
||||
},
|
||||
"pdb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minAvailable": {
|
||||
"type": ["string", "integer"]
|
||||
},
|
||||
"maxUnavailable": {
|
||||
"type": ["string", "integer"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"hpa": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minReplicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"maxReplicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"targetCPUUtilizationPercentage": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"extraVolumes": {
|
||||
"type": "array"
|
||||
},
|
||||
"extraVolumeMounts": {
|
||||
"type": "array"
|
||||
},
|
||||
"terminationGracePeriodSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"thirdPartyImage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tag": {
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["Always", "IfNotPresent", "Never"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"persistence": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"storageClassName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"svc": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"router": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"shard": {
|
||||
"$ref": "#/definitions/workload"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["domain"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"global": {
|
||||
"type": "object"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Public hostname of the instance, for example chat.example.com"
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"publicOrigin": {
|
||||
"type": "string"
|
||||
},
|
||||
"adminBasePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"clusterDomain": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"clusterChecks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"registry": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tag": {
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["Always", "IfNotPresent", "Never"]
|
||||
},
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"digests": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"pullSecrets": {
|
||||
"type": "array"
|
||||
},
|
||||
"revision": {
|
||||
"type": ["string", "number"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"initImage": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"existingSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"generate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"FLUXER_POSTGRES_PASSWORD": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_SEARCH_API_KEY": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_S3_ACCESS_KEY_ID": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_S3_SECRET_ACCESS_KEY": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_SUDO_MODE_SECRET": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_CONNECTION_INITIATION_SECRET": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_PROFILE_PSEUDONYM_SECRET": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_GATEWAY_RPC_AUTH_TOKEN": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_VALKEY_PASSWORD": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_NATS_AUTH_TOKEN": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_ERLANG_COOKIE": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_MEDIA_PROXY_SECRET_KEY": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_ADMIN_SECRET_KEY_BASE": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_ADMIN_OAUTH_CLIENT_SECRET": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_SETUP_TOKEN": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_LIVEKIT_API_KEY": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_LIVEKIT_API_SECRET": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_VAPID_PUBLIC_KEY": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"FLUXER_VAPID_PRIVATE_KEY": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"extra": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"external": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"ssl": {
|
||||
"type": ["string", "boolean"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"runAsUser": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"shmSize": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"valkey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"external": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"maxmemory": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxmemoryPolicy": {
|
||||
"type": "string"
|
||||
},
|
||||
"appendfsync": {
|
||||
"type": "string",
|
||||
"enum": ["always", "everysec", "no"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"nats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"external": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"meilisearch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"external": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"maxIndexingMemory": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxIndexingThreads": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"seaweedfs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"volumeGrowth": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"gomemlimit": {
|
||||
"type": "string"
|
||||
},
|
||||
"initAttempts": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"s3": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicEndpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"forcePathStyle": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"buckets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cdn": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uploads": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"reports": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"harvests": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"livekit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"external": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"expose": {
|
||||
"type": "string",
|
||||
"enum": ["hostNetwork", "loadBalancer", "nodePort"]
|
||||
},
|
||||
"nodeIP": {
|
||||
"type": "string"
|
||||
},
|
||||
"loadBalancerIP": {
|
||||
"type": "string"
|
||||
},
|
||||
"useExternalIP": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tcpPort": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"udpPort": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"nodePorts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tcp": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"udp": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"stunServers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"logLevel": {
|
||||
"type": "string"
|
||||
},
|
||||
"extraConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"defaultRegion": {
|
||||
"type": "string"
|
||||
},
|
||||
"nodeCIDRs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"serviceAnnotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"api": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"worker": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"gateway": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"mediaProxy": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"push": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"admin": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"appProxy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"registry": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["Always", "IfNotPresent", "Never"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array"
|
||||
},
|
||||
"probes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"startup": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"liveness": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"readiness": {
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"podAnnotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
},
|
||||
"topologySpreadConstraints": {
|
||||
"type": "array"
|
||||
},
|
||||
"pdb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minAvailable": {
|
||||
"type": ["string", "integer"]
|
||||
},
|
||||
"maxUnavailable": {
|
||||
"type": ["string", "integer"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"hpa": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minReplicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"maxReplicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"targetCPUUtilizationPercentage": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"extraVolumes": {
|
||||
"type": "array"
|
||||
},
|
||||
"extraVolumeMounts": {
|
||||
"type": "array"
|
||||
},
|
||||
"terminationGracePeriodSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"staticProxy": {
|
||||
"$ref": "#/definitions/workload"
|
||||
},
|
||||
"svc": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gifs": {
|
||||
"$ref": "#/definitions/svc"
|
||||
},
|
||||
"messages": {
|
||||
"$ref": "#/definitions/svc"
|
||||
},
|
||||
"snowflakes": {
|
||||
"$ref": "#/definitions/svc"
|
||||
},
|
||||
"unfurl": {
|
||||
"$ref": "#/definitions/svc"
|
||||
},
|
||||
"users": {
|
||||
"$ref": "#/definitions/svc"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {
|
||||
"$ref": "#/definitions/thirdPartyImage"
|
||||
},
|
||||
"expose": {
|
||||
"type": "string",
|
||||
"enum": ["ingress", "gateway", "loadBalancer", "none"]
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"className": {
|
||||
"type": "string"
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["certManager", "existingSecret", "none"]
|
||||
},
|
||||
"secretName": {
|
||||
"type": "string"
|
||||
},
|
||||
"acmeServer": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"gateway": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parentRefs": {
|
||||
"type": "array"
|
||||
},
|
||||
"timeouts": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"loadBalancer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"http3": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"persistence": {
|
||||
"$ref": "#/definitions/persistence"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array"
|
||||
},
|
||||
"resources": {
|
||||
"type": "object"
|
||||
},
|
||||
"podAnnotations": {
|
||||
"type": "object"
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array"
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ingressNamespaceSelector": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
# The public hostname of the instance, lowercase, for example chat.example.com.
|
||||
# Required. Point DNS for it at your ingress controller or at the edge load balancer.
|
||||
domain: ""
|
||||
|
||||
# The scheme browsers use to reach the instance.
|
||||
scheme: https
|
||||
|
||||
# The port browsers use to reach the instance.
|
||||
port: 443
|
||||
|
||||
# The origin browsers see, with no trailing slash. Set it only when browsers
|
||||
# reach the instance on an origin that domain, scheme and port do not describe.
|
||||
# It wins over those three in every derived URL.
|
||||
publicOrigin: ""
|
||||
|
||||
# The path the admin panel is served under, with a leading slash and no
|
||||
# trailing slash.
|
||||
adminBasePath: /admin
|
||||
|
||||
# A contact address. It becomes FLUXER_VAPID_EMAIL for web push and the ACME
|
||||
# account email. Empty uses admin@<domain> for web push and no ACME email.
|
||||
email: ""
|
||||
|
||||
# The cluster DNS suffix, used in the gateway discovery name.
|
||||
clusterDomain: cluster.local
|
||||
|
||||
# Look up namespaces, StorageClasses, IngressClasses and installed APIs before
|
||||
# installing, to fail early on a cluster that cannot run the release. Set false
|
||||
# when your account cannot read those cluster-scoped objects.
|
||||
clusterChecks: true
|
||||
|
||||
image:
|
||||
# The registry and owner the Fluxer images come from.
|
||||
registry: ghcr.io/fluxerapp
|
||||
# The tag for every Fluxer image. v1 moves with each release.
|
||||
tag: v1
|
||||
# Always pulls the moving tag whenever a pod starts, including a pod that
|
||||
# restarts by itself. Pin with digests for rollouts you control.
|
||||
pullPolicy: Always
|
||||
# Per-image tag overrides, keyed by image name, for example fluxer-api: 2026.1012.120000.
|
||||
tags: {}
|
||||
# Per-image digests, keyed by image name, for example fluxer-api: sha256:0123.
|
||||
# A digest pins the image whatever the tag.
|
||||
digests: {}
|
||||
# Image pull secrets for every pod, as a list of {name: <secret>}.
|
||||
pullSecrets: []
|
||||
# Any new value restarts every Fluxer pod, which pulls the moving tag again.
|
||||
# Add --set-string image.revision=$(date +%s) to a helm upgrade that passes
|
||||
# your values file to move to the latest v1.
|
||||
revision: ""
|
||||
|
||||
# The image of the init container that waits for the backing services.
|
||||
initImage:
|
||||
repository: busybox
|
||||
tag: "1.37"
|
||||
# A digest pins the image whatever the tag.
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
secrets:
|
||||
# The name of a Secret you manage yourself. The chart then renders no Secret
|
||||
# and never changes this one. It needs every key listed under values below.
|
||||
# After editing it, run kubectl rollout restart for the deployments and
|
||||
# statefulsets of the release.
|
||||
existingSecret: ""
|
||||
# Generate every empty key below on first install and keep it on upgrade.
|
||||
# Set false under Argo CD or helm template, which cannot read the existing
|
||||
# Secret, and give every key here or use existingSecret.
|
||||
generate: true
|
||||
# Keys of the fluxer-env Secret. Empty means generate once, then keep.
|
||||
# Generated values are never rotated by an upgrade. Back up the Secret.
|
||||
values:
|
||||
# Password of the bundled Postgres role. Changing it after the first install
|
||||
# does not change the stored role password.
|
||||
FLUXER_POSTGRES_PASSWORD: ""
|
||||
# Meilisearch master key.
|
||||
FLUXER_SEARCH_API_KEY: ""
|
||||
# S3 access key id. The bundled store installs it on every upgrade.
|
||||
FLUXER_S3_ACCESS_KEY_ID: fluxer
|
||||
# S3 secret key. Letters and digits only for the bundled store.
|
||||
FLUXER_S3_SECRET_ACCESS_KEY: ""
|
||||
# Signs sudo mode tokens.
|
||||
FLUXER_SUDO_MODE_SECRET: ""
|
||||
# Signs connection initiation tokens.
|
||||
FLUXER_CONNECTION_INITIATION_SECRET: ""
|
||||
# Derives hidden profile names. Changing it renames every hidden profile.
|
||||
FLUXER_PROFILE_PSEUDONYM_SECRET: ""
|
||||
# Authenticates RPC between the gateway, push and the api.
|
||||
FLUXER_GATEWAY_RPC_AUTH_TOKEN: ""
|
||||
# Password of the bundled Valkey. Letters and digits only.
|
||||
FLUXER_VALKEY_PASSWORD: ""
|
||||
# Token of the bundled NATS, letters and digits only. With an outside NATS,
|
||||
# its token if it has one.
|
||||
FLUXER_NATS_AUTH_TOKEN: ""
|
||||
# Erlang distribution cookie of the gateway.
|
||||
FLUXER_ERLANG_COOKIE: ""
|
||||
# Signs media proxy URLs.
|
||||
FLUXER_MEDIA_PROXY_SECRET_KEY: ""
|
||||
# Standard base64 of at least 32 random bytes. Signs upload relay tokens.
|
||||
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ""
|
||||
# Signs admin panel sessions.
|
||||
FLUXER_ADMIN_SECRET_KEY_BASE: ""
|
||||
# Client secret of the built-in admin OAuth application.
|
||||
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ""
|
||||
# Setup token the wizard asks for before the first admin account. With
|
||||
# existingSecret, or with generate false and no value here, the API makes
|
||||
# one on its first start and writes it to its log once.
|
||||
FLUXER_SETUP_TOKEN: ""
|
||||
# LiveKit API key.
|
||||
FLUXER_LIVEKIT_API_KEY: fluxer
|
||||
# LiveKit API secret, at least 32 characters.
|
||||
FLUXER_LIVEKIT_API_SECRET: ""
|
||||
# Web push key pair, base64url. Set both or neither.
|
||||
FLUXER_VAPID_PUBLIC_KEY: ""
|
||||
FLUXER_VAPID_PRIVATE_KEY: ""
|
||||
# Optional secret-bearing settings, added to the Secret as given. The keys are
|
||||
# the variable names on the configuration page of the operator docs.
|
||||
extra: {}
|
||||
# A full Postgres URL. Wins over the host, port, database and password.
|
||||
# FLUXER_POSTGRES_URL: postgres://fluxer:[email protected]:5432/fluxer
|
||||
# PEM text of the CA that signed the Postgres server certificate.
|
||||
# FLUXER_POSTGRES_SSL_CA: ""
|
||||
# The URL of an outside key-value store that holds a password.
|
||||
# FLUXER_KV_URL: redis://:[email protected]:6379/0
|
||||
# The password of an outside search server.
|
||||
# FLUXER_SEARCH_PASSWORD: ""
|
||||
# A temporary S3 session token.
|
||||
# FLUXER_S3_SESSION_TOKEN: ""
|
||||
# Comma-separated base64 secrets that sign attachment URLs.
|
||||
# FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ""
|
||||
# SMTP password for outgoing email.
|
||||
# FLUXER_EMAIL_SMTP_PASSWORD: ""
|
||||
# Secret that authenticates email provider webhooks.
|
||||
# FLUXER_EMAIL_WEBHOOK_SECRET: ""
|
||||
# Stripe secret key.
|
||||
# FLUXER_STRIPE_SECRET_KEY: ""
|
||||
# Stripe webhook signing secret.
|
||||
# FLUXER_STRIPE_WEBHOOK_SECRET: ""
|
||||
# Key of the donation proxy.
|
||||
# FLUXER_API_DONATION_PROXY_KEY: ""
|
||||
# Bearer token of the HTTP cache purge endpoint.
|
||||
# FLUXER_CACHE_PURGE_HTTP_TOKEN: ""
|
||||
# Klipy GIF API key.
|
||||
# FLUXER_KLIPY_API_KEY: ""
|
||||
# YouTube Data API key.
|
||||
# FLUXER_YOUTUBE_API_KEY: ""
|
||||
# Bluesky OAuth signing keys.
|
||||
# FLUXER_AUTH_BLUESKY_KEYS: ""
|
||||
# APNs private key for iOS push.
|
||||
# FLUXER_PUSH_APNS_PRIVATE_KEY: ""
|
||||
# FCM private key for Android push.
|
||||
# FLUXER_PUSH_FCM_PRIVATE_KEY: ""
|
||||
|
||||
# Extra non-secret settings for every Fluxer service, as NAME: value. The names
|
||||
# and meanings are on the configuration page of the operator docs. An empty
|
||||
# value is left out. The chart refuses a secret here. Secrets belong in
|
||||
# secrets.extra.
|
||||
env: {}
|
||||
|
||||
postgres:
|
||||
# Run Postgres in the release. Set false to use your own server.
|
||||
enabled: true
|
||||
image:
|
||||
repository: postgres
|
||||
# A new major needs a dump and restore. The pod refuses a data directory
|
||||
# from another major.
|
||||
tag: 16-alpine
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
# The user and group id of the postgres user in the image. 70 in the Alpine
|
||||
# images, 999 in the Debian ones.
|
||||
runAsUser: 70
|
||||
# Your own server, used when enabled is false. Its password goes in
|
||||
# secrets.values.FLUXER_POSTGRES_PASSWORD, or a URL in secrets.extra.FLUXER_POSTGRES_URL.
|
||||
# The database and the role have to exist already.
|
||||
external:
|
||||
host: ""
|
||||
port: 5432
|
||||
database: fluxer
|
||||
username: fluxer
|
||||
# true or false. Empty keeps the service default.
|
||||
ssl: ""
|
||||
# The database and role the bundled server creates.
|
||||
database: fluxer
|
||||
username: fluxer
|
||||
# Server settings, passed as -c name=value.
|
||||
parameters:
|
||||
max_connections: "150"
|
||||
shared_buffers: 512MB
|
||||
effective_cache_size: 2GB
|
||||
work_mem: 8MB
|
||||
maintenance_work_mem: 256MB
|
||||
autovacuum_work_mem: 128MB
|
||||
random_page_cost: "1.1"
|
||||
effective_io_concurrency: "200"
|
||||
default_statistics_target: "200"
|
||||
jit: "off"
|
||||
min_wal_size: 512MB
|
||||
max_wal_size: 2GB
|
||||
checkpoint_completion_target: "0.9"
|
||||
wal_buffers: 16MB
|
||||
wal_compression: zstd
|
||||
bgwriter_delay: 50ms
|
||||
bgwriter_lru_maxpages: "1000"
|
||||
autovacuum_vacuum_scale_factor: "0.05"
|
||||
autovacuum_analyze_scale_factor: "0.02"
|
||||
autovacuum_vacuum_cost_limit: "2000"
|
||||
track_io_timing: "on"
|
||||
shared_preload_libraries: pg_stat_statements
|
||||
# Size of the in-memory /dev/shm. Parallel vacuum needs it.
|
||||
shmSize: 1Gi
|
||||
# The size and the class are fixed once the volume exists. To grow a volume
|
||||
# later, edit the PersistentVolumeClaim, not this value.
|
||||
persistence:
|
||||
size: 20Gi
|
||||
# Empty uses the cluster default StorageClass.
|
||||
storageClassName: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
memory: 5Gi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
valkey:
|
||||
# Run Valkey in the release. Set false and give external.url to use your own.
|
||||
enabled: true
|
||||
image:
|
||||
repository: valkey/valkey
|
||||
tag: 9.1-alpine
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
external:
|
||||
# A URL without a password. One with a password goes in secrets.extra.FLUXER_KV_URL.
|
||||
url: ""
|
||||
maxmemory: 192mb
|
||||
maxmemoryPolicy: noeviction
|
||||
appendfsync: everysec
|
||||
persistence:
|
||||
size: 1Gi
|
||||
storageClassName: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
nats:
|
||||
# Run NATS with JetStream in the release. Set false and give external.url to use your own.
|
||||
enabled: true
|
||||
image:
|
||||
repository: nats
|
||||
tag: 2.14-alpine
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
external:
|
||||
# Its token, if any, goes in secrets.values.FLUXER_NATS_AUTH_TOKEN.
|
||||
url: ""
|
||||
# 1, or 3 or more for a JetStream cluster.
|
||||
replicas: 1
|
||||
persistence:
|
||||
size: 10Gi
|
||||
storageClassName: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
meilisearch:
|
||||
# Run Meilisearch in the release. Set false and give external.url to use your own.
|
||||
enabled: true
|
||||
image:
|
||||
repository: getmeili/meilisearch
|
||||
tag: v1.53
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
external:
|
||||
# Its key goes in secrets.values.FLUXER_SEARCH_API_KEY.
|
||||
url: ""
|
||||
maxIndexingMemory: 256mb
|
||||
maxIndexingThreads: 2
|
||||
persistence:
|
||||
size: 10Gi
|
||||
storageClassName: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 1536Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
seaweedfs:
|
||||
# Run SeaweedFS as the S3 store and create the buckets. Only its S3 port
|
||||
# listens outside the pod. Set false and give s3.endpoint to use your own
|
||||
# store, with the buckets already created.
|
||||
enabled: true
|
||||
image:
|
||||
repository: chrislusf/seaweedfs
|
||||
tag: "4.47"
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
# Volumes grown per bucket. 1 keeps the store usable on small disks.
|
||||
volumeGrowth: 1
|
||||
gomemlimit: 1536MiB
|
||||
# Tries, 2 seconds apart, the bucket setup makes before it starts over.
|
||||
initAttempts: 60
|
||||
persistence:
|
||||
size: 20Gi
|
||||
storageClassName: ""
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 2Gi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
s3:
|
||||
# The S3 endpoint the services use. Empty uses the bundled SeaweedFS.
|
||||
endpoint: ""
|
||||
# The S3 endpoint browsers use for direct uploads. Empty uses endpoint.
|
||||
publicEndpoint: ""
|
||||
region: us-east-1
|
||||
forcePathStyle: true
|
||||
buckets:
|
||||
cdn: fluxer
|
||||
uploads: fluxer-uploads
|
||||
reports: fluxer-reports
|
||||
harvests: fluxer-harvests
|
||||
|
||||
livekit:
|
||||
# Run LiveKit for voice and video. false with no external.url turns voice off.
|
||||
enabled: true
|
||||
# A LiveKit server of your own, used when enabled is false. Its key and
|
||||
# secret go in secrets.values.FLUXER_LIVEKIT_API_KEY and FLUXER_LIVEKIT_API_SECRET.
|
||||
# Point its webhook at <origin>/api/webhooks/livekit, signed with the same key.
|
||||
external:
|
||||
# The address the api uses to reach it, for example http://livekit.voice.svc:7880.
|
||||
url: ""
|
||||
# The address browsers use to reach it, for example wss://livekit.example.com.
|
||||
publicUrl: ""
|
||||
image:
|
||||
repository: livekit/livekit-server
|
||||
tag: v1.12.0
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
# How clients reach the media ports. hostNetwork binds them on the node and
|
||||
# needs a privileged namespace. loadBalancer and nodePort use the Service
|
||||
# livekit-rtc, which serves TCP and UDP on one address.
|
||||
expose: hostNetwork
|
||||
# The public IP LiveKit advertises. Required for loadBalancer, where it is the
|
||||
# address of the load balancer. Empty finds it over STUN.
|
||||
nodeIP: ""
|
||||
# The address the loadBalancer Service asks for. Reserve it with your
|
||||
# provider first. Some providers take it from an annotation instead.
|
||||
loadBalancerIP: ""
|
||||
# Find the public IP over STUN.
|
||||
useExternalIP: true
|
||||
tcpPort: 7881
|
||||
udpPort: 7882
|
||||
# The node ports in nodePort mode. LiveKit advertises these too.
|
||||
nodePorts:
|
||||
tcp: 30881
|
||||
udp: 30882
|
||||
stunServers:
|
||||
- stun.l.google.com:19302
|
||||
- stun1.l.google.com:19302
|
||||
logLevel: info
|
||||
# More LiveKit configuration, merged over the chart's, for example
|
||||
# {rtc: {ips: {includes: [10.0.0.0/8]}}} to advertise only the node network.
|
||||
extraConfig: {}
|
||||
# The voice region seeded on every api start, as JSON.
|
||||
defaultRegion: '{"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0}'
|
||||
# Node addresses allowed to reach the edge in hostNetwork mode, for the
|
||||
# LiveKit webhook. Needed only with networkPolicy.ingressNamespaceSelector.
|
||||
# Keep it as narrow as the LiveKit node, for example 10.0.0.12/32 together
|
||||
# with nodeSelector.
|
||||
nodeCIDRs: []
|
||||
# Annotations for the Service livekit-rtc.
|
||||
serviceAnnotations: {}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
# Every Fluxer workload below takes the same keys:
|
||||
# replicas, image (registry, tag, digest, pullPolicy), resources, env (NAME: value),
|
||||
# extraEnv (a list of Kubernetes env entries), probes (startup, liveness or
|
||||
# readiness, each replacing the default probe whole), podAnnotations,
|
||||
# nodeSelector, tolerations, affinity, topologySpreadConstraints, pdb
|
||||
# (minAvailable or maxUnavailable), hpa (minReplicas, maxReplicas,
|
||||
# targetCPUUtilizationPercentage), extraVolumes and extraVolumeMounts.
|
||||
|
||||
api:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 768Mi
|
||||
limits:
|
||||
memory: 2560Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
worker:
|
||||
# Must stay 1. The worker runs the cron scheduler.
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 768Mi
|
||||
limits:
|
||||
memory: 2560Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
gateway:
|
||||
replicas: 1
|
||||
terminationGracePeriodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
mediaProxy:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
push:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
admin:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
appProxy:
|
||||
replicas: 1
|
||||
# Settings for app-proxy only: branding, CSP extras, the index upstream,
|
||||
# same-origin hosts and manifest scope, as NAME: value. For example
|
||||
# FLUXER_APP_PRODUCT_NAME, FLUXER_CSP_EXTRA_CONNECT_SRC or RUST_LOG.
|
||||
config: {}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
staticProxy:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
env: {}
|
||||
extraEnv: []
|
||||
probes: {}
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
pdb: {}
|
||||
hpa: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
# The internal services. Each runs a router Deployment and a shard StatefulSet.
|
||||
# router and shard take the workload keys above. shard.replicas sets the shard count.
|
||||
svc:
|
||||
gifs:
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {memory: 128Mi}
|
||||
shard:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
messages:
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {memory: 128Mi}
|
||||
shard:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
snowflakes:
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {memory: 128Mi}
|
||||
shard:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
unfurl:
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {memory: 128Mi}
|
||||
shard:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
users:
|
||||
router:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {memory: 128Mi}
|
||||
shard:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
|
||||
edge:
|
||||
image:
|
||||
repository: caddy
|
||||
tag: 2.11-alpine
|
||||
digest: ""
|
||||
pullPolicy: IfNotPresent
|
||||
# How traffic reaches the edge. ingress puts an Ingress in front of it,
|
||||
# gateway an HTTPRoute, loadBalancer a LoadBalancer Service with its own TLS,
|
||||
# and none leaves only the ClusterIP Service edge on port 8080.
|
||||
expose: ingress
|
||||
ingress:
|
||||
# The IngressClass. Empty uses the cluster default.
|
||||
className: ""
|
||||
# Extra annotations. The chart always adds the ingress-nginx body size and
|
||||
# timeout annotations, which other controllers ignore.
|
||||
annotations: {}
|
||||
tls:
|
||||
# certManager issues a certificate through a namespaced ACME Issuer.
|
||||
# existingSecret uses secretName. none serves plain HTTP behind the controller.
|
||||
mode: certManager
|
||||
secretName: fluxer-tls
|
||||
acmeServer: https://acme-v02.api.letsencrypt.org/directory
|
||||
gateway:
|
||||
# The Gateway the HTTPRoute attaches to, as Gateway API parentRefs.
|
||||
parentRefs: []
|
||||
# HTTPRoute timeouts, for example {request: 0s}. Envoy-based gateways cut
|
||||
# requests at 15 seconds without it.
|
||||
timeouts: {}
|
||||
loadBalancer:
|
||||
# Also publish UDP 443 for HTTP/3. Some clouds cannot mix TCP and UDP on one Service.
|
||||
http3: false
|
||||
annotations: {}
|
||||
# Size of the volume that keeps the certificates.
|
||||
persistence:
|
||||
size: 1Gi
|
||||
storageClassName: ""
|
||||
replicas: 1
|
||||
# Edge settings as NAME: value, overriding the chart's, for example
|
||||
# FLUXER_EDGE_GLOBAL_OPTIONS or FLUXER_EDGE_ENCODE.
|
||||
env: {}
|
||||
extraEnv: []
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
podAnnotations: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
networkPolicy:
|
||||
# Admit traffic to the release pods only from the release itself, to the edge
|
||||
# from anywhere, and to the LiveKit media ports. Network plugins without
|
||||
# NetworkPolicy support ignore it. Add a NetworkPolicy of your own to let
|
||||
# another namespace in, for example a metrics scraper.
|
||||
enabled: true
|
||||
# Namespaces allowed to reach the edge on port 8080, as a namespaceSelector,
|
||||
# for example {matchLabels: {kubernetes.io/metadata.name: ingress-nginx}}.
|
||||
# Empty allows any source.
|
||||
ingressNamespaceSelector: {}
|
||||
@@ -236,8 +236,8 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
|
||||
# Optional media policies, both off by default. See the operator docs.
|
||||
#
|
||||
# CORS limits which web origins may read media. A request with no Origin is
|
||||
# always served. Add https://web.fluxer.app if people use the hosted client.
|
||||
# CORS limits which web origins may read media. A request with no Origin, or
|
||||
# from the desktop app, is always served.
|
||||
#
|
||||
# Signatures make an attachment read need a signed URL, so a copied link stops
|
||||
# working. Needs a secret from openssl rand -base64 32, first entry signs and
|
||||
@@ -247,7 +247,7 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
# media-proxy reads these at start, so apply with docker compose up -d
|
||||
# media-proxy. The values below are examples.
|
||||
#FLUXER_MEDIA_PROXY_CORS_MODE=enforce
|
||||
#FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS=https://chat.example.com,https://web.fluxer.app
|
||||
#FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS=https://chat.example.com
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=
|
||||
#FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE=enforce
|
||||
|
||||
@@ -344,6 +344,8 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_APP_STATUS_PAGE_URL=
|
||||
#FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL=
|
||||
#FLUXER_INSTANCE_SETUP_CONFIGURED=false
|
||||
# Setup token the wizard asks for before the first admin account. 32 to 256 characters. Unset means the API generates one on the first start and writes it to its log once.
|
||||
#FLUXER_SETUP_TOKEN=
|
||||
# How members sign in on a new instance, username or email. Unset means username. Read only on the first start.
|
||||
#FLUXER_ACCOUNT_IDENTITY=
|
||||
# Username tags on a new email instance. none gives unique names with no tag, random gives name#4821. Unset means none. A username instance always uses none. Read only on the first start.
|
||||
@@ -404,6 +406,10 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
# The compression the edge offers, as Caddy encode arguments.
|
||||
#FLUXER_EDGE_ENCODE=zstd gzip
|
||||
|
||||
# Extra Caddy global options, one line, added to the global block of the edge
|
||||
# Caddyfile. Empty adds nothing. Example: email [email protected]
|
||||
#FLUXER_EDGE_GLOBAL_OPTIONS=
|
||||
|
||||
# Images of the bundled services, for a mirror or another tag. A new Postgres
|
||||
# major needs a dump and restore, as the upgrade guide describes.
|
||||
#FLUXER_CADDY_IMAGE=caddy:2.11-alpine
|
||||
@@ -532,7 +538,7 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
|
||||
# The bundled Valkey holds durable state as well as cache, so it runs with an
|
||||
# append-only file and with noeviction, which fails an over-limit write instead
|
||||
# of dropping queued work. Change the policy only if that state lives elsewhere.
|
||||
# of dropping queued work. Change the policy only if that state is kept elsewhere.
|
||||
#FLUXER_VALKEY_MAXMEMORY=192mb
|
||||
#FLUXER_VALKEY_MAXMEMORY_POLICY=noeviction
|
||||
#FLUXER_VALKEY_APPENDFSYNC=everysec
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
trusted_proxies static {$FLUXER_EDGE_TRUSTED_PROXIES:private_ranges}
|
||||
trusted_proxies_strict
|
||||
}
|
||||
{$FLUXER_EDGE_GLOBAL_OPTIONS}
|
||||
}
|
||||
|
||||
{$FLUXER_EDGE_SITE_ADDRESS} {
|
||||
|
||||
@@ -231,6 +231,7 @@ services:
|
||||
FLUXER_EDGE_SITE_ADDRESS: ${FLUXER_EDGE_SITE_ADDRESS:-${FLUXER_CADDY_SITE_ADDRESS:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}}}
|
||||
FLUXER_EDGE_TRUSTED_PROXIES: ${FLUXER_EDGE_TRUSTED_PROXIES:-private_ranges}
|
||||
FLUXER_EDGE_ENCODE: ${FLUXER_EDGE_ENCODE:-zstd gzip}
|
||||
FLUXER_EDGE_GLOBAL_OPTIONS: ${FLUXER_EDGE_GLOBAL_OPTIONS:-}
|
||||
FLUXER_ADMIN_BASE_PATH: ${FLUXER_ADMIN_BASE_PATH:-/admin}
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
@@ -465,6 +466,7 @@ services:
|
||||
environment:
|
||||
<<: *fluxer-env
|
||||
FLUXER_API_PORT: "8080"
|
||||
FLUXER_SETUP_TOKEN: ${FLUXER_SETUP_TOKEN:-}
|
||||
NODE_OPTIONS: --enable-source-maps${FLUXER_API_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_API_NODE_HEAP_MB}${FLUXER_API_NODE_OPTIONS:+ $FLUXER_API_NODE_OPTIONS}
|
||||
NODE_EXTRA_CA_CERTS: ${FLUXER_NODE_EXTRA_CA_CERTS:-/etc/ssl/certs/ca-certificates.crt}
|
||||
FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_API_POSTGRES_MAX_CONNECTIONS:-25}"
|
||||
|
||||
@@ -259,3 +259,42 @@ describe('buildAPIConfigFromMaster trusted callers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function setupTokenFromEnv(env: Record<string, string>): Promise<string | null> {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
vi.stubEnv(key, value);
|
||||
}
|
||||
resetConfig();
|
||||
return buildAPIConfigFromMaster(await loadConfig()).instance.setup.token;
|
||||
}
|
||||
|
||||
describe('buildAPIConfigFromMaster setup token', () => {
|
||||
const setupToken = 's'.repeat(32);
|
||||
|
||||
test('reads the token from FLUXER_SETUP_TOKEN', async () => {
|
||||
expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: ` ${setupToken}\n`})).toBe(setupToken);
|
||||
});
|
||||
|
||||
test.each(['', ' '])('leaves the token unset for %j', async (blank) => {
|
||||
expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: blank})).toBeNull();
|
||||
});
|
||||
|
||||
test('leaves the token unset when the variable is missing', async () => {
|
||||
expect(await setupTokenFromEnv({})).toBeNull();
|
||||
});
|
||||
|
||||
test('accepts a token of 256 characters', async () => {
|
||||
expect(await setupTokenFromEnv({FLUXER_SETUP_TOKEN: 's'.repeat(256)})).toBe('s'.repeat(256));
|
||||
});
|
||||
|
||||
test.each([
|
||||
['shorter than 32 characters', 's'.repeat(31)],
|
||||
['longer than 256 characters', 's'.repeat(257)],
|
||||
['holding a form feed', `${'s'.repeat(16)}\f${'s'.repeat(16)}`],
|
||||
['holding a text direction override', `${'s'.repeat(16)}\u202E${'s'.repeat(16)}`],
|
||||
])('fails at boot on a token %s', async (_case, token) => {
|
||||
await expect(setupTokenFromEnv({FLUXER_SETUP_TOKEN: token})).rejects.toThrow(
|
||||
'FLUXER_SETUP_TOKEN must be 32 to 256 printable characters',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
|
||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||
import {smtpTlsModeFromSecure} from '@fluxer/constants/src/SmtpConstants';
|
||||
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import {normalizeString} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup';
|
||||
|
||||
function extractHostname(url: string): string {
|
||||
@@ -123,6 +124,24 @@ function buildTrustedCallers(master: MasterConfig): Array<TrustedCallerConfig> {
|
||||
return trustedCallers;
|
||||
}
|
||||
|
||||
const SETUP_TOKEN_MIN_LENGTH = 32;
|
||||
const SETUP_TOKEN_MAX_LENGTH = 256;
|
||||
|
||||
function resolveSetupToken(master: MasterConfig): string | null {
|
||||
const token = master.instance.setup.token?.trim();
|
||||
if (!token) return null;
|
||||
if (
|
||||
token.length < SETUP_TOKEN_MIN_LENGTH ||
|
||||
token.length > SETUP_TOKEN_MAX_LENGTH ||
|
||||
normalizeString(token) !== token
|
||||
) {
|
||||
throw new Error(
|
||||
`FLUXER_SETUP_TOKEN must be ${SETUP_TOKEN_MIN_LENGTH} to ${SETUP_TOKEN_MAX_LENGTH} printable characters`,
|
||||
);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
if (!master.internal) {
|
||||
throw new Error('internal configuration is required for the API');
|
||||
@@ -402,6 +421,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
},
|
||||
setup: {
|
||||
configured: master.instance.setup.configured,
|
||||
token: resolveSetupToken(master),
|
||||
},
|
||||
accountIdentity: master.instance.account_identity,
|
||||
tagStyle: master.instance.tag_style,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
REGISTRATION_PENDING_APPROVAL_TRAIT,
|
||||
REGISTRATION_REJECTED_TRAIT,
|
||||
} from '@app/api/instance/InstanceConfigRepository';
|
||||
import {clearUsedSetupToken} from '@app/api/instance/SetupToken';
|
||||
import {deriveSsoRedirectUri, normalizeAndValidateSsoConfig} from '@app/api/instance/SsoConfigValidation';
|
||||
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
@@ -44,7 +45,6 @@ import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experim
|
||||
import type {InstanceBranding} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
|
||||
import type {Context} from 'hono';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
|
||||
const INSTANCE_BRANDING_ENTITY_ID = 0n;
|
||||
const ATTACHMENT_EXPIRY_RULE_FIELDS = [
|
||||
@@ -187,49 +187,6 @@ function buildAdminIssuedRegistrationUrl(code: string): string {
|
||||
return `${baseUrl}/register?registration_url=${encodeURIComponent(code)}`;
|
||||
}
|
||||
|
||||
function requireSetupSessionOrAdminACL(requiredACL: string) {
|
||||
const requireAcl = requireAdminACL(requiredACL);
|
||||
return createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||
const user = ctx.get('user');
|
||||
const tokenType = ctx.get('authTokenType');
|
||||
if (user && tokenType === 'session') {
|
||||
const appPublic = await getInstanceConfigRepository().getAppPublicConfig();
|
||||
if (!appPublic.setup.configured) {
|
||||
ctx.set('adminUserId', user.id);
|
||||
ctx.set('adminUserAcls', user.acls);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return requireAcl(ctx, next);
|
||||
});
|
||||
}
|
||||
|
||||
function hasAdminAuthenticationACL(acls: ReadonlySet<string>): boolean {
|
||||
return acls.has(AdminACLs.AUTHENTICATE) || acls.has(AdminACLs.WILDCARD);
|
||||
}
|
||||
|
||||
function completesInitialSetup(data: InstanceConfigUpdateRequest, setupConfigured: boolean): boolean {
|
||||
return (
|
||||
!setupConfigured &&
|
||||
data.app_public?.setup != null &&
|
||||
readOptionalField(data.app_public.setup, 'configured') === true
|
||||
);
|
||||
}
|
||||
|
||||
async function grantSetupCompleterAdminACL(ctx: Context<HonoEnv>): Promise<boolean> {
|
||||
const user = ctx.get('user');
|
||||
if (!user || ctx.get('authTokenType') !== 'session' || hasAdminAuthenticationACL(user.acls)) {
|
||||
return false;
|
||||
}
|
||||
const nextACLs = new Set(user.acls);
|
||||
nextACLs.add(AdminACLs.WILDCARD);
|
||||
const updatedUser = await ctx.get('userRepository').patchUpsert(user.id, {acls: nextACLs}, user.toRow());
|
||||
ctx.set('user', updatedUser);
|
||||
ctx.set('adminUserAcls', updatedUser.acls);
|
||||
return true;
|
||||
}
|
||||
|
||||
function relayConsentStamp(
|
||||
current: PushRelayConfig,
|
||||
patch: PushRelayConfigUpdateRequest,
|
||||
@@ -346,7 +303,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
app.get(
|
||||
'/admin/instance/config',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
||||
requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_VIEW),
|
||||
requireAdminACL(AdminACLs.INSTANCE_CONFIG_VIEW),
|
||||
OpenAPI({
|
||||
operationId: 'get_admin_instance_config',
|
||||
summary: 'Get instance configuration',
|
||||
@@ -374,7 +331,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
app.patch(
|
||||
'/admin/instance/config',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
Validator('json', InstanceConfigUpdateRequest),
|
||||
OpenAPI({
|
||||
operationId: 'update_admin_instance_config',
|
||||
@@ -390,11 +347,6 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
const data = ctx.req.valid('json');
|
||||
assertSelfHostedBillingSections(data);
|
||||
await assertBillingCompatibleWithPremiumMode(data);
|
||||
const appPublicBeforeUpdate = completesInitialSetup(data, false)
|
||||
? await instanceConfigRepository.getAppPublicConfig()
|
||||
: null;
|
||||
const shouldGrantSetupCompleterAdmin =
|
||||
appPublicBeforeUpdate !== null && completesInitialSetup(data, appPublicBeforeUpdate.setup.configured);
|
||||
if (data.gateway_rollout) {
|
||||
const patch = data.gateway_rollout;
|
||||
const landed = await instanceConfigRepository.updateGatewayRolloutConfig((current) =>
|
||||
@@ -628,16 +580,14 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
);
|
||||
}
|
||||
if (data.app_public?.setup) {
|
||||
const configured = readOptionalField(data.app_public.setup, 'configured');
|
||||
await instanceConfigRepository.setAppPublicConfig({
|
||||
setup: omitUndefinedFields({
|
||||
configured: readOptionalField(data.app_public.setup, 'configured'),
|
||||
}),
|
||||
setup: omitUndefinedFields({configured}),
|
||||
});
|
||||
}
|
||||
let grantedSetupCompleterAdmin = false;
|
||||
if (shouldGrantSetupCompleterAdmin) {
|
||||
grantedSetupCompleterAdmin = await grantSetupCompleterAdminACL(ctx);
|
||||
await instanceConfigRepository.markAdminBootstrapped();
|
||||
if (configured === true) {
|
||||
await instanceConfigRepository.claimAdminBootstrap();
|
||||
await clearUsedSetupToken(instanceConfigRepository);
|
||||
}
|
||||
}
|
||||
await recordAdminWrite(ctx, {
|
||||
targetType: 'instance_config',
|
||||
@@ -645,7 +595,6 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
action: 'update_instance_config',
|
||||
metadata: {
|
||||
sections: listSuppliedSections(data),
|
||||
granted_acls: grantedSetupCompleterAdmin ? AdminACLs.WILDCARD : undefined,
|
||||
},
|
||||
});
|
||||
return ctx.json(await buildInstanceConfigResponse());
|
||||
@@ -654,7 +603,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
app.post(
|
||||
'/admin/instance/config/branding-assets',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
Validator('json', BrandingAssetUploadRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_admin_instance_branding_asset',
|
||||
@@ -690,7 +639,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
app.post(
|
||||
'/admin/instance/config/smtp-tests',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireSetupSessionOrAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
requireAdminACL(AdminACLs.INSTANCE_CONFIG_UPDATE),
|
||||
Validator('json', InstanceEmailSmtpTestRequest),
|
||||
OpenAPI({
|
||||
operationId: 'create_admin_instance_smtp_test',
|
||||
|
||||
@@ -27,7 +27,6 @@ export const InstanceConfigAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/admin/instance/config',
|
||||
name: 'admin acl',
|
||||
async prepare({harness, admin}) {
|
||||
await createRegistrationUrl(harness, admin);
|
||||
return {
|
||||
@@ -41,23 +40,6 @@ export const InstanceConfigAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/admin/instance/config',
|
||||
name: 'setup session',
|
||||
async prepare() {
|
||||
await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: false}});
|
||||
return {
|
||||
request: {path: '/admin/instance/config'},
|
||||
expected: {
|
||||
action: 'get_instance_config',
|
||||
targetType: 'instance_config',
|
||||
targetId: '0',
|
||||
metadata: {registration_url_count: '0', pending_registration_count: '0'},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
route: '/admin/instance/config',
|
||||
|
||||
@@ -12,8 +12,10 @@ import {createPasswordResetToken} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {UserRow} from '@app/api/database/types/UserTypes';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {getKVClient} from '@app/api/middleware/ServiceRegistry';
|
||||
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import {PASSWORD_RESET_TOKEN_TTL_SECONDS} from '@app/api/user/repositories/auth/TokenRepository';
|
||||
import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits';
|
||||
import * as FetchUtils from '@app/api/utils/FetchUtils';
|
||||
import {hashPassword} from '@app/api/utils/PasswordUtils';
|
||||
@@ -31,6 +33,7 @@ import {ms} from 'itty-time';
|
||||
|
||||
const PWNED_PASSWORDS_TIMEOUT_MS = ms('5 seconds');
|
||||
const PWNED_PASSWORD_CACHE_MAX_PREFIXES = 128;
|
||||
const PASSWORD_RESET_CLAIM_KEY_PREFIX = 'auth:password-reset:claimed:';
|
||||
|
||||
interface CacheEntry {
|
||||
pwnedSuffixes: ReadonlySet<string>;
|
||||
@@ -94,12 +97,14 @@ interface ApplyPasswordResetParams {
|
||||
password: string;
|
||||
request: Request;
|
||||
afterPasswordSet?: () => Promise<void>;
|
||||
onPasswordWritten?: () => void;
|
||||
}
|
||||
|
||||
interface CommitPasswordResetParams {
|
||||
user: User;
|
||||
passwordHash: string;
|
||||
webauthnIsSecondFactor: boolean;
|
||||
onPasswordWritten?: () => void;
|
||||
}
|
||||
|
||||
type ResetPasswordResult =
|
||||
@@ -242,6 +247,22 @@ export async function validateResetToken(ctx: ApiContext, token: string): Promis
|
||||
return true;
|
||||
}
|
||||
|
||||
function passwordResetClaimKey(token: string): string {
|
||||
return `${PASSWORD_RESET_CLAIM_KEY_PREFIX}${crypto.createHash('sha256').update(token).digest('hex')}`;
|
||||
}
|
||||
|
||||
function claimPasswordResetToken(token: string): Promise<boolean> {
|
||||
return getKVClient().setnx(passwordResetClaimKey(token), '1', PASSWORD_RESET_TOKEN_TTL_SECONDS);
|
||||
}
|
||||
|
||||
async function releasePasswordResetTokenClaim(token: string): Promise<void> {
|
||||
try {
|
||||
await getKVClient().del(passwordResetClaimKey(token));
|
||||
} catch (error) {
|
||||
Logger.error({error}, 'Could not release the claim of a password reset token whose reset failed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetPassword(
|
||||
ctx: ApiContext,
|
||||
{data, request}: ResetPasswordParams,
|
||||
@@ -263,28 +284,42 @@ export async function resetPassword(
|
||||
if (await isPasswordPwned(ctx, data.password)) {
|
||||
throw InputValidationError.fromCode('password', ValidationErrorCodes.PASSWORD_IS_TOO_COMMON);
|
||||
}
|
||||
if (tokenData.email !== null) {
|
||||
if (!(await claimPasswordResetToken(data.token))) {
|
||||
throw InputValidationError.fromCode('token', ValidationErrorCodes.INVALID_OR_EXPIRED_RESET_TOKEN);
|
||||
}
|
||||
let passwordWritten = false;
|
||||
const onPasswordWritten = () => {
|
||||
passwordWritten = true;
|
||||
};
|
||||
try {
|
||||
if (tokenData.email !== null) {
|
||||
return await applyPasswordReset(ctx, {
|
||||
user,
|
||||
password: data.password,
|
||||
request,
|
||||
afterPasswordSet: () => users.deleteAllPasswordResetTokens(user.id),
|
||||
onPasswordWritten,
|
||||
});
|
||||
}
|
||||
return await applyPasswordReset(ctx, {
|
||||
user,
|
||||
user: currentUser,
|
||||
password: data.password,
|
||||
request,
|
||||
afterPasswordSet: () => users.deleteAllPasswordResetTokens(user.id),
|
||||
afterPasswordSet: async () => {
|
||||
await users.deleteAllPasswordResetTokens(currentUser.id);
|
||||
await new RecoveryKitRepository().delete(currentUser.id);
|
||||
},
|
||||
onPasswordWritten,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!passwordWritten) await releasePasswordResetTokenClaim(data.token);
|
||||
throw error;
|
||||
}
|
||||
return await applyPasswordReset(ctx, {
|
||||
user: currentUser,
|
||||
password: data.password,
|
||||
request,
|
||||
afterPasswordSet: async () => {
|
||||
await users.deleteAllPasswordResetTokens(currentUser.id);
|
||||
await new RecoveryKitRepository().delete(currentUser.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function commitPasswordReset(
|
||||
ctx: ApiContext,
|
||||
{user, passwordHash, webauthnIsSecondFactor}: CommitPasswordResetParams,
|
||||
{user, passwordHash, webauthnIsSecondFactor, onPasswordWritten}: CommitPasswordResetParams,
|
||||
): Promise<User> {
|
||||
const {users} = ctx.services;
|
||||
const updates: Partial<UserRow> = {
|
||||
@@ -297,6 +332,7 @@ export async function commitPasswordReset(
|
||||
updates.authenticator_types = authenticatorTypes;
|
||||
}
|
||||
const updatedUser = await users.patchUpsert(user.id, updates, user.toRow());
|
||||
onPasswordWritten?.();
|
||||
if (updates.authenticator_types) {
|
||||
await ctx.services.botMfaMirror.syncAuthenticatorTypesForOwner(updatedUser);
|
||||
}
|
||||
@@ -306,12 +342,12 @@ export async function commitPasswordReset(
|
||||
|
||||
export async function applyPasswordReset(
|
||||
ctx: ApiContext,
|
||||
{user, password, request, afterPasswordSet}: ApplyPasswordResetParams,
|
||||
{user, password, request, afterPasswordSet, onPasswordWritten}: ApplyPasswordResetParams,
|
||||
): Promise<ResetPasswordResult> {
|
||||
const webauthnIsSecondFactor = await resolveWebAuthnSecondFactor(ctx, user);
|
||||
const hasMfa = user.authenticatorTypes.has(UserAuthenticatorTypes.TOTP) || webauthnIsSecondFactor;
|
||||
const passwordHash = await hashPassword(password);
|
||||
const updatedUser = await commitPasswordReset(ctx, {user, passwordHash, webauthnIsSecondFactor});
|
||||
const updatedUser = await commitPasswordReset(ctx, {user, passwordHash, webauthnIsSecondFactor, onPasswordWritten});
|
||||
await afterPasswordSet?.();
|
||||
if (hasMfa) {
|
||||
return await createMfaTicketResponse(ctx, updatedUser, webauthnIsSecondFactor);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
REGISTRATION_PENDING_APPROVAL_TRAIT,
|
||||
type RegistrationUrlClaim,
|
||||
} from '@app/api/instance/InstanceConfigRepository';
|
||||
import {assertSetupToken, clearUsedSetupToken, isSetupTokenEnforced} from '@app/api/instance/SetupToken';
|
||||
import type {SingleCommunityService} from '@app/api/instance/SingleCommunityService';
|
||||
import type {InviteService} from '@app/api/invite/InviteService';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
@@ -85,6 +86,11 @@ interface RegisterParams {
|
||||
requestCache: RequestCache;
|
||||
}
|
||||
|
||||
interface RegisterAccountParams extends RegisterParams {
|
||||
clientIp: string;
|
||||
setupTokenVerified: boolean;
|
||||
}
|
||||
|
||||
export interface RegistrationDependencies {
|
||||
inviteService: InviteService;
|
||||
instanceConfigRepository: InstanceConfigRepository;
|
||||
@@ -114,16 +120,26 @@ export async function register(
|
||||
deps: RegistrationDependencies,
|
||||
params: RegisterParams,
|
||||
): Promise<RegisterResult> {
|
||||
if (!ctx.services.config.instance.selfHosted || (await deps.instanceConfigRepository.isAccountIdentityLocked())) {
|
||||
return await registerAccount(ctx, deps, params);
|
||||
const {config, rateLimit, cache} = ctx.services;
|
||||
const clientIp = requireConfiguredClientIp(params.request, config.proxy);
|
||||
const setupTokenVerified = await assertSetupToken({
|
||||
rateLimit,
|
||||
config,
|
||||
repository: deps.instanceConfigRepository,
|
||||
presented: params.data.setup_token,
|
||||
clientIp,
|
||||
});
|
||||
const account = {...params, clientIp, setupTokenVerified};
|
||||
if (!config.instance.selfHosted || (await deps.instanceConfigRepository.isAccountIdentityLocked())) {
|
||||
return await registerAccount(ctx, deps, account);
|
||||
}
|
||||
return await withAccountIdentitySetupLock(ctx.services.cache, () => registerAccount(ctx, deps, params));
|
||||
return await withAccountIdentitySetupLock(cache, () => registerAccount(ctx, deps, account));
|
||||
}
|
||||
|
||||
async function registerAccount(
|
||||
ctx: ApiContext,
|
||||
deps: RegistrationDependencies,
|
||||
{data, request, requestCache}: RegisterParams,
|
||||
{data, request, requestCache, clientIp, setupTokenVerified}: RegisterAccountParams,
|
||||
): Promise<RegisterResult> {
|
||||
const {users, snowflake, emailDnsValidation, config} = ctx.services;
|
||||
const {inviteService, instanceConfigRepository, singleCommunityService, discriminatorService, kvActivityTracker} =
|
||||
@@ -144,8 +160,9 @@ async function registerAccount(
|
||||
throw InputValidationError.fromCode('consent', ValidationErrorCodes.MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY);
|
||||
}
|
||||
const now = new Date();
|
||||
const registrationAccess = await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code);
|
||||
const clientIp = requireConfiguredClientIp(request, config.proxy);
|
||||
const registrationAccess = setupTokenVerified
|
||||
? {pendingApproval: false, registrationUrl: null}
|
||||
: await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code);
|
||||
const geoipResult = await lookupGeoip(clientIp);
|
||||
const countryCode = geoipResult.countryCode;
|
||||
const collectDateOfBirth = appPublicConfig.registration.collect_date_of_birth;
|
||||
@@ -168,7 +185,7 @@ async function registerAccount(
|
||||
}
|
||||
const rawEmail = usernameMode ? null : (data.email ?? null);
|
||||
const emailKey = rawEmail ? rawEmail.toLowerCase() : null;
|
||||
const enforceRateLimits = !config.dev.relaxRegistrationRateLimits;
|
||||
const enforceRateLimits = !config.dev.relaxRegistrationRateLimits && !setupTokenVerified;
|
||||
await enforceRegistrationRateLimits(ctx, {enforceRateLimits, clientIp, emailKey});
|
||||
if (rawEmail) {
|
||||
if (isBlockedEmailDomain(extractEmailDomain(rawEmail))) {
|
||||
@@ -217,13 +234,14 @@ async function registerAccount(
|
||||
discriminator = await allocateDiscriminator(discriminatorService, usernameCandidate);
|
||||
}
|
||||
const username = usernameCandidate!;
|
||||
const grantBootstrapAdmin =
|
||||
const bootstrapAdminEligible =
|
||||
shouldAttemptBootstrapAdminGrant(config, {
|
||||
rawEmail,
|
||||
hasPassword: Boolean(data.password),
|
||||
usernameMode,
|
||||
pendingApproval: registrationAccess.pendingApproval,
|
||||
setupConfigured: appPublicConfig.setup.configured,
|
||||
setupTokenVerified,
|
||||
}) && !(await instanceConfigRepository.isAdminBootstrapped());
|
||||
if (
|
||||
profileSubstringBlocklistCache.containsBannedSubstring('username', username) ||
|
||||
@@ -258,7 +276,7 @@ async function registerAccount(
|
||||
privacy_agreed_at: requiresPrivacyConsent ? now : null,
|
||||
last_active_at: now,
|
||||
last_active_ip: clientIp,
|
||||
acls: grantBootstrapAdmin ? new Set([AdminACLs.WILDCARD]) : null,
|
||||
acls: null,
|
||||
traits: registrationAccess.pendingApproval ? new Set([REGISTRATION_PENDING_APPROVAL_TRAIT]) : null,
|
||||
version: 1,
|
||||
};
|
||||
@@ -269,6 +287,7 @@ async function registerAccount(
|
||||
);
|
||||
let user: User;
|
||||
let createAttempted = false;
|
||||
let grantBootstrapAdmin = false;
|
||||
let usernameReservation: UsernameReservation | null = null;
|
||||
try {
|
||||
if (uniqueUsernames) {
|
||||
@@ -286,8 +305,9 @@ async function registerAccount(
|
||||
client_ip: clientIp,
|
||||
});
|
||||
}
|
||||
grantBootstrapAdmin = bootstrapAdminEligible && (await instanceConfigRepository.claimAdminBootstrap(userId));
|
||||
createAttempted = true;
|
||||
user = await users.create(userRow);
|
||||
user = await users.create(grantBootstrapAdmin ? {...userRow, acls: new Set([AdminACLs.WILDCARD])} : userRow);
|
||||
} catch (error) {
|
||||
if (!createAttempted) {
|
||||
await withdrawSignupOfUncreatedAccount(instanceConfigRepository, {
|
||||
@@ -295,11 +315,16 @@ async function registerAccount(
|
||||
registrationUrlUse,
|
||||
pendingApproval: registrationAccess.pendingApproval,
|
||||
});
|
||||
} else if (grantBootstrapAdmin) {
|
||||
await releaseBootstrapAdminOfUncreatedAccount(instanceConfigRepository, userId);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await usernameReservation?.release();
|
||||
}
|
||||
if (grantBootstrapAdmin) {
|
||||
await clearUsedSetupToken(instanceConfigRepository);
|
||||
}
|
||||
await users.upsertSettings(
|
||||
UserSettings.getDefaultUserSettings({
|
||||
userId,
|
||||
@@ -348,9 +373,6 @@ async function registerAccount(
|
||||
user,
|
||||
origin: AuthSession.resolveSessionOrigin(ctx, request),
|
||||
});
|
||||
if (grantBootstrapAdmin) {
|
||||
await instanceConfigRepository.markAdminBootstrapped();
|
||||
}
|
||||
return {
|
||||
user_id: user.id.toString(),
|
||||
token,
|
||||
@@ -365,11 +387,13 @@ function shouldAttemptBootstrapAdminGrant(
|
||||
usernameMode: boolean;
|
||||
pendingApproval: boolean;
|
||||
setupConfigured: boolean;
|
||||
setupTokenVerified: boolean;
|
||||
},
|
||||
): boolean {
|
||||
const claimedAccount = params.usernameMode ? params.hasPassword : params.rawEmail !== null;
|
||||
if (isSetupTokenEnforced(config)) return params.setupTokenVerified && claimedAccount;
|
||||
const localDevInstance = config.nodeEnv === 'development' && !config.dev.testModeEnabled;
|
||||
const setupBootstrapOpen = !params.setupConfigured;
|
||||
const claimedAccount = params.usernameMode ? params.hasPassword : params.rawEmail !== null;
|
||||
return (
|
||||
(config.instance.selfHosted || localDevInstance || setupBootstrapOpen) && claimedAccount && !params.pendingApproval
|
||||
);
|
||||
@@ -432,6 +456,20 @@ async function withdrawSignupOfUncreatedAccount(
|
||||
}
|
||||
}
|
||||
|
||||
async function releaseBootstrapAdminOfUncreatedAccount(
|
||||
instanceConfigRepository: InstanceConfigRepository,
|
||||
userId: UserID,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await instanceConfigRepository.releaseAdminBootstrap();
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{userId: userId.toString(), error},
|
||||
'[AuthRegistration] Failed to release the admin bootstrap claim of an account that was never created',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRegistrationAccess(
|
||||
instanceConfigRepository: InstanceConfigRepository,
|
||||
registrationUrlCode: string | null | undefined,
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {generateUniquePassword, HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {UserRepository} from '@app/api/user/repositories/UserRepository';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
describe('Password reset flow', () => {
|
||||
let harness: ApiTestHarness;
|
||||
@@ -104,6 +105,61 @@ describe('Password reset flow', () => {
|
||||
const login = await loginUser(harness, {email: account.email, password: newPassword});
|
||||
expect('token' in login && login.token.length > 0).toBe(true);
|
||||
});
|
||||
it('accepts a reset token for only one of several concurrent resets', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/auth/forgot')
|
||||
.body({email: account.email})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
const emails = await listTestEmails(harness, {recipient: account.email});
|
||||
const token = findLastTestEmail(emails, 'password_reset')!.metadata!.token!;
|
||||
const results = await Promise.all(
|
||||
Array.from({length: 4}, () =>
|
||||
createBuilderWithoutAuth(harness)
|
||||
.post('/auth/reset')
|
||||
.body({token, password: generateUniquePassword()})
|
||||
.executeRaw(),
|
||||
),
|
||||
);
|
||||
const statuses = results.map(({response}) => response.status).sort();
|
||||
expect(statuses).toEqual(
|
||||
[HTTP_STATUS.OK, HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.BAD_REQUEST, HTTP_STATUS.BAD_REQUEST].sort(),
|
||||
);
|
||||
});
|
||||
it('keeps a reset token usable when the reset fails before the password is written', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/auth/forgot')
|
||||
.body({email: account.email})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
const emails = await listTestEmails(harness, {recipient: account.email});
|
||||
const token = findLastTestEmail(emails, 'password_reset')!.metadata!.token!;
|
||||
const patchUpsert = vi
|
||||
.spyOn(UserRepository.prototype, 'patchUpsert')
|
||||
.mockRejectedValueOnce(new Error('the user row write failed'));
|
||||
try {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/auth/reset')
|
||||
.body({token, password: generateUniquePassword()})
|
||||
.expect(HTTP_STATUS.INTERNAL_SERVER_ERROR)
|
||||
.execute();
|
||||
} finally {
|
||||
patchUpsert.mockRestore();
|
||||
}
|
||||
const newPassword = generateUniquePassword();
|
||||
const resetResp = await createBuilderWithoutAuth<LoginSuccessResponse>(harness)
|
||||
.post('/auth/reset')
|
||||
.body({token, password: newPassword})
|
||||
.execute();
|
||||
expect(resetResp.token.length).toBeGreaterThan(0);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/auth/reset')
|
||||
.body({token, password: generateUniquePassword()})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
it('rejects invalid reset token', async () => {
|
||||
await createTestAccount(harness);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
|
||||
@@ -14,11 +14,19 @@ import {
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {getConfig} from '@app/api/Config';
|
||||
import {applySharedListUpdate, resetSharedListsForTests} from '@app/api/infrastructure/activity/SharedLists';
|
||||
import {getInstanceConfigRepository, getUserRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import {
|
||||
getInstanceConfigRepository,
|
||||
getRateLimitService,
|
||||
getUserRepository,
|
||||
} from '@app/api/middleware/ServiceSingletons';
|
||||
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import {ms} from 'itty-time';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
function bootstrapRegistrationBody(prefix: string): Record<string, unknown> {
|
||||
return {
|
||||
@@ -57,6 +65,37 @@ async function withBootstrapAdminConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz';
|
||||
const WRONG_SETUP_TOKEN = 'wrong-setup-token-0123456789-abcdefghijklmnopqrstuvwxyz';
|
||||
const SETUP_TOKEN_HASH_KEY = 'setup_token_hash';
|
||||
const TOO_MANY_REQUESTS = 429;
|
||||
|
||||
interface SetupTokenRegistrationResponse {
|
||||
user_id?: string;
|
||||
token?: string;
|
||||
registration_pending_approval?: boolean;
|
||||
code?: string;
|
||||
errors?: Array<{path: string; code: string}>;
|
||||
}
|
||||
|
||||
async function withSetupTokenInstance(callback: () => Promise<void>): Promise<void> {
|
||||
const config = getConfig();
|
||||
const originalSelfHosted = config.instance.selfHosted;
|
||||
const originalSetupToken = config.instance.setup.token;
|
||||
const originalSetupConfigured = config.instance.setup.configured;
|
||||
const originalRelaxRegistrationRateLimits = config.dev.relaxRegistrationRateLimits;
|
||||
try {
|
||||
config.instance.selfHosted = true;
|
||||
config.instance.setup.token = SETUP_TOKEN;
|
||||
await callback();
|
||||
} finally {
|
||||
config.instance.selfHosted = originalSelfHosted;
|
||||
config.instance.setup.token = originalSetupToken;
|
||||
config.instance.setup.configured = originalSetupConfigured;
|
||||
config.dev.relaxRegistrationRateLimits = originalRelaxRegistrationRateLimits;
|
||||
}
|
||||
}
|
||||
|
||||
async function expectUserACLs(userId: string, expectedACLs: Array<string>): Promise<void> {
|
||||
const user = await getUserRepository().findUniqueAssert(createUserID(BigInt(userId)));
|
||||
expect([...user.acls].sort()).toEqual([...expectedACLs].sort());
|
||||
@@ -123,31 +162,45 @@ describe('Auth registration', () => {
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
it('allows setup-open sessions to fetch instance config when bootstrap marker is stale', async () => {
|
||||
it('lets exactly one concurrent caller claim the admin bootstrap', async () => {
|
||||
const instanceConfigRepository = getInstanceConfigRepository();
|
||||
const claims = await Promise.all(Array.from({length: 5}, () => instanceConfigRepository.claimAdminBootstrap()));
|
||||
expect(claims.filter(Boolean)).toHaveLength(1);
|
||||
await expect(instanceConfigRepository.isAdminBootstrapped()).resolves.toBe(true);
|
||||
await expect(instanceConfigRepository.claimAdminBootstrap()).resolves.toBe(false);
|
||||
});
|
||||
it('keeps instance config closed to sessions without admin ACLs while setup is open', async () => {
|
||||
await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => {
|
||||
const instanceConfigRepository = getInstanceConfigRepository();
|
||||
await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}});
|
||||
await instanceConfigRepository.markAdminBootstrapped();
|
||||
const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupmarker'));
|
||||
await instanceConfigRepository.claimAdminBootstrap();
|
||||
const account = await registerUser(harness, bootstrapRegistrationBody('setupopennonadmin'));
|
||||
await expectUserACLs(account.user_id, []);
|
||||
await createBuilder(harness, account.token).get('/admin/instance/config').execute();
|
||||
await createBuilder(harness, account.token).get('/admin/instance/config').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
await createBuilder(harness, account.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({app_public: {setup: {configured: true}}})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
await expectUserACLs(account.user_id, []);
|
||||
await expect(instanceConfigRepository.getAppPublicConfig()).resolves.toMatchObject({
|
||||
setup: {configured: false},
|
||||
});
|
||||
});
|
||||
});
|
||||
it('repairs setup completer admin ACL when bootstrap marker is stale', async () => {
|
||||
it('lets the bootstrap admin complete setup', async () => {
|
||||
await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => {
|
||||
const instanceConfigRepository = getInstanceConfigRepository();
|
||||
await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}});
|
||||
await instanceConfigRepository.markAdminBootstrapped();
|
||||
const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupcomplete'));
|
||||
await expectUserACLs(account.user_id, []);
|
||||
|
||||
const account = await registerUser(harness, bootstrapRegistrationBody('setupopenowner'));
|
||||
await expectUserACLs(account.user_id, [AdminACLs.WILDCARD]);
|
||||
await createBuilder(harness, account.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({app_public: {setup: {configured: true}}})
|
||||
.execute();
|
||||
|
||||
await expectUserACLs(account.user_id, [AdminACLs.WILDCARD]);
|
||||
await createBuilder(harness, account.token).get('/admin/instance/config').execute();
|
||||
await expect(instanceConfigRepository.getAppPublicConfig()).resolves.toMatchObject({
|
||||
setup: {configured: true},
|
||||
});
|
||||
});
|
||||
});
|
||||
it('derives username from display name when username is omitted', async () => {
|
||||
@@ -237,3 +290,303 @@ describe('Auth registration', () => {
|
||||
expect(me.email).toBe('[email protected]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setup token registration', () => {
|
||||
let harness: ApiTestHarness;
|
||||
beforeAll(async () => {
|
||||
harness = await createAuthHarness();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
async function register(
|
||||
body: Record<string, unknown>,
|
||||
ipAddress?: string,
|
||||
): Promise<{status: number; json: SetupTokenRegistrationResponse}> {
|
||||
const builder = createBuilderWithoutAuth<SetupTokenRegistrationResponse>(harness).post('/auth/register').body(body);
|
||||
if (ipAddress) builder.header('x-forwarded-for', ipAddress);
|
||||
const {response, json} = await builder.executeRaw();
|
||||
return {status: response.status, json};
|
||||
}
|
||||
|
||||
function setupTokenErrors(json: SetupTokenRegistrationResponse): Array<{path: string; code: string}> {
|
||||
return (json.errors ?? []).map(({path, code}) => ({path, code}));
|
||||
}
|
||||
|
||||
async function registerAdmin(prefix: string, ipAddress?: string): Promise<string> {
|
||||
const {status, json} = await register({...bootstrapRegistrationBody(prefix), setup_token: SETUP_TOKEN}, ipAddress);
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
return json.user_id!;
|
||||
}
|
||||
|
||||
it('refuses a registration that sends no setup token', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const body = bootstrapRegistrationBody('tokenless');
|
||||
const {status, json} = await register(body);
|
||||
expect(status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}]);
|
||||
expect(await getUserRepository().findByEmail(body.email as string)).toBeNull();
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a registration that sends a wrong setup token', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const body = bootstrapRegistrationBody('wrongtoken');
|
||||
const {status, json} = await register({...body, setup_token: WRONG_SETUP_TOKEN});
|
||||
expect(status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID}]);
|
||||
expect(await getUserRepository().findByEmail(body.email as string)).toBeNull();
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('makes the registration that presents the setup token the admin', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const userId = await registerAdmin('tokenadmin');
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true);
|
||||
expect((await getInstanceConfigRepository().getConfig(SETUP_TOKEN_HASH_KEY)) ?? '').toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a setup token padded with whitespace', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const {status, json} = await register({
|
||||
...bootstrapRegistrationBody('paddedtoken'),
|
||||
setup_token: ` ${SETUP_TOKEN}\n`,
|
||||
});
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]);
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores the setup token once the admin exists', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
await registerAdmin('firstadmin');
|
||||
const tokenless = await register(bootstrapRegistrationBody('aftertokenless'));
|
||||
expect(tokenless.status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(tokenless.json.user_id!, []);
|
||||
const wrong = await register({...bootstrapRegistrationBody('afterwrong'), setup_token: WRONG_SETUP_TOKEN});
|
||||
expect(wrong.status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(wrong.json.user_id!, []);
|
||||
const repeated = await register({...bootstrapRegistrationBody('afterrepeat'), setup_token: SETUP_TOKEN});
|
||||
expect(repeated.status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(repeated.json.user_id!, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses an unclaimed registration that sends no setup token', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const {status, json} = await register({global_name: 'Unclaimed Visitor'});
|
||||
expect(status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(json)).toEqual([{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a plain account for an unclaimed registration that presents the setup token', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const {status, json} = await register({global_name: 'Unclaimed Operator', setup_token: SETUP_TOKEN});
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(json.user_id!, []);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
const userId = await registerAdmin('claimedoperator');
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['approval', 'closed'] as const)(
|
||||
'lets the setup token holder register as admin while registration is %s',
|
||||
async (mode) => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
await getInstanceConfigRepository().setRegistrationConfig({mode});
|
||||
const {status, json} = await register({...bootstrapRegistrationBody(`mode${mode}`), setup_token: SETUP_TOKEN});
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
expect(json.registration_pending_approval).toBeUndefined();
|
||||
expect(json.token).toBeTruthy();
|
||||
await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]);
|
||||
await expect(getInstanceConfigRepository().getPendingRegistrations()).resolves.toEqual([]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('reports the missing setup token before any other registration check', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
await getInstanceConfigRepository().setAppPublicConfig({legal: {terms_url: 'https://example.com/terms'}});
|
||||
const body = {...bootstrapRegistrationBody('noconsent'), consent: false};
|
||||
const tokenless = await register(body);
|
||||
expect(tokenless.status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(tokenless.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED},
|
||||
]);
|
||||
const verified = await register({...body, setup_token: SETUP_TOKEN});
|
||||
expect(verified.status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(verified.json)).toEqual([
|
||||
{path: 'consent', code: ValidationErrorCodes.MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY},
|
||||
]);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('locks an address out after ten wrong setup tokens', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
getConfig().dev.relaxRegistrationRateLimits = false;
|
||||
const attacker = '203.0.113.10';
|
||||
const operator = '203.0.113.20';
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const tokenless = await register(bootstrapRegistrationBody(`limitmissing${attempt}`), attacker);
|
||||
expect(setupTokenErrors(tokenless.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED},
|
||||
]);
|
||||
}
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const wrong = await register(
|
||||
{...bootstrapRegistrationBody(`limitwrong${attempt}`), setup_token: WRONG_SETUP_TOKEN},
|
||||
attacker,
|
||||
);
|
||||
expect(wrong.status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(wrong.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID},
|
||||
]);
|
||||
}
|
||||
const eleventh = await register(
|
||||
{...bootstrapRegistrationBody('limiteleventh'), setup_token: WRONG_SETUP_TOKEN},
|
||||
attacker,
|
||||
);
|
||||
expect(eleventh.status).toBe(TOO_MANY_REQUESTS);
|
||||
const lockedOut = await register(
|
||||
{...bootstrapRegistrationBody('limitlocked'), setup_token: SETUP_TOKEN},
|
||||
attacker,
|
||||
);
|
||||
expect(lockedOut.status).toBe(TOO_MANY_REQUESTS);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
const userId = await registerAdmin('limitoperator', operator);
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
});
|
||||
});
|
||||
|
||||
it('lets the setup token holder past the registration limiter of their address', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
getConfig().dev.relaxRegistrationRateLimits = false;
|
||||
const operator = '203.0.113.30';
|
||||
const registrationLimit = {
|
||||
identifier: `registration:ip:${getSameIpDecisionKey(operator) ?? operator}`,
|
||||
maxAttempts: 3,
|
||||
windowMs: ms('1 hour'),
|
||||
};
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await getRateLimitService().checkLimit(registrationLimit);
|
||||
}
|
||||
const userId = await registerAdmin('limitedoperator', operator);
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
const next = await register(bootstrapRegistrationBody('limitedmember'), operator);
|
||||
expect(next.status).toBe(TOO_MANY_REQUESTS);
|
||||
});
|
||||
});
|
||||
|
||||
it('ties the admin grant to the setup token on an instance configured through the environment', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
getConfig().instance.setup.configured = true;
|
||||
const tokenless = await register(bootstrapRegistrationBody('envtokenless'));
|
||||
expect(tokenless.status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(tokenless.json.user_id!, []);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
const wrong = await register({...bootstrapRegistrationBody('envwrong'), setup_token: WRONG_SETUP_TOKEN});
|
||||
expect(wrong.status).toBe(HTTP_STATUS.BAD_REQUEST);
|
||||
expect(setupTokenErrors(wrong.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID},
|
||||
]);
|
||||
const userId = await registerAdmin('envadmin');
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('ties the admin grant to the setup token on an instance configured in storage', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: true}});
|
||||
const tokenless = await register(bootstrapRegistrationBody('storedtokenless'));
|
||||
expect(tokenless.status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(tokenless.json.user_id!, []);
|
||||
const userId = await registerAdmin('storedadmin');
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves the hosted service alone even with a setup token configured', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
getConfig().instance.selfHosted = false;
|
||||
await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: false}});
|
||||
const {status, json} = await register(bootstrapRegistrationBody('hostedtokenless'));
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]);
|
||||
});
|
||||
});
|
||||
|
||||
it('releases the admin claim when the account cannot be created', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const create = vi.spyOn(getUserRepository(), 'create').mockRejectedValueOnce(new Error('insert failed'));
|
||||
try {
|
||||
const failed = await register({...bootstrapRegistrationBody('failedadmin'), setup_token: SETUP_TOKEN});
|
||||
expect(failed.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
||||
expect(create).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
create.mockRestore();
|
||||
}
|
||||
await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false);
|
||||
const tokenless = await register(bootstrapRegistrationBody('failedtokenless'));
|
||||
expect(setupTokenErrors(tokenless.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED},
|
||||
]);
|
||||
const userId = await registerAdmin('retriedadmin');
|
||||
await expectUserACLs(userId, [AdminACLs.WILDCARD]);
|
||||
await expect(getInstanceConfigRepository().getConfig('admin_bootstrap_user_id')).resolves.toBe(userId);
|
||||
});
|
||||
});
|
||||
|
||||
it('burns a generated setup token once the admin exists', async () => {
|
||||
await withSetupTokenInstance(async () => {
|
||||
const config = getConfig();
|
||||
const originalNodeEnv = config.nodeEnv;
|
||||
config.instance.setup.token = null;
|
||||
config.nodeEnv = 'production';
|
||||
try {
|
||||
const repository = getInstanceConfigRepository();
|
||||
const generated = await repository.createSetupTokenIfAbsent();
|
||||
expect(generated).not.toBeNull();
|
||||
const wrong = await register({...bootstrapRegistrationBody('genwrong'), setup_token: SETUP_TOKEN});
|
||||
expect(setupTokenErrors(wrong.json)).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID},
|
||||
]);
|
||||
const {status, json} = await register({...bootstrapRegistrationBody('genadmin'), setup_token: generated});
|
||||
expect(status).toBe(HTTP_STATUS.OK);
|
||||
await expectUserACLs(json.user_id!, [AdminACLs.WILDCARD]);
|
||||
expect(await repository.getConfig(SETUP_TOKEN_HASH_KEY)).toBe('');
|
||||
await expect(repository.verifySetupToken(generated)).resolves.toBe(false);
|
||||
} finally {
|
||||
config.nodeEnv = originalNodeEnv;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('burns the stored setup token when an admin marks setup as configured', async () => {
|
||||
await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => {
|
||||
const repository = getInstanceConfigRepository();
|
||||
await repository.setAppPublicConfig({setup: {configured: false}});
|
||||
const account = await registerUser(harness, bootstrapRegistrationBody('configuringadmin'));
|
||||
await repository.releaseAdminBootstrap();
|
||||
const generated = await repository.createSetupTokenIfAbsent();
|
||||
await expect(repository.verifySetupToken(generated)).resolves.toBe(true);
|
||||
await createBuilder(harness, account.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({app_public: {setup: {configured: true}}})
|
||||
.execute();
|
||||
await expect(repository.isAdminBootstrapped()).resolves.toBe(true);
|
||||
await expect(repository.verifySetupToken(generated)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -283,6 +283,7 @@ export interface APIConfig {
|
||||
};
|
||||
setup: {
|
||||
configured: boolean;
|
||||
token: string | null;
|
||||
};
|
||||
accountIdentity: AccountIdentityMode | null;
|
||||
tagStyle: TagStyle | null;
|
||||
|
||||
@@ -71,9 +71,6 @@ export interface IStorageService {
|
||||
contentLength: number;
|
||||
contentRange?: string | null;
|
||||
contentType?: string | null;
|
||||
cacheControl?: string | null;
|
||||
contentDisposition?: string | null;
|
||||
expires?: Date | null;
|
||||
etag?: string | null;
|
||||
lastModified?: Date | null;
|
||||
} | null>;
|
||||
|
||||
@@ -225,3 +225,55 @@ describe('StorageService.listObjects', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function copySourceFor(endpoint: string, forcePathStyle: boolean): Promise<unknown> {
|
||||
const service = new StorageService({
|
||||
endpoint,
|
||||
forcePathStyle,
|
||||
region: 'us-east-1',
|
||||
accessKeyId: 'fluxer',
|
||||
secretAccessKey: 'fluxer-secret',
|
||||
});
|
||||
let copySource: unknown;
|
||||
Object.assign(service, {
|
||||
client: {
|
||||
send: async (command: {input: {CopySource?: unknown}}) => {
|
||||
copySource = command.input.CopySource;
|
||||
return {};
|
||||
},
|
||||
},
|
||||
});
|
||||
return service
|
||||
.copyObject({
|
||||
sourceBucket: 'fluxer-uploads',
|
||||
sourceKey: 'a b/c+d.pdf',
|
||||
destinationBucket: 'fluxer-cdn',
|
||||
destinationKey: 'attachments/1/2/c.pdf',
|
||||
})
|
||||
.then(() => copySource);
|
||||
}
|
||||
|
||||
describe('StorageService.copyObject copy source', () => {
|
||||
it.each([
|
||||
['http://seaweedfs:8333', true],
|
||||
['http://seaweedfs:8333/', true],
|
||||
['https://s3.example.test', false],
|
||||
])(
|
||||
'names only the bucket and key for an endpoint without a path (%s, path style %s)',
|
||||
async (endpoint, pathStyle) => {
|
||||
await expect(copySourceFor(endpoint, pathStyle)).resolves.toBe('fluxer-uploads/a%20b/c%2Bd.pdf');
|
||||
},
|
||||
);
|
||||
|
||||
it('puts the endpoint path in front of the bucket with path-style addressing', async () => {
|
||||
await expect(copySourceFor('https://s3.example.test/real%20bucket/sub/', true)).resolves.toBe(
|
||||
'real%20bucket/sub/fluxer-uploads/a%20b/c%2Bd.pdf',
|
||||
);
|
||||
});
|
||||
|
||||
it('puts the endpoint path between the bucket and the key with virtual-hosted addressing', async () => {
|
||||
await expect(copySourceFor('https://s3.example.test/real%20bucket/sub', false)).resolves.toBe(
|
||||
'fluxer-uploads/real%20bucket/sub/a%20b/c%2Bd.pdf',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -107,6 +107,7 @@ function extractStreamFromGet(out: GetObjectCommandOutput): Readable {
|
||||
}
|
||||
|
||||
const REJECTED_SERVER_SIDE_COPY_ERRORS = new Set([
|
||||
'NoSuchBucket',
|
||||
'NoSuchKey',
|
||||
'NotFound',
|
||||
'NotImplemented',
|
||||
@@ -115,6 +116,13 @@ const REJECTED_SERVER_SIDE_COPY_ERRORS = new Set([
|
||||
'MethodNotAllowed',
|
||||
]);
|
||||
|
||||
function encodedEndpointPath(endpoint: string): Array<string> {
|
||||
return (URL.parse(endpoint)?.pathname ?? '')
|
||||
.split('/')
|
||||
.filter((segment) => segment !== '')
|
||||
.map((segment) => encodeURIComponent(decodeURIComponent(segment)));
|
||||
}
|
||||
|
||||
function isRejectedServerSideCopy(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof S3ServiceException &&
|
||||
@@ -126,6 +134,7 @@ export class StorageService implements IStorageService {
|
||||
private readonly client: S3Client;
|
||||
private readonly presignClient: S3Client;
|
||||
private readonly provider: S3ProviderSettings;
|
||||
private readonly endpointPath: Array<string>;
|
||||
|
||||
constructor(provider?: S3ProviderSettings) {
|
||||
this.provider = provider ?? {
|
||||
@@ -136,6 +145,7 @@ export class StorageService implements IStorageService {
|
||||
accessKeyId: Config.s3.accessKeyId,
|
||||
secretAccessKey: Config.s3.secretAccessKey,
|
||||
};
|
||||
this.endpointPath = encodedEndpointPath(this.provider.endpoint);
|
||||
this.client = buildPooledS3Client({
|
||||
endpoint: this.provider.endpoint,
|
||||
region: this.provider.region,
|
||||
@@ -230,6 +240,8 @@ export class StorageService implements IStorageService {
|
||||
contentType,
|
||||
contentLength,
|
||||
expiresAt,
|
||||
cacheControl,
|
||||
contentDisposition,
|
||||
}: {
|
||||
bucket: string;
|
||||
key: string;
|
||||
@@ -237,6 +249,8 @@ export class StorageService implements IStorageService {
|
||||
contentType?: string;
|
||||
contentLength?: number;
|
||||
expiresAt?: Date;
|
||||
cacheControl?: string;
|
||||
contentDisposition?: string;
|
||||
}): Promise<void> {
|
||||
const size = contentLength ?? (await fs.promises.stat(filePath)).size;
|
||||
if (size <= STREAM_UPLOAD_PART_BYTES) {
|
||||
@@ -249,6 +263,8 @@ export class StorageService implements IStorageService {
|
||||
ContentType: contentType,
|
||||
ContentLength: size,
|
||||
Expires: expiresAt,
|
||||
CacheControl: cacheControl,
|
||||
ContentDisposition: contentDisposition,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -269,6 +285,8 @@ export class StorageService implements IStorageService {
|
||||
Body: stream,
|
||||
ContentType: contentType,
|
||||
Expires: expiresAt,
|
||||
CacheControl: cacheControl,
|
||||
ContentDisposition: contentDisposition,
|
||||
},
|
||||
partSize: STREAM_UPLOAD_PART_BYTES,
|
||||
queueSize: STREAM_UPLOAD_CONCURRENCY,
|
||||
@@ -417,9 +435,6 @@ export class StorageService implements IStorageService {
|
||||
contentLength: number;
|
||||
contentRange?: string | null;
|
||||
contentType?: string | null;
|
||||
cacheControl?: string | null;
|
||||
contentDisposition?: string | null;
|
||||
expires?: Date | null;
|
||||
etag?: string | null;
|
||||
lastModified?: Date | null;
|
||||
} | null> {
|
||||
@@ -432,9 +447,6 @@ export class StorageService implements IStorageService {
|
||||
contentLength: out.ContentLength ?? 0,
|
||||
contentRange: out.ContentRange ?? null,
|
||||
contentType: out.ContentType ?? null,
|
||||
cacheControl: out.CacheControl ?? null,
|
||||
contentDisposition: out.ContentDisposition ?? null,
|
||||
expires: out.Expires ?? null,
|
||||
etag: out.ETag ?? null,
|
||||
lastModified: out.LastModified ?? null,
|
||||
};
|
||||
@@ -459,6 +471,10 @@ export class StorageService implements IStorageService {
|
||||
|
||||
async writeObjectToDisk(bucket: string, key: string, filePath: string): Promise<void> {
|
||||
await this.ensureDirectoryExists(path.dirname(filePath));
|
||||
await this.downloadObject(bucket, key, filePath);
|
||||
}
|
||||
|
||||
private async downloadObject(bucket: string, key: string, filePath: string): Promise<GetObjectCommandOutput> {
|
||||
const out = await this.client.send(new GetObjectCommand({Bucket: bucket, Key: key}));
|
||||
const stream = extractStreamFromGet(out);
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
@@ -468,6 +484,7 @@ export class StorageService implements IStorageService {
|
||||
writeStream.destroy();
|
||||
throw error;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async copyObject({
|
||||
@@ -492,13 +509,13 @@ export class StorageService implements IStorageService {
|
||||
new CopyObjectCommand({
|
||||
Bucket: destinationBucket,
|
||||
Key: destinationKey,
|
||||
CopySource: `${encodeURIComponent(sourceBucket)}/${sourceKey.split('/').map(encodeURIComponent).join('/')}`,
|
||||
CopySource: this.copySource(sourceBucket, sourceKey),
|
||||
ContentType: newContentType,
|
||||
MetadataDirective: newContentType ? 'REPLACE' : undefined,
|
||||
}),
|
||||
);
|
||||
} catch (copyError) {
|
||||
if (sourceBucket === destinationBucket || !isRejectedServerSideCopy(copyError)) {
|
||||
if (!isRejectedServerSideCopy(copyError)) {
|
||||
throw copyError;
|
||||
}
|
||||
await this.copyObjectThroughApi(
|
||||
@@ -508,6 +525,14 @@ export class StorageService implements IStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
private copySource(bucket: string, key: string): string {
|
||||
const encodedBucket = encodeURIComponent(bucket);
|
||||
const location = this.provider.forcePathStyle
|
||||
? [...this.endpointPath, encodedBucket]
|
||||
: [encodedBucket, ...this.endpointPath];
|
||||
return [...location, ...key.split('/').map(encodeURIComponent)].join('/');
|
||||
}
|
||||
|
||||
private async copyObjectThroughApi(
|
||||
{
|
||||
sourceBucket,
|
||||
@@ -524,38 +549,28 @@ export class StorageService implements IStorageService {
|
||||
},
|
||||
copyError: unknown,
|
||||
): Promise<void> {
|
||||
const source = await this.streamObject({bucket: sourceBucket, key: sourceKey});
|
||||
if (!source) {
|
||||
throw copyError;
|
||||
}
|
||||
Logger.warn(
|
||||
{sourceBucket, destinationBucket, error: copyError},
|
||||
'Object storage rejected a cross-bucket copy, copying through the API instead',
|
||||
);
|
||||
const upload = new Upload({
|
||||
client: this.client,
|
||||
params: {
|
||||
Bucket: destinationBucket,
|
||||
Key: destinationKey,
|
||||
Body: source.body,
|
||||
ContentType: newContentType ?? source.contentType ?? undefined,
|
||||
const downloadedSource = temporaryFile();
|
||||
try {
|
||||
const source = await this.downloadObject(sourceBucket, sourceKey, downloadedSource);
|
||||
Logger.warn(
|
||||
{sourceBucket, destinationBucket, error: copyError},
|
||||
'Object storage rejected a server-side copy, copying through the API instead',
|
||||
);
|
||||
await this.uploadObjectFromFile({
|
||||
bucket: destinationBucket,
|
||||
key: destinationKey,
|
||||
filePath: downloadedSource,
|
||||
contentType: newContentType ?? source.ContentType,
|
||||
...(newContentType
|
||||
? {}
|
||||
: {
|
||||
CacheControl: source.cacheControl ?? undefined,
|
||||
ContentDisposition: source.contentDisposition ?? undefined,
|
||||
Expires: source.expires ?? undefined,
|
||||
cacheControl: source.CacheControl,
|
||||
contentDisposition: source.ContentDisposition,
|
||||
expiresAt: source.Expires,
|
||||
}),
|
||||
},
|
||||
partSize: STREAM_UPLOAD_PART_BYTES,
|
||||
queueSize: STREAM_UPLOAD_CONCURRENCY,
|
||||
leavePartsOnError: false,
|
||||
});
|
||||
try {
|
||||
await upload.done();
|
||||
} catch (error) {
|
||||
source.body.destroy();
|
||||
throw error;
|
||||
});
|
||||
} finally {
|
||||
await fs.promises.unlink(downloadedSource).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('StorageService copies on providers that reject cross-bucket CopyObject
|
||||
expect(ceph.objects.has(`${CDN}/attachments/1/4/missing.pdf`)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not retry a failed same-bucket copy through the API', async () => {
|
||||
it('reports a missing same-bucket source as a missing object', async () => {
|
||||
await expect(
|
||||
storage().copyObject({
|
||||
sourceBucket: CDN,
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {StorageService} from '@app/api/infrastructure/StorageService';
|
||||
import {server} from '@app/api/test/msw/server';
|
||||
import {HttpResponse, http} from 'msw';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const HOST = 's3.single-bucket.test';
|
||||
const REAL_BUCKET = 'realbucket';
|
||||
|
||||
function errorResponse(code: string, status: number): HttpResponse<string> {
|
||||
return new HttpResponse(
|
||||
`<?xml version="1.0" encoding="UTF-8"?><Error><Code>${code}</Code><Message></Message><RequestId>tx0</RequestId></Error>`,
|
||||
{status, headers: {'Content-Type': 'application/xml'}},
|
||||
);
|
||||
}
|
||||
|
||||
function splitBucket(path: string): {bucket: string; key: string} {
|
||||
const [bucket = '', ...key] = path.split('/');
|
||||
return {bucket, key: key.join('/')};
|
||||
}
|
||||
|
||||
function singleBucketStore(unresolvedSourceCode = 'NoSuchBucket') {
|
||||
const objects = new Map<string, {body: Uint8Array; contentType: string}>();
|
||||
const resolve = (url: URL): {bucket: string; key: string} => {
|
||||
const path = decodeURIComponent(url.pathname.slice(1));
|
||||
if (url.hostname === HOST) {
|
||||
return splitBucket(path);
|
||||
}
|
||||
return {bucket: url.hostname.slice(0, -HOST.length - 1), key: path};
|
||||
};
|
||||
server.use(
|
||||
http.all(/^https:\/\/([a-z0-9-]+\.)?s3\.single-bucket\.test\//u, async ({request}) => {
|
||||
const target = resolve(new URL(request.url));
|
||||
if (target.bucket !== REAL_BUCKET) {
|
||||
return errorResponse('NoSuchBucket', 404);
|
||||
}
|
||||
if (request.method === 'GET') {
|
||||
const object = objects.get(target.key);
|
||||
if (!object) {
|
||||
return errorResponse('NoSuchKey', 404);
|
||||
}
|
||||
return new HttpResponse(object.body, {
|
||||
status: 200,
|
||||
headers: {'Content-Type': object.contentType, 'Content-Length': String(object.body.length)},
|
||||
});
|
||||
}
|
||||
if (request.method !== 'PUT') {
|
||||
return errorResponse('NotImplemented', 501);
|
||||
}
|
||||
const copySource = request.headers.get('x-amz-copy-source');
|
||||
if (!copySource) {
|
||||
objects.set(target.key, {
|
||||
body: new Uint8Array(await request.arrayBuffer()),
|
||||
contentType: request.headers.get('content-type') ?? 'application/octet-stream',
|
||||
});
|
||||
return new HttpResponse(null, {status: 200, headers: {ETag: '"x"'}});
|
||||
}
|
||||
const source = splitBucket(decodeURIComponent(copySource.replace(/^\//u, '')));
|
||||
if (source.bucket !== REAL_BUCKET) {
|
||||
return errorResponse(unresolvedSourceCode, unresolvedSourceCode === 'AccessDenied' ? 403 : 404);
|
||||
}
|
||||
const object = objects.get(source.key);
|
||||
if (!object) {
|
||||
return errorResponse('NoSuchKey', 404);
|
||||
}
|
||||
objects.set(target.key, {...object, contentType: request.headers.get('content-type') ?? object.contentType});
|
||||
return new HttpResponse(
|
||||
'<?xml version="1.0" encoding="UTF-8"?><CopyObjectResult><ETag>"x"</ETag></CopyObjectResult>',
|
||||
{status: 200, headers: {'Content-Type': 'application/xml'}},
|
||||
);
|
||||
}),
|
||||
);
|
||||
return objects;
|
||||
}
|
||||
|
||||
function storage(endpoint: string, forcePathStyle: boolean): StorageService {
|
||||
return new StorageService({
|
||||
endpoint,
|
||||
forcePathStyle,
|
||||
region: 'us-east-1',
|
||||
accessKeyId: 'TEST',
|
||||
secretAccessKey: 'TEST',
|
||||
});
|
||||
}
|
||||
|
||||
const PDF = new TextEncoder().encode('%PDF-1.7\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n');
|
||||
|
||||
describe('StorageService with every bucket inside one real bucket', () => {
|
||||
let objects: ReturnType<typeof singleBucketStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
objects = singleBucketStore();
|
||||
});
|
||||
|
||||
it('copies a non-media attachment when the endpoint names the bucket with path-style addressing', async () => {
|
||||
const service = storage(`https://${HOST}/${REAL_BUCKET}`, true);
|
||||
await service.uploadObject({bucket: 'fluxer-uploads', key: 'upload 1', body: PDF});
|
||||
|
||||
await expect(
|
||||
service.copyObjectWithMetadataStripping({
|
||||
sourceBucket: 'fluxer-uploads',
|
||||
sourceKey: 'upload 1',
|
||||
destinationBucket: 'fluxer-cdn',
|
||||
destinationKey: 'attachments/1/2/file.pdf',
|
||||
contentType: 'application/pdf',
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(objects.get('fluxer-cdn/attachments/1/2/file.pdf')?.contentType).toBe('application/pdf');
|
||||
await expect(service.readObject('fluxer-cdn', 'attachments/1/2/file.pdf')).resolves.toEqual(PDF);
|
||||
});
|
||||
|
||||
it('copies within a bucket whose virtual host keeps the endpoint path as a key prefix', async () => {
|
||||
const service = storage(`https://${HOST}/prefix`, false);
|
||||
await service.uploadObject({bucket: REAL_BUCKET, key: 'upload-2', body: PDF});
|
||||
|
||||
await service.copyObject({
|
||||
sourceBucket: REAL_BUCKET,
|
||||
sourceKey: 'upload-2',
|
||||
destinationBucket: REAL_BUCKET,
|
||||
destinationKey: 'attachments/1/3/file.pdf',
|
||||
newContentType: 'application/pdf',
|
||||
});
|
||||
|
||||
expect(objects.get('prefix/attachments/1/3/file.pdf')?.contentType).toBe('application/pdf');
|
||||
await expect(service.readObject(REAL_BUCKET, 'attachments/1/3/file.pdf')).resolves.toEqual(PDF);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(['NoSuchBucket', 'AccessDenied'])(
|
||||
'StorageService with the bucket in the endpoint hostname, store answers %s',
|
||||
(code) => {
|
||||
let objects: ReturnType<typeof singleBucketStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
objects = singleBucketStore(code);
|
||||
});
|
||||
|
||||
it('copies a non-media attachment out of the uploads prefix', async () => {
|
||||
const service = storage(`https://${REAL_BUCKET}.${HOST}`, true);
|
||||
await service.uploadObject({bucket: 'fluxer-uploads', key: 'upload 1', body: PDF});
|
||||
|
||||
await expect(
|
||||
service.copyObjectWithMetadataStripping({
|
||||
sourceBucket: 'fluxer-uploads',
|
||||
sourceKey: 'upload 1',
|
||||
destinationBucket: 'fluxer',
|
||||
destinationKey: 'attachments/1/2/file.pdf',
|
||||
contentType: 'application/pdf',
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(objects.get('fluxer/attachments/1/2/file.pdf')).toEqual({body: PDF, contentType: 'application/pdf'});
|
||||
});
|
||||
|
||||
it('copies within the cdn prefix', async () => {
|
||||
const service = storage(`https://${REAL_BUCKET}.${HOST}`, true);
|
||||
await service.uploadObject({bucket: 'fluxer', key: 'emojis/1', body: PDF, contentType: 'image/png'});
|
||||
|
||||
await service.copyObject({
|
||||
sourceBucket: 'fluxer',
|
||||
sourceKey: 'emojis/1',
|
||||
destinationBucket: 'fluxer',
|
||||
destinationKey: 'emojis/2',
|
||||
});
|
||||
|
||||
expect(objects.get('fluxer/emojis/2')).toEqual({body: PDF, contentType: 'image/png'});
|
||||
});
|
||||
|
||||
it('reports a missing source as a missing object', async () => {
|
||||
const service = storage(`https://${REAL_BUCKET}.${HOST}`, true);
|
||||
|
||||
await expect(
|
||||
service.copyObject({
|
||||
sourceBucket: 'fluxer',
|
||||
sourceKey: 'emojis/missing',
|
||||
destinationBucket: 'fluxer',
|
||||
destinationKey: 'emojis/3',
|
||||
}),
|
||||
).rejects.toMatchObject({name: 'NoSuchKey'});
|
||||
expect(objects.has('fluxer/emojis/3')).toBe(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createHash} from 'node:crypto';
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {Config, getConfig} from '@app/api/Config';
|
||||
import type {CassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution';
|
||||
import {setCassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution';
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from '@app/api/instance/InstanceConfigRepository';
|
||||
import {getLegalUrls, setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls';
|
||||
import {getInstanceProductName, setCachedProductName} from '@app/api/instance/ProductName';
|
||||
import {ensureSetupToken, isSetupTokenEnforced} from '@app/api/instance/SetupToken';
|
||||
import {InstanceConfigWriteRaceExecutor} from '@app/api/instance/tests/InstanceConfigWriteRaceExecutor';
|
||||
import {dockerAvailable, startPostgresKv, stopPostgresKv} from '@app/api/test/DockerTestContainer';
|
||||
import {InMemoryCassandraQueryExecutor} from '@app/api/test/InMemoryCassandraQueryExecutor';
|
||||
@@ -38,6 +40,8 @@ const REGISTRATION_CONFIG_KEY = 'registration_config';
|
||||
const REGISTRATION_URLS_KEY = 'registration_urls';
|
||||
const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals';
|
||||
const POSTGRES_KV_TABLE = 'kv_instance_config_races';
|
||||
const SETUP_TOKEN_HASH_KEY = 'setup_token_hash';
|
||||
const ENV_SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz';
|
||||
describe('InstanceConfigRepository', () => {
|
||||
const repositories: Array<InstanceConfigRepository> = [];
|
||||
|
||||
@@ -945,6 +949,271 @@ function registrationUrlParams(maxUses: number | null) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('InstanceConfigRepository setup token', () => {
|
||||
const repositories: Array<InstanceConfigRepository> = [];
|
||||
const config = getConfig();
|
||||
let executor: InstanceConfigWriteRaceExecutor;
|
||||
let kvProvider: MockKVProvider;
|
||||
let originalSelfHosted: boolean;
|
||||
let originalNodeEnv: typeof config.nodeEnv;
|
||||
let originalSetupToken: typeof config.instance.setup.token;
|
||||
let originalSetupConfigured: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
executor = new InstanceConfigWriteRaceExecutor(new InMemoryCassandraQueryExecutor());
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
kvProvider = new MockKVProvider();
|
||||
originalSelfHosted = config.instance.selfHosted;
|
||||
originalNodeEnv = config.nodeEnv;
|
||||
originalSetupToken = config.instance.setup.token;
|
||||
originalSetupConfigured = config.instance.setup.configured;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.instance.selfHosted = originalSelfHosted;
|
||||
config.nodeEnv = originalNodeEnv;
|
||||
config.instance.setup.token = originalSetupToken;
|
||||
config.instance.setup.configured = originalSetupConfigured;
|
||||
for (const repository of repositories) {
|
||||
repository.shutdown();
|
||||
}
|
||||
repositories.length = 0;
|
||||
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
|
||||
});
|
||||
|
||||
function createPod(): InstanceConfigRepository {
|
||||
const repository = new InstanceConfigRepository(kvProvider);
|
||||
repositories.push(repository);
|
||||
return repository;
|
||||
}
|
||||
|
||||
function useSelfHostedProduction(): void {
|
||||
config.instance.selfHosted = true;
|
||||
config.nodeEnv = 'production';
|
||||
config.instance.setup.token = null;
|
||||
config.instance.setup.configured = false;
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
const accountExists = async () => true;
|
||||
const accountMissing = async () => false;
|
||||
|
||||
it('generates one setup token when two pods start together and stores only its hash', async () => {
|
||||
const tokens = await Promise.all([createPod().createSetupTokenIfAbsent(), createPod().createSetupTokenIfAbsent()]);
|
||||
const generated = tokens.filter((token) => token !== null);
|
||||
expect(generated).toHaveLength(1);
|
||||
expect(tokens.filter((token) => token === null)).toHaveLength(1);
|
||||
const stored = await executor.readDirectly(SETUP_TOKEN_HASH_KEY);
|
||||
expect(stored).toBe(sha256(generated[0]!));
|
||||
expect(stored).not.toBe(generated[0]);
|
||||
expect(await createPod().createSetupTokenIfAbsent()).toBeNull();
|
||||
});
|
||||
|
||||
it('verifies only the generated setup token', async () => {
|
||||
const pod = createPod();
|
||||
for (const presented of ['anything', '', null, undefined]) {
|
||||
expect(await pod.verifySetupToken(presented)).toBe(false);
|
||||
}
|
||||
const token = await pod.createSetupTokenIfAbsent();
|
||||
expect(await pod.verifySetupToken(token)).toBe(true);
|
||||
expect(await createPod().verifySetupToken(token)).toBe(true);
|
||||
for (const presented of [`${token}x`, sha256(token!), '', null, undefined]) {
|
||||
expect(await pod.verifySetupToken(presented)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('takes the environment setup token over a generated one', async () => {
|
||||
const pod = createPod();
|
||||
const generated = await pod.createSetupTokenIfAbsent();
|
||||
config.instance.setup.token = ENV_SETUP_TOKEN;
|
||||
expect(await pod.verifySetupToken(ENV_SETUP_TOKEN)).toBe(true);
|
||||
expect(await pod.verifySetupToken(generated)).toBe(false);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(sha256(generated!));
|
||||
});
|
||||
|
||||
it('burns the stored setup token and leaves it alone when the admin bootstrap is only claimed', async () => {
|
||||
useSelfHostedProduction();
|
||||
const pod = createPod();
|
||||
const token = await pod.createSetupTokenIfAbsent();
|
||||
await pod.claimAdminBootstrap();
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(sha256(token!));
|
||||
await pod.burnSetupToken();
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe('');
|
||||
expect(await pod.verifySetupToken(token)).toBe(false);
|
||||
expect(await pod.verifySetupToken('')).toBe(false);
|
||||
await ensureSetupToken(pod, accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe('');
|
||||
});
|
||||
|
||||
it('writes nothing when there is no stored setup token to burn', async () => {
|
||||
await createPod().burnSetupToken();
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('releases a claimed admin bootstrap so it can be claimed again', async () => {
|
||||
const pod = createPod();
|
||||
await pod.releaseAdminBootstrap();
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBeNull();
|
||||
expect(await pod.claimAdminBootstrap()).toBe(true);
|
||||
expect(await pod.isAdminBootstrapped()).toBe(true);
|
||||
await pod.releaseAdminBootstrap();
|
||||
expect(await pod.isAdminBootstrapped()).toBe(false);
|
||||
expect(await createPod().isAdminBootstrapped()).toBe(false);
|
||||
expect(await pod.claimAdminBootstrap()).toBe(true);
|
||||
expect(await pod.claimAdminBootstrap()).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a won admin bootstrap claim when the refresh cannot be published', async () => {
|
||||
const pod = createPod();
|
||||
await pod.isAdminBootstrapped();
|
||||
vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down'));
|
||||
expect(await pod.claimAdminBootstrap()).toBe(true);
|
||||
expect(await pod.isAdminBootstrapped()).toBe(true);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('true');
|
||||
});
|
||||
|
||||
it('enforces the setup token only on self-hosted instances that run in production or configure one', async () => {
|
||||
const pod = createPod();
|
||||
const gate = async () => ({
|
||||
enforced: isSetupTokenEnforced(config),
|
||||
open: await pod.isSetupTokenGateOpen(),
|
||||
required: await pod.isSetupTokenRequired(),
|
||||
});
|
||||
config.instance.setup.configured = false;
|
||||
config.instance.selfHosted = false;
|
||||
config.nodeEnv = 'production';
|
||||
config.instance.setup.token = ENV_SETUP_TOKEN;
|
||||
expect(await gate()).toEqual({enforced: false, open: false, required: false});
|
||||
config.instance.selfHosted = true;
|
||||
config.nodeEnv = 'development';
|
||||
config.instance.setup.token = null;
|
||||
expect(await gate()).toEqual({enforced: false, open: false, required: false});
|
||||
config.instance.setup.token = ENV_SETUP_TOKEN;
|
||||
expect(await gate()).toEqual({enforced: true, open: true, required: true});
|
||||
config.nodeEnv = 'production';
|
||||
config.instance.setup.token = null;
|
||||
expect(await gate()).toEqual({enforced: true, open: true, required: true});
|
||||
config.instance.setup.configured = true;
|
||||
expect(await gate()).toEqual({enforced: true, open: true, required: false});
|
||||
config.instance.setup.configured = false;
|
||||
expect(await gate()).toEqual({enforced: true, open: true, required: true});
|
||||
await pod.setAppPublicConfig({setup: {configured: true}});
|
||||
expect(await gate()).toEqual({enforced: true, open: true, required: false});
|
||||
await pod.claimAdminBootstrap();
|
||||
expect(await gate()).toEqual({enforced: true, open: false, required: false});
|
||||
});
|
||||
|
||||
it('stores a setup token on the first production start and keeps it on the next', async () => {
|
||||
useSelfHostedProduction();
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
const stored = await executor.readDirectly(SETUP_TOKEN_HASH_KEY);
|
||||
expect(stored).toMatch(/^[a-f0-9]{64}$/);
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBe(stored);
|
||||
});
|
||||
|
||||
it('stores a setup token when setup is configured but no admin exists', async () => {
|
||||
useSelfHostedProduction();
|
||||
config.instance.setup.configured = true;
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toMatch(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
|
||||
it('stores no setup token when one is set in the environment', async () => {
|
||||
useSelfHostedProduction();
|
||||
config.instance.setup.token = ENV_SETUP_TOKEN;
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('stores no setup token on the hosted service or in self-hosted development', async () => {
|
||||
useSelfHostedProduction();
|
||||
config.instance.selfHosted = false;
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
config.instance.selfHosted = true;
|
||||
config.nodeEnv = 'development';
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('stores no setup token once the admin exists', async () => {
|
||||
useSelfHostedProduction();
|
||||
const pod = createPod();
|
||||
await pod.claimAdminBootstrap();
|
||||
await ensureSetupToken(pod, accountExists);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('releases an admin bootstrap claim at start when the claiming account was never created', async () => {
|
||||
useSelfHostedProduction();
|
||||
const pod = createPod();
|
||||
expect(await pod.claimAdminBootstrap(createUserID(4242n))).toBe(true);
|
||||
expect(await executor.readDirectly('admin_bootstrap_user_id')).toBe('4242');
|
||||
await ensureSetupToken(createPod(), accountExists);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('true');
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toBeNull();
|
||||
const restarted = createPod();
|
||||
await ensureSetupToken(restarted, accountMissing);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('false');
|
||||
expect(await restarted.isSetupTokenGateOpen()).toBe(true);
|
||||
expect(await executor.readDirectly(SETUP_TOKEN_HASH_KEY)).toMatch(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
|
||||
it('keeps an admin bootstrap claim at start when no claiming account is recorded', async () => {
|
||||
useSelfHostedProduction();
|
||||
const pod = createPod();
|
||||
await pod.claimAdminBootstrap();
|
||||
expect(await executor.readDirectly('admin_bootstrap_user_id')).toBeNull();
|
||||
await ensureSetupToken(createPod(), accountMissing);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('true');
|
||||
await pod.releaseAdminBootstrap();
|
||||
await pod.claimAdminBootstrap(createUserID(4242n));
|
||||
await pod.releaseAdminBootstrap();
|
||||
await pod.claimAdminBootstrap();
|
||||
expect(await executor.readDirectly('admin_bootstrap_user_id')).toBe('');
|
||||
await ensureSetupToken(createPod(), accountMissing);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('true');
|
||||
});
|
||||
|
||||
it('keeps a won admin bootstrap claim when the claiming account cannot be recorded', async () => {
|
||||
const pod = createPod();
|
||||
executor.watch('admin_bootstrap_user_id');
|
||||
executor.competeBeforeEachWrite(async () => {
|
||||
throw new Error('database down');
|
||||
});
|
||||
expect(await pod.claimAdminBootstrap(createUserID(4242n))).toBe(true);
|
||||
expect(await pod.isAdminBootstrapped()).toBe(true);
|
||||
expect(await executor.readDirectly('admin_bootstrapped')).toBe('true');
|
||||
expect(await executor.readDirectly('admin_bootstrap_user_id')).toBeNull();
|
||||
});
|
||||
|
||||
it('confirms the admin bootstrap against the database and repairs a stale snapshot', async () => {
|
||||
const stale = createPod();
|
||||
const writer = createPod();
|
||||
expect(await stale.isAdminBootstrapped()).toBe(false);
|
||||
vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down'));
|
||||
await writer.claimAdminBootstrap();
|
||||
expect(await stale.isAdminBootstrapped()).toBe(false);
|
||||
expect(await stale.confirmAdminBootstrapped()).toBe(true);
|
||||
expect(await stale.isAdminBootstrapped()).toBe(true);
|
||||
vi.spyOn(kvProvider, 'publish').mockRejectedValueOnce(new Error('kv down'));
|
||||
await writer.releaseAdminBootstrap();
|
||||
expect(await stale.isAdminBootstrapped()).toBe(true);
|
||||
expect(await stale.confirmAdminBootstrapped()).toBe(false);
|
||||
expect(await stale.isAdminBootstrapped()).toBe(false);
|
||||
});
|
||||
|
||||
it('refuses to start when the setup token cannot be stored', async () => {
|
||||
useSelfHostedProduction();
|
||||
const pod = createPod();
|
||||
vi.spyOn(pod, 'createSetupTokenIfAbsent').mockRejectedValueOnce(new Error('database down'));
|
||||
await expect(ensureSetupToken(pod, accountExists)).rejects.toThrow('database down');
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstanceConfigRepository concurrent writes', () => {
|
||||
describe('in memory', () => {
|
||||
describeConcurrentInstanceConfigWrites(async () => new InMemoryCassandraQueryExecutor());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import {createUserID, type UserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '@app/api/config/APIConfig';
|
||||
import type {StoredLimitConfig} from '@app/api/constants/LimitConfig';
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
import {InstanceConfigCache} from '@app/api/instance/InstanceConfigCache';
|
||||
import {setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls';
|
||||
import {getDefaultProductName, setCachedProductName} from '@app/api/instance/ProductName';
|
||||
import {isSetupTokenEnforced} from '@app/api/instance/SetupToken';
|
||||
import {normalizeSsoAllowedEmailDomains} from '@app/api/instance/SsoConfigValidation';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {isStoredLimitConfig} from '@app/api/limits/LimitConfigValidation';
|
||||
@@ -44,6 +46,7 @@ import {
|
||||
import {InstanceConfiguration, Users} from '@app/api/Tables';
|
||||
import {DEFAULT_DECAY_CONSTANTS, DEFAULT_RENEWAL_CONSTANTS} from '@app/api/utils/AttachmentDecay';
|
||||
import {isJsonRecord} from '@app/api/utils/JsonBoundaryUtils';
|
||||
import {randomBase64Url, secretMatchesHash, sha256Hex} from '@app/api/utils/SecretHash';
|
||||
import {
|
||||
type AccountIdentityMode,
|
||||
AccountIdentityModes,
|
||||
@@ -120,6 +123,8 @@ const REGISTRATION_URLS_KEY = 'registration_urls';
|
||||
const REGISTRATION_PENDING_APPROVALS_KEY = 'registration_pending_approvals';
|
||||
const APP_PUBLIC_CONFIG_KEY = 'app_public_config';
|
||||
const ADMIN_BOOTSTRAP_KEY = 'admin_bootstrapped';
|
||||
const ADMIN_BOOTSTRAP_USER_ID_KEY = 'admin_bootstrap_user_id';
|
||||
const SETUP_TOKEN_HASH_KEY = 'setup_token_hash';
|
||||
const INSTANCE_POLICY_CONFIG_KEY = 'instance_policy_config';
|
||||
const LIMIT_CONFIG_KEY = 'limit_config';
|
||||
const INSTANCE_INTEGRATIONS_CONFIG_KEY = 'instance_integrations_config';
|
||||
@@ -1484,8 +1489,95 @@ export class InstanceConfigRepository {
|
||||
return (await this.getConfig(ADMIN_BOOTSTRAP_KEY)) === 'true';
|
||||
}
|
||||
|
||||
async markAdminBootstrapped(): Promise<void> {
|
||||
await this.setConfig(ADMIN_BOOTSTRAP_KEY, 'true');
|
||||
async confirmAdminBootstrapped(): Promise<boolean> {
|
||||
const cache = this.configCache;
|
||||
const stored = (await this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_KEY)) === 'true';
|
||||
if (stored !== (await this.isAdminBootstrapped())) cache.update(ADMIN_BOOTSTRAP_KEY, stored ? 'true' : 'false');
|
||||
return stored;
|
||||
}
|
||||
|
||||
async claimAdminBootstrap(userId: UserID | null = null): Promise<boolean> {
|
||||
const cache = this.configCache;
|
||||
const {written} = await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_KEY, (raw) => ({
|
||||
value: raw === 'true' ? null : 'true',
|
||||
result: undefined,
|
||||
}));
|
||||
if (!written) return false;
|
||||
await this.recordAdminBootstrapClaimant(cache, userId);
|
||||
await this.publishAdminBootstrapRefresh(cache.sourceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async recordAdminBootstrapClaimant(cache: InstanceConfigCache, userId: UserID | null): Promise<void> {
|
||||
const claimant = userId?.toString() ?? '';
|
||||
try {
|
||||
await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_USER_ID_KEY, (raw) => ({
|
||||
value: (raw ?? '') === claimant ? null : claimant,
|
||||
result: undefined,
|
||||
}));
|
||||
} catch (error) {
|
||||
Logger.error({error}, 'Could not record which account claimed the admin bootstrap');
|
||||
}
|
||||
}
|
||||
|
||||
async getUnconfirmedAdminBootstrapClaimant(): Promise<UserID | null> {
|
||||
const [bootstrapped, claimant] = await Promise.all([
|
||||
this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_KEY),
|
||||
this.fetchConfigFromDatabase(ADMIN_BOOTSTRAP_USER_ID_KEY),
|
||||
]);
|
||||
if (bootstrapped !== 'true' || !claimant || !/^\d+$/.test(claimant)) return null;
|
||||
return createUserID(BigInt(claimant));
|
||||
}
|
||||
|
||||
async releaseAdminBootstrap(): Promise<void> {
|
||||
const cache = this.configCache;
|
||||
const {written} = await this.compareAndSetStoredValue(cache, ADMIN_BOOTSTRAP_KEY, (raw) => ({
|
||||
value: raw === 'true' ? 'false' : null,
|
||||
result: undefined,
|
||||
}));
|
||||
if (written) await this.publishAdminBootstrapRefresh(cache.sourceId);
|
||||
}
|
||||
|
||||
private async publishAdminBootstrapRefresh(sourceId: string): Promise<void> {
|
||||
try {
|
||||
await this.publishRefresh(sourceId);
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{error},
|
||||
'Could not publish the admin bootstrap change. Other API processes see it after the next instance config change or a restart',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async isSetupTokenGateOpen(): Promise<boolean> {
|
||||
return isSetupTokenEnforced(Config) && !(await this.isAdminBootstrapped());
|
||||
}
|
||||
|
||||
async isSetupTokenRequired(): Promise<boolean> {
|
||||
return (await this.isSetupTokenGateOpen()) && !(await this.getAppPublicConfig()).setup.configured;
|
||||
}
|
||||
|
||||
async createSetupTokenIfAbsent(): Promise<string | null> {
|
||||
const token = randomBase64Url(32);
|
||||
const {written} = await this.compareAndSetStoredValue(this.configCache, SETUP_TOKEN_HASH_KEY, (raw) => ({
|
||||
value: raw ? null : sha256Hex(token),
|
||||
result: undefined,
|
||||
}));
|
||||
return written ? token : null;
|
||||
}
|
||||
|
||||
async verifySetupToken(presented: string | null | undefined): Promise<boolean> {
|
||||
const expectedHash = Config.instance.setup.token
|
||||
? sha256Hex(Config.instance.setup.token)
|
||||
: await this.fetchConfigFromDatabase(SETUP_TOKEN_HASH_KEY);
|
||||
return secretMatchesHash(presented, expectedHash);
|
||||
}
|
||||
|
||||
async burnSetupToken(): Promise<void> {
|
||||
await this.compareAndSetStoredValue(this.configCache, SETUP_TOKEN_HASH_KEY, (raw) => ({
|
||||
value: raw ? '' : null,
|
||||
result: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
async getAccountIdentity(): Promise<AccountIdentity> {
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
isDiscoveryNotModified,
|
||||
nextDiscoveryValidators,
|
||||
} from '@app/api/instance/DiscoveryValidators';
|
||||
import {assertSetupToken} from '@app/api/instance/SetupToken';
|
||||
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@app/api/RateLimitConfig';
|
||||
import {isBillingActive, isPremiumTieringActive, isStripeServiceable} from '@app/api/stripe/BillingConfigCache';
|
||||
import type {HonoEnv} from '@app/api/types/HonoEnv';
|
||||
import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp';
|
||||
import {Validator} from '@app/api/Validator';
|
||||
import {AccountIdentityModes, TagStyles} from '@fluxer/constants/src/AccountIdentityConstants';
|
||||
import {API_CODE_VERSION} from '@fluxer/constants/src/AppConstants';
|
||||
@@ -130,10 +132,13 @@ export function InstanceController(app: Hono<HonoEnv>) {
|
||||
instanceConfigRepository.getDomainMigrationConfig(),
|
||||
instanceConfigRepository.getAccountIdentity(),
|
||||
]);
|
||||
const accountIdentityLocked =
|
||||
const openSetup =
|
||||
Config.instance.selfHosted && !appPublicConfig.setup.configured
|
||||
? await instanceConfigRepository.isAccountIdentityLocked()
|
||||
: undefined;
|
||||
? {
|
||||
account_identity_locked: await instanceConfigRepository.isAccountIdentityLocked(),
|
||||
setup_token_required: await instanceConfigRepository.isSetupTokenRequired(),
|
||||
}
|
||||
: {};
|
||||
const discovery = buildDiscoveryResponse(
|
||||
buildDiscoveryStaticInput(
|
||||
gifService,
|
||||
@@ -142,7 +147,7 @@ export function InstanceController(app: Hono<HonoEnv>) {
|
||||
setup: {
|
||||
...appPublicConfig.setup,
|
||||
admin_url: Config.endpoints.admin || null,
|
||||
...(accountIdentityLocked === undefined ? {} : {account_identity_locked: accountIdentityLocked}),
|
||||
...openSetup,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -187,15 +192,23 @@ export function InstanceController(app: Hono<HonoEnv>) {
|
||||
security: [],
|
||||
tags: ['Instance'],
|
||||
description:
|
||||
'Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED.',
|
||||
'Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED. While the instance requires a setup token, the request must send it as setup_token.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {mode, tag_style} = ctx.req.valid('json');
|
||||
const {mode, tag_style, setup_token} = ctx.req.valid('json');
|
||||
const instanceConfigRepository = ctx.get('instanceConfigRepository');
|
||||
const {config, rateLimit, cache} = ctx.get('apiContext').services;
|
||||
await assertSetupToken({
|
||||
rateLimit,
|
||||
config,
|
||||
repository: instanceConfigRepository,
|
||||
presented: setup_token,
|
||||
clientIp: requireConfiguredClientIp(ctx.req.raw, config.proxy),
|
||||
});
|
||||
if (mode === AccountIdentityModes.USERNAME && tag_style === TagStyles.RANDOM) {
|
||||
throw InputValidationError.fromCode('tag_style', ValidationErrorCodes.TAG_STYLE_REQUIRES_EMAIL_SIGN_IN);
|
||||
}
|
||||
const instanceConfigRepository = ctx.get('instanceConfigRepository');
|
||||
const identity = await withAccountIdentitySetupLock(ctx.get('apiContext').services.cache, async () => {
|
||||
const identity = await withAccountIdentitySetupLock(cache, async () => {
|
||||
if (await instanceConfigRepository.isAccountIdentityLocked()) {
|
||||
throw new AccountIdentityLockedError();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {UserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {APIConfig} from '@app/api/config/APIConfig';
|
||||
import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {createRateLimitError} from '@app/api/utils/RateLimitUtils';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService';
|
||||
import {ms} from 'itty-time';
|
||||
|
||||
const SETUP_TOKEN_FAILURE_MAX_ATTEMPTS = 10;
|
||||
const SETUP_TOKEN_FAILURE_WINDOW_MS = ms('30 minutes');
|
||||
|
||||
export function isSetupTokenEnforced(config: APIConfig): boolean {
|
||||
return config.instance.selfHosted && (config.nodeEnv === 'production' || config.instance.setup.token !== null);
|
||||
}
|
||||
|
||||
async function releaseAdminBootstrapOfMissingAccount(
|
||||
repository: InstanceConfigRepository,
|
||||
userExists: (userId: UserID) => Promise<boolean>,
|
||||
): Promise<void> {
|
||||
const claimant = await repository.getUnconfirmedAdminBootstrapClaimant();
|
||||
if (claimant === null || (await userExists(claimant))) return;
|
||||
await repository.releaseAdminBootstrap();
|
||||
Logger.warn(
|
||||
{user_id: claimant.toString()},
|
||||
'The account that claimed the admin bootstrap does not exist. The claim is released and the setup token applies again',
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureSetupToken(
|
||||
repository: InstanceConfigRepository,
|
||||
userExists: (userId: UserID) => Promise<boolean>,
|
||||
): Promise<void> {
|
||||
if (!isSetupTokenEnforced(Config)) return;
|
||||
await releaseAdminBootstrapOfMissingAccount(repository, userExists);
|
||||
if (!(await repository.isSetupTokenGateOpen())) return;
|
||||
if (Config.instance.setup.token !== null) {
|
||||
Logger.info('Setup token read from FLUXER_SETUP_TOKEN');
|
||||
return;
|
||||
}
|
||||
const token = await repository.createSetupTokenIfAbsent();
|
||||
if (token === null) {
|
||||
Logger.info(
|
||||
'A setup token from an earlier start is still unused. Set FLUXER_SETUP_TOKEN to replace it if it is lost',
|
||||
);
|
||||
return;
|
||||
}
|
||||
Logger.warn(
|
||||
{setup_token: token},
|
||||
'Setup token generated. This instance has not recorded an admin account yet. The registration that presents this token becomes an admin. It is shown once',
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearUsedSetupToken(repository: InstanceConfigRepository): Promise<void> {
|
||||
try {
|
||||
await repository.burnSetupToken();
|
||||
} catch (error) {
|
||||
Logger.error({error}, 'Could not clear the stored setup token after the admin bootstrap was claimed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertSetupToken(params: {
|
||||
rateLimit: IRateLimitService;
|
||||
config: APIConfig;
|
||||
repository: InstanceConfigRepository;
|
||||
presented: string | null | undefined;
|
||||
clientIp: string;
|
||||
}): Promise<boolean> {
|
||||
const {rateLimit, config, repository, clientIp} = params;
|
||||
if (!isSetupTokenEnforced(config)) return false;
|
||||
const setupConfigured = (await repository.getAppPublicConfig()).setup.configured;
|
||||
const adminBootstrapped = setupConfigured
|
||||
? await repository.isAdminBootstrapped()
|
||||
: await repository.confirmAdminBootstrapped();
|
||||
if (adminBootstrapped) return false;
|
||||
const token = params.presented?.trim();
|
||||
if (!token) {
|
||||
if (!setupConfigured) {
|
||||
throw InputValidationError.fromCode('setup_token', ValidationErrorCodes.SETUP_TOKEN_REQUIRED);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const failureLimit = config.dev.relaxRegistrationRateLimits
|
||||
? null
|
||||
: {
|
||||
identifier: `setup-token:ip:${getSameIpDecisionKey(clientIp) ?? clientIp}`,
|
||||
maxAttempts: SETUP_TOKEN_FAILURE_MAX_ATTEMPTS,
|
||||
windowMs: SETUP_TOKEN_FAILURE_WINDOW_MS,
|
||||
};
|
||||
if (failureLimit) {
|
||||
const failureState = await rateLimit.peekLimit(failureLimit);
|
||||
if (failureState.remaining === 0) {
|
||||
throw createRateLimitError({
|
||||
...failureState,
|
||||
allowed: false,
|
||||
retryAfter: Math.ceil(failureLimit.windowMs / failureLimit.maxAttempts / 1000),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!(await repository.verifySetupToken(token))) {
|
||||
if (failureLimit) await rateLimit.checkLimit(failureLimit);
|
||||
throw InputValidationError.fromCode('setup_token', ValidationErrorCodes.SETUP_TOKEN_INVALID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createHash} from 'node:crypto';
|
||||
import {createAuthHarness, createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {getConfig} from '@app/api/Config';
|
||||
import {getCachedAccountIdentity, getCachedAccountIdentityMode} from '@app/api/instance/AccountIdentityModeCache';
|
||||
@@ -14,10 +15,12 @@ import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const ACCOUNT_IDENTITY_CONFIG_KEY = 'account_identity_config';
|
||||
const SETUP_TOKEN = 'setup-token-for-tests-0123456789-abcdefghijklmnopqrstuvwxyz';
|
||||
const WRONG_SETUP_TOKEN = 'wrong-setup-token-0123456789-abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
interface DiscoveryResponse {
|
||||
features: {account_identity: string; emails_enabled: boolean; tag_style: string};
|
||||
app_public: {setup: {configured: boolean; account_identity_locked?: boolean}};
|
||||
app_public: {setup: {configured: boolean; account_identity_locked?: boolean; setup_token_required?: boolean}};
|
||||
}
|
||||
|
||||
interface AdminInstanceConfigResponse {
|
||||
@@ -33,6 +36,7 @@ describe('Account identity mode', () => {
|
||||
let originalSetupConfigured: boolean;
|
||||
let originalAccountIdentity: typeof config.instance.accountIdentity;
|
||||
let originalTagStyle: typeof config.instance.tagStyle;
|
||||
let originalSetupToken: typeof config.instance.setup.token;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createAuthHarness();
|
||||
@@ -45,6 +49,7 @@ describe('Account identity mode', () => {
|
||||
originalSetupConfigured = config.instance.setup.configured;
|
||||
originalAccountIdentity = config.instance.accountIdentity;
|
||||
originalTagStyle = config.instance.tagStyle;
|
||||
originalSetupToken = config.instance.setup.token;
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -53,6 +58,7 @@ describe('Account identity mode', () => {
|
||||
config.instance.setup.configured = originalSetupConfigured;
|
||||
config.instance.accountIdentity = originalAccountIdentity;
|
||||
config.instance.tagStyle = originalTagStyle;
|
||||
config.instance.setup.token = originalSetupToken;
|
||||
repository.clearCacheForTesting();
|
||||
});
|
||||
afterAll(async () => {
|
||||
@@ -239,7 +245,7 @@ describe('Account identity mode', () => {
|
||||
|
||||
it('stamps email when the admin was bootstrapped', async () => {
|
||||
useFreshSelfHostedInstance();
|
||||
await repository.markAdminBootstrapped();
|
||||
await repository.claimAdminBootstrap();
|
||||
await repository.ensureAccountIdentityMode();
|
||||
expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'existing_instance'});
|
||||
});
|
||||
@@ -321,7 +327,7 @@ describe('Account identity mode', () => {
|
||||
it('skips the users probe when a cheaper signal already decides', async () => {
|
||||
useFreshSelfHostedInstance();
|
||||
const probe = vi.spyOn(repository as unknown as {hasAnyUser: () => Promise<boolean>}, 'hasAnyUser');
|
||||
await repository.markAdminBootstrapped();
|
||||
await repository.claimAdminBootstrap();
|
||||
await repository.ensureAccountIdentityMode();
|
||||
expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'existing_instance'});
|
||||
expect(await repository.isAccountIdentityLocked()).toBe(true);
|
||||
@@ -540,6 +546,77 @@ describe('Account identity mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup token', () => {
|
||||
async function putModeWithToken(setupToken?: string) {
|
||||
return await createBuilderWithoutAuth<{
|
||||
mode?: string;
|
||||
tag_style?: string;
|
||||
errors?: Array<{path: string; code: string}>;
|
||||
}>(harness)
|
||||
.put('/instance/setup/account-identity')
|
||||
.body(
|
||||
setupToken === undefined
|
||||
? {mode: AccountIdentityModes.EMAIL}
|
||||
: {mode: AccountIdentityModes.EMAIL, setup_token: setupToken},
|
||||
)
|
||||
.executeRaw();
|
||||
}
|
||||
|
||||
it('reports in discovery whether registration needs the setup token', async () => {
|
||||
useFreshSelfHostedInstance();
|
||||
await repository.ensureAccountIdentityMode();
|
||||
expect((await fetchDiscovery()).app_public.setup.setup_token_required).toBe(false);
|
||||
config.instance.setup.token = SETUP_TOKEN;
|
||||
const {text, json} = await createBuilderWithoutAuth<DiscoveryResponse>(harness)
|
||||
.get('/.well-known/fluxer')
|
||||
.executeRaw();
|
||||
expect(json.app_public.setup.setup_token_required).toBe(true);
|
||||
expect(text).not.toContain(SETUP_TOKEN);
|
||||
expect(text).not.toContain(createHash('sha256').update(SETUP_TOKEN).digest('hex'));
|
||||
await repository.claimAdminBootstrap();
|
||||
expect((await fetchDiscovery()).app_public.setup.setup_token_required).toBe(false);
|
||||
await repository.setAppPublicConfig({setup: {configured: true}});
|
||||
expect((await fetchDiscovery()).app_public.setup).not.toHaveProperty('setup_token_required');
|
||||
});
|
||||
|
||||
it('leaves the setup token out of discovery on the hosted service', async () => {
|
||||
config.instance.selfHosted = false;
|
||||
config.instance.setup.token = SETUP_TOKEN;
|
||||
await repository.setAppPublicConfig({setup: {configured: false}});
|
||||
expect((await fetchDiscovery()).app_public.setup).not.toHaveProperty('setup_token_required');
|
||||
});
|
||||
|
||||
it('needs the setup token to choose the sign-in method while one is required', async () => {
|
||||
useFreshSelfHostedInstance();
|
||||
await repository.ensureAccountIdentityMode();
|
||||
config.instance.setup.token = SETUP_TOKEN;
|
||||
const missing = await putModeWithToken();
|
||||
expect(missing.response.status).toBe(400);
|
||||
expect(missing.json.errors?.map(({path, code}) => ({path, code}))).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_REQUIRED},
|
||||
]);
|
||||
const wrong = await putModeWithToken(WRONG_SETUP_TOKEN);
|
||||
expect(wrong.response.status).toBe(400);
|
||||
expect(wrong.json.errors?.map(({path, code}) => ({path, code}))).toEqual([
|
||||
{path: 'setup_token', code: ValidationErrorCodes.SETUP_TOKEN_INVALID},
|
||||
]);
|
||||
expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.USERNAME});
|
||||
const correct = await putModeWithToken(SETUP_TOKEN);
|
||||
expect(correct.response.status).toBe(200);
|
||||
expect(correct.json).toEqual({mode: AccountIdentityModes.EMAIL, tag_style: 'none'});
|
||||
expect(await readStoredRow()).toMatchObject({mode: AccountIdentityModes.EMAIL, source: 'setup'});
|
||||
});
|
||||
|
||||
it('chooses the sign-in method without a setup token when none is required', async () => {
|
||||
useFreshSelfHostedInstance();
|
||||
await repository.ensureAccountIdentityMode();
|
||||
const tokenless = await putModeWithToken();
|
||||
expect(tokenless.response.status).toBe(200);
|
||||
const ignored = await putModeWithToken(WRONG_SETUP_TOKEN);
|
||||
expect(ignored.response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin instance config', () => {
|
||||
async function fetchAdminConfig(token: string): Promise<AdminInstanceConfigResponse> {
|
||||
return await createBuilder<AdminInstanceConfigResponse>(harness, token).get('/admin/instance/config').execute();
|
||||
|
||||
@@ -47,6 +47,7 @@ import {InstanceConfigRepository} from '@app/api/instance/InstanceConfigReposito
|
||||
import {type LegalUrls, resolveLegalUrls} from '@app/api/instance/LegalUrls';
|
||||
import {NatsConfigPublisher} from '@app/api/instance/NatsConfigPublisher';
|
||||
import {getInstanceProductName} from '@app/api/instance/ProductName';
|
||||
import {ensureSetupToken} from '@app/api/instance/SetupToken';
|
||||
import {InviteRepository} from '@app/api/invite/InviteRepository';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {LimitConfigService} from '@app/api/limits/LimitConfigService';
|
||||
@@ -482,6 +483,11 @@ export async function initializeServiceSingletons(): Promise<void> {
|
||||
assertServiceSingletonInitializationActive(owner);
|
||||
await getInstanceConfigRepository().ensureAccountIdentityMode();
|
||||
assertServiceSingletonInitializationActive(owner);
|
||||
await ensureSetupToken(
|
||||
getInstanceConfigRepository(),
|
||||
async (userId) => (await getUserRepository().findUnique(userId)) !== null,
|
||||
);
|
||||
assertServiceSingletonInitializationActive(owner);
|
||||
await limitConfigService.initialize();
|
||||
assertServiceSingletonInitializationActive(owner);
|
||||
limitConfigService.setAsGlobalInstance();
|
||||
|
||||
Generated
+13
-1
@@ -11390,7 +11390,7 @@
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
|
||||
}
|
||||
},
|
||||
"description": "Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED.",
|
||||
"description": "Sets how people sign in on a new self-hosted instance, and for email sign-in whether usernames are unique with no tag. Username sign-in always uses unique usernames. It works only before setup is finished and before the first account exists. After that it fails with ACCOUNT_IDENTITY_LOCKED. While the instance requires a setup token, the request must send it as setup_token.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -29472,6 +29472,10 @@
|
||||
"tag_style": {
|
||||
"description": "How usernames are tagged. Defaults to none. Username sign-in accepts only none",
|
||||
"$ref": "#/components/schemas/TagStyleSchema"
|
||||
},
|
||||
"setup_token": {
|
||||
"description": "Setup token of a new self-hosted instance. Required until the first admin account exists",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["mode"]
|
||||
@@ -32830,6 +32834,10 @@
|
||||
],
|
||||
"enum": ["dark", "dark_legacy", "coal", "light", "system"],
|
||||
"type": "string"
|
||||
},
|
||||
"setup_token": {
|
||||
"description": "Setup token of a new self-hosted instance. Required until the first admin account exists",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -33569,6 +33577,10 @@
|
||||
"account_identity_locked": {
|
||||
"description": "Present only while a self-hosted instance is unconfigured. True when the sign-in method can no longer change",
|
||||
"type": "boolean"
|
||||
},
|
||||
"setup_token_required": {
|
||||
"description": "Present only while a self-hosted instance is unconfigured. True when registration needs the setup token",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["configured", "admin_url"],
|
||||
|
||||
@@ -6,7 +6,7 @@ import {afterAll, beforeAll, describe, expect, test} from 'vitest';
|
||||
|
||||
// Mirrors the live BRL/TRY configuration after the repricing: the BRL slots point at the new list
|
||||
// prices, TRY is not configured at all (production has no TRY prices), and every retired price id
|
||||
// lives only in the legacy map.
|
||||
// is only in the legacy map.
|
||||
const MOCK_PRICES = {
|
||||
monthlyUsd: 'price_monthly_usd',
|
||||
monthlyBrl: 'price_1TMbqsFPC94Os7FdfElrIIaZ',
|
||||
|
||||
@@ -216,9 +216,6 @@ export class MockStorageService implements IStorageService {
|
||||
contentLength: number;
|
||||
contentRange?: string | null;
|
||||
contentType?: string | null;
|
||||
cacheControl?: string | null;
|
||||
contentDisposition?: string | null;
|
||||
expires?: Date | null;
|
||||
etag?: string | null;
|
||||
lastModified?: Date | null;
|
||||
} | null> {
|
||||
@@ -247,9 +244,6 @@ export class MockStorageService implements IStorageService {
|
||||
contentLength: slice.length,
|
||||
contentRange,
|
||||
contentType: obj?.contentType ?? 'application/octet-stream',
|
||||
cacheControl: null,
|
||||
contentDisposition: null,
|
||||
expires: null,
|
||||
etag: computeEtag(data),
|
||||
lastModified: obj?.lastModified ?? null,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Fluxer">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#4641D9">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="msapplication-navbutton-color" content="#4641D9">
|
||||
<meta name="msapplication-TileColor" content="#4641D9">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"author": "Fluxer Platform AB <[email protected]>",
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"**/*.css"
|
||||
"**/*.css",
|
||||
"./src/app/AbortSignalPolyfill.ts"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
function abortSignalTimeout(milliseconds: number): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(new DOMException('The operation timed out.', 'TimeoutError')), milliseconds);
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
function abortSignalAny(signals: Iterable<AbortSignal>): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
const sources = [...signals];
|
||||
const aborted = sources.find((source) => source.aborted);
|
||||
if (aborted) {
|
||||
controller.abort(aborted.reason);
|
||||
return controller.signal;
|
||||
}
|
||||
for (const source of sources) {
|
||||
source.addEventListener('abort', () => controller.abort(source.reason), {once: true, signal: controller.signal});
|
||||
}
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
export function installAbortSignalPolyfill(target: typeof AbortSignal = AbortSignal): void {
|
||||
if (typeof target.timeout !== 'function') {
|
||||
target.timeout = abortSignalTimeout;
|
||||
}
|
||||
if (typeof target.any !== 'function') {
|
||||
target.any = abortSignalAny;
|
||||
}
|
||||
}
|
||||
|
||||
installAbortSignalPolyfill();
|
||||
Loaded 100 of 293 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user