mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-09 12:12:25 +09:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811341bc2f | ||
|
|
98fa41dcf0 | ||
|
|
b52a0b5d5f | ||
|
|
effeaaa435 | ||
|
|
27fc634bc9 | ||
|
|
69d93f9fee | ||
|
|
00620715da | ||
|
|
1ec8f31253 | ||
|
|
1abde06824 | ||
|
|
b54016653b | ||
|
|
ee74d61f27 | ||
|
|
1f18d3262d | ||
|
|
8ea7707b37 | ||
|
|
7b40df5d6c | ||
|
|
6f98de33f7 | ||
|
|
efe94ed094 | ||
|
|
603b936536 | ||
|
|
d87351eefe | ||
|
|
76e6891f5b | ||
|
|
5040ae2c10 | ||
|
|
87df92e2c2 | ||
|
|
237aff666d | ||
|
|
11645cbf28 | ||
|
|
e297a6a653 | ||
|
|
1eed347ffb | ||
|
|
4aa7a3e181 | ||
|
|
69786d3b49 | ||
|
|
2fb5fb1abb | ||
|
|
a9265cbb39 | ||
|
|
ee2d11ee0a | ||
|
|
632067b552 | ||
|
|
840dc3dfa5 | ||
|
|
4e6f9b539c | ||
|
|
98cce4815d | ||
|
|
b375abc20a | ||
|
|
21cb7ba69c | ||
|
|
be69333eaf | ||
|
|
9a074adb11 | ||
|
|
2df82b2b5e | ||
|
|
d691047884 | ||
|
|
c2e7fde5bc | ||
|
|
7e4d5137f8 | ||
|
|
376afd2ad6 | ||
|
|
e3fcedbec5 | ||
|
|
7c9564bcad | ||
|
|
cfed6cc4e0 | ||
|
|
c7bd1be3e4 | ||
|
|
2161d84701 | ||
|
|
eaeeb3b502 | ||
|
|
dc32a7c70e | ||
|
|
ab0b483fbe | ||
|
|
6e2f90b03c | ||
|
|
5e0806f479 | ||
|
|
dfdfffe5de | ||
|
|
f5e32aed31 | ||
|
|
710c1aeaa8 | ||
|
|
af49cd6cc4 | ||
|
|
ca719e7b5e | ||
|
|
ab4069ed0e | ||
|
|
12bfaa83ba | ||
|
|
1076728241 | ||
|
|
360b984adc | ||
|
|
dcdf7e1d93 | ||
|
|
e8cb167dbf | ||
|
|
0b3418dcbe | ||
|
|
ec7649193c |
@@ -2,3 +2,5 @@
|
||||
fluxer_static/** -text -diff
|
||||
fluxer_static/**/*.md text diff
|
||||
packages/fonts/files/** -text -diff
|
||||
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.wasm -text -diff
|
||||
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.tar.gz -text -diff
|
||||
|
||||
Generated
+7
-2
@@ -1785,8 +1785,10 @@ name = "fluxer-gifs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"fluxer-svc",
|
||||
"fluxer_common",
|
||||
"futures",
|
||||
"hmac 0.13.0",
|
||||
"moka",
|
||||
"reqwest",
|
||||
@@ -1823,6 +1825,7 @@ dependencies = [
|
||||
"cc",
|
||||
"clap",
|
||||
"criterion",
|
||||
"flate2",
|
||||
"fluxer_common",
|
||||
"futures-util",
|
||||
"hex",
|
||||
@@ -1850,6 +1853,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http 0.7.1",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
@@ -2038,6 +2042,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -5830,9 +5835,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
],
|
||||
"linter": {"rules": {"style": {"noRestrictedImports": "off"}}}
|
||||
},
|
||||
{
|
||||
"includes": ["fluxer_app/src/**/*.worklet.js"],
|
||||
"javascript": {"globals": ["AudioWorkletProcessor", "registerProcessor", "sampleRate", "currentTime"]}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.astro"],
|
||||
"linter": {"rules": {"correctness": {"noUnusedImports": "off", "noUnusedVariables": "off"}}},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-api
|
||||
description: Fluxer HTTP API and background job workers
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,244 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,24 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,8 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,86 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-gateway
|
||||
description: A Helm chart for the Fluxer realtime gateway.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,280 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
{{- $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 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,86 @@
|
||||
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: {}
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-infra
|
||||
description: NATS and Valkey for a Fluxer installation.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,282 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,71 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,67 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,108 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-ingress
|
||||
description: Ingress routing for the public Fluxer endpoints.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,27 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,58 @@
|
||||
{{- $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 }}
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-media-proxy
|
||||
description: Fluxer media proxy and upload relay workloads.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,87 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,191 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,72 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-push
|
||||
description: Fluxer push notification delivery service
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,71 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,205 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,65 @@
|
||||
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
|
||||
@@ -0,0 +1,6 @@
|
||||
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
|
||||
@@ -0,0 +1,203 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,145 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,89 @@
|
||||
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: {}
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: fluxer-web
|
||||
description: Fluxer web app proxy and admin dashboard.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "v1"
|
||||
@@ -0,0 +1,80 @@
|
||||
{{- 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 }}
|
||||
@@ -0,0 +1,172 @@
|
||||
{{- $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 }}
|
||||
@@ -0,0 +1,83 @@
|
||||
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_ENV: production
|
||||
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
|
||||
env:
|
||||
RELEASE_CHANNEL: stable
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://web.example.com/api
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 96Mi
|
||||
limits:
|
||||
memory: 384Mi
|
||||
@@ -160,7 +160,6 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
# api.pwnedpasswords.com.
|
||||
#FLUXER_BREACHED_PASSWORD_CHECK_ENABLED=false
|
||||
#FLUXER_BLOCKLIST_FEEDS_ENABLED=false
|
||||
#FLUXER_IPINFO_API_KEY=
|
||||
# A local path, or an s3:// URL read with the S3 credentials of this file.
|
||||
#FLUXER_GEOIP_DB_PATH=
|
||||
|
||||
@@ -310,6 +309,7 @@ FLUXER_EMAIL_ENABLED=false
|
||||
FLUXER_EMAIL_PROVIDER=none
|
||||
FLUXER_EMAIL_FROM_EMAIL=[email protected]
|
||||
FLUXER_EMAIL_FROM_NAME=Fluxer
|
||||
#[email protected]
|
||||
FLUXER_EMAIL_APP_BASE_URL=
|
||||
FLUXER_EMAIL_SMTP_HOST=
|
||||
FLUXER_EMAIL_SMTP_PORT=587
|
||||
@@ -449,6 +449,11 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_SEAWEEDFS_GOMEMLIMIT=1536MiB
|
||||
#FLUXER_SEAWEEDFS_TELEMETRY=false
|
||||
|
||||
# Volumes SeaweedFS creates at once when a bucket needs space. Each reserves 1 GB
|
||||
# of free disk from the start, and SeaweedFS's own default of 7 fills a small
|
||||
# disk before every bucket has one, so uploads fail with no free volumes left.
|
||||
#FLUXER_SEAWEEDFS_VOLUME_GROWTH=1
|
||||
|
||||
# Node sizes its heap from the container limit by default. Leave these unset
|
||||
# unless you need to pin it. A heap ceiling above the container limit gets the
|
||||
# container OOM-killed instead of reporting a heap error. The values below are
|
||||
|
||||
@@ -33,7 +33,6 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_APP_ORIGIN_ALIASES: ${FLUXER_APP_ORIGIN_ALIASES:-}
|
||||
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: ${FLUXER_BREACHED_PASSWORD_CHECK_ENABLED:-}
|
||||
FLUXER_BLOCKLIST_FEEDS_ENABLED: ${FLUXER_BLOCKLIST_FEEDS_ENABLED:-}
|
||||
FLUXER_IPINFO_API_KEY: ${FLUXER_IPINFO_API_KEY:-}
|
||||
FLUXER_GEOIP_DB_PATH: ${FLUXER_GEOIP_DB_PATH:-}
|
||||
|
||||
FLUXER_API_ENDPOINT: ${FLUXER_API_ENDPOINT:-}
|
||||
@@ -114,6 +113,7 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_EMAIL_PROVIDER: ${FLUXER_EMAIL_PROVIDER:-}
|
||||
FLUXER_EMAIL_FROM_EMAIL: ${FLUXER_EMAIL_FROM_EMAIL:-noreply@localhost}
|
||||
FLUXER_EMAIL_FROM_NAME: ${FLUXER_EMAIL_FROM_NAME:-}
|
||||
FLUXER_EMAIL_REPLY_TO_EMAIL: ${FLUXER_EMAIL_REPLY_TO_EMAIL:-}
|
||||
FLUXER_EMAIL_APP_BASE_URL: ${FLUXER_EMAIL_APP_BASE_URL:-}
|
||||
FLUXER_EMAIL_WEBHOOK_SECRET: ${FLUXER_EMAIL_WEBHOOK_SECRET:-}
|
||||
FLUXER_EMAIL_SMTP_HOST: ${FLUXER_EMAIL_SMTP_HOST:-}
|
||||
@@ -354,6 +354,7 @@ services:
|
||||
memory: ${FLUXER_SEAWEEDFS_MEMORY_LIMIT:-2gb}
|
||||
environment:
|
||||
GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
|
||||
WEED_MASTER_VOLUME_GROWTH_COPY_1: ${FLUXER_SEAWEEDFS_VOLUME_GROWTH:-1}
|
||||
command: ["server", "-s3", "-dir=/data", "-master.telemetry=${FLUXER_SEAWEEDFS_TELEMETRY:-false}"]
|
||||
volumes:
|
||||
- seaweedfs-data:/data
|
||||
|
||||
@@ -40,6 +40,7 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
|
||||
adapt_progenitor_throttled_errors(&mut spec);
|
||||
relax_guild_audit_log_schemas(&mut spec);
|
||||
relax_progenitor_schema_strictness(&mut spec);
|
||||
relax_integer_enums(&mut spec);
|
||||
|
||||
let mut settings = progenitor::GenerationSettings::new();
|
||||
settings.with_interface(progenitor::InterfaceStyle::Positional);
|
||||
@@ -174,6 +175,23 @@ fn relax_guild_audit_log_schemas(spec: &mut openapiv3::OpenAPI) {
|
||||
}
|
||||
}
|
||||
|
||||
const OPEN_INTEGER_ENUMS: &[&str] = &["ChannelType", "MessageType", "WebhookType"];
|
||||
|
||||
fn relax_integer_enums(spec: &mut openapiv3::OpenAPI) {
|
||||
let components = spec.components.as_mut().expect("missing API components");
|
||||
for name in OPEN_INTEGER_ENUMS {
|
||||
let Some(openapiv3::ReferenceOr::Item(schema)) = components.schemas.get_mut(*name) else {
|
||||
panic!("missing inline {name} schema");
|
||||
};
|
||||
let openapiv3::SchemaKind::Type(openapiv3::Type::Integer(integer)) =
|
||||
&mut schema.schema_kind
|
||||
else {
|
||||
panic!("{name} must be an integer schema");
|
||||
};
|
||||
integer.enumeration.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn object_schema_mut<'a>(
|
||||
components: &'a mut openapiv3::Components,
|
||||
name: &str,
|
||||
|
||||
+363
-251
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,6 @@ pub const BULK_ADD_GUILD_MEMBERS: &str = "bulk:add:guild_members";
|
||||
pub const BULK_DELETE_USERS: &str = "bulk:delete:users";
|
||||
pub const BULK_DELETE_USER_MESSAGES: &str = "bulk:delete:user_messages";
|
||||
pub const BULK_UPDATE_GUILD_FEATURES: &str = "bulk:update:guild_features";
|
||||
pub const BULK_UPDATE_SUSPICIOUS_ACTIVITY: &str = "bulk:update:suspicious_activity";
|
||||
pub const BULK_UPDATE_USER_FLAGS: &str = "bulk:update:user_flags";
|
||||
pub const CSAM_SUBMIT_NCMEC: &str = "csam:submit_ncmec";
|
||||
pub const DISCOVERY_REMOVE: &str = "discovery:remove";
|
||||
@@ -78,7 +77,6 @@ pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii";
|
||||
pub const SYSTEM_DM_SEND: &str = "system_dm:send";
|
||||
pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion";
|
||||
pub const USER_DELETE: &str = "user:delete";
|
||||
pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious";
|
||||
pub const USER_LIST_DM_CHANNELS: &str = "user:list:dm_channels";
|
||||
pub const USER_LIST_GUILDS: &str = "user:list:guilds";
|
||||
pub const USER_LIST_RELATIONSHIPS: &str = "user:list:relationships";
|
||||
@@ -95,9 +93,7 @@ pub const USER_UPDATE_DOB: &str = "user:update:dob";
|
||||
pub const USER_UPDATE_EMAIL: &str = "user:update:email";
|
||||
pub const USER_UPDATE_FLAGS: &str = "user:update:flags";
|
||||
pub const USER_UPDATE_MFA: &str = "user:update:mfa";
|
||||
pub const USER_UPDATE_PHONE: &str = "user:update:phone";
|
||||
pub const USER_UPDATE_PROFILE: &str = "user:update:profile";
|
||||
pub const USER_UPDATE_SUSPICIOUS_ACTIVITY: &str = "user:update:suspicious_activity";
|
||||
pub const USER_UPDATE_TRAITS: &str = "user:update:traits";
|
||||
pub const USER_UPDATE_USERNAME: &str = "user:update:username";
|
||||
pub const VOICE_REGION_CREATE: &str = "voice:region:create";
|
||||
@@ -151,7 +147,6 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
BULK_DELETE_USERS,
|
||||
BULK_DELETE_USER_MESSAGES,
|
||||
BULK_UPDATE_GUILD_FEATURES,
|
||||
BULK_UPDATE_SUSPICIOUS_ACTIVITY,
|
||||
BULK_UPDATE_USER_FLAGS,
|
||||
CSAM_SUBMIT_NCMEC,
|
||||
DISCOVERY_REMOVE,
|
||||
@@ -187,7 +182,6 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
SYSTEM_DM_SEND,
|
||||
USER_CANCEL_BULK_MESSAGE_DELETION,
|
||||
USER_DELETE,
|
||||
USER_DISABLE_SUSPICIOUS,
|
||||
USER_LIST_DM_CHANNELS,
|
||||
USER_LIST_GUILDS,
|
||||
USER_LIST_RELATIONSHIPS,
|
||||
@@ -204,9 +198,7 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
USER_UPDATE_EMAIL,
|
||||
USER_UPDATE_FLAGS,
|
||||
USER_UPDATE_MFA,
|
||||
USER_UPDATE_PHONE,
|
||||
USER_UPDATE_PROFILE,
|
||||
USER_UPDATE_SUSPICIOUS_ACTIVITY,
|
||||
USER_UPDATE_TRAITS,
|
||||
USER_UPDATE_USERNAME,
|
||||
VOICE_REGION_CREATE,
|
||||
|
||||
@@ -19,19 +19,18 @@ pub mod user_flag_bits {
|
||||
pub const SPAMMER: u64 = 1 << 6;
|
||||
pub const HIGH_GLOBAL_RATE_LIMIT: u64 = 1 << 33;
|
||||
pub const DELETED: u64 = 1 << 34;
|
||||
pub const DISABLED_SUSPICIOUS_ACTIVITY: u64 = 1 << 35;
|
||||
pub const SELF_DELETED: u64 = 1 << 36;
|
||||
pub const DISABLED: u64 = 1 << 38;
|
||||
pub const HAS_SESSION_STARTED: u64 = 1 << 39;
|
||||
pub const RATE_LIMIT_BYPASS: u64 = 1 << 47;
|
||||
pub const REPORT_BANNED: u64 = 1 << 48;
|
||||
pub const VERIFIED_NOT_UNDERAGE: u64 = 1 << 49;
|
||||
pub const ACCOUNT_LIMITED: u64 = 1 << 50;
|
||||
pub const HAS_DISMISSED_PREMIUM_ONBOARDING: u64 = 1 << 51;
|
||||
pub const APP_STORE_REVIEWER: u64 = 1 << 53;
|
||||
pub const STAFF_HIDDEN: u64 = 1 << 57;
|
||||
pub const AGE_VERIFIED_ADULT: u64 = 1 << 60;
|
||||
pub const FORCE_INBOUND_PHONE_VERIFICATION: u64 = 1 << 61;
|
||||
pub const NOT_SUSPICIOUS: u64 = 1 << 62;
|
||||
pub const LIMIT_EXEMPT: u64 = 1 << 62;
|
||||
}
|
||||
|
||||
pub const USER_FLAGS: &[U64Flag] = &[
|
||||
@@ -67,10 +66,6 @@ pub const USER_FLAGS: &[U64Flag] = &[
|
||||
name: "DELETED",
|
||||
value: user_flag_bits::DELETED,
|
||||
},
|
||||
U64Flag {
|
||||
name: "DISABLED_SUSPICIOUS_ACTIVITY",
|
||||
value: user_flag_bits::DISABLED_SUSPICIOUS_ACTIVITY,
|
||||
},
|
||||
U64Flag {
|
||||
name: "SELF_DELETED",
|
||||
value: user_flag_bits::SELF_DELETED,
|
||||
@@ -95,6 +90,10 @@ pub const USER_FLAGS: &[U64Flag] = &[
|
||||
name: "VERIFIED_NOT_UNDERAGE",
|
||||
value: user_flag_bits::VERIFIED_NOT_UNDERAGE,
|
||||
},
|
||||
U64Flag {
|
||||
name: "ACCOUNT_LIMITED",
|
||||
value: user_flag_bits::ACCOUNT_LIMITED,
|
||||
},
|
||||
U64Flag {
|
||||
name: "HAS_DISMISSED_PREMIUM_ONBOARDING",
|
||||
value: user_flag_bits::HAS_DISMISSED_PREMIUM_ONBOARDING,
|
||||
@@ -112,12 +111,8 @@ pub const USER_FLAGS: &[U64Flag] = &[
|
||||
value: user_flag_bits::AGE_VERIFIED_ADULT,
|
||||
},
|
||||
U64Flag {
|
||||
name: "FORCE_INBOUND_PHONE_VERIFICATION",
|
||||
value: user_flag_bits::FORCE_INBOUND_PHONE_VERIFICATION,
|
||||
},
|
||||
U64Flag {
|
||||
name: "NOT_SUSPICIOUS",
|
||||
value: user_flag_bits::NOT_SUSPICIOUS,
|
||||
name: "LIMIT_EXEMPT",
|
||||
value: user_flag_bits::LIMIT_EXEMPT,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -159,42 +154,3 @@ pub const PREMIUM_FLAGS: &[I32Flag] = &[
|
||||
value: 1 << 8,
|
||||
},
|
||||
];
|
||||
|
||||
pub const SUSPICIOUS_ACTIVITY_FLAGS: &[I32Flag] = &[
|
||||
I32Flag {
|
||||
name: "REQUIRE_VERIFIED_EMAIL",
|
||||
value: 1 << 0,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_REVERIFIED_EMAIL",
|
||||
value: 1 << 1,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_VERIFIED_PHONE",
|
||||
value: 1 << 2,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_REVERIFIED_PHONE",
|
||||
value: 1 << 3,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE",
|
||||
value: 1 << 4,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE",
|
||||
value: 1 << 5,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE",
|
||||
value: 1 << 6,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE",
|
||||
value: 1 << 7,
|
||||
},
|
||||
I32Flag {
|
||||
name: "REQUIRE_INBOUND_PHONE_VERIFICATION",
|
||||
value: 1 << 8,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::templates::components::tooltip::{Hint, HintLink};
|
||||
|
||||
pub fn limit_key_hint(key: &str) -> Option<Hint<'static>> {
|
||||
match key {
|
||||
"feature_guild_create" => Some(Hint {
|
||||
name: Some("Community Creation Access"),
|
||||
body: "Admins with the wildcard ACL can always create communities.",
|
||||
link: Some(HintLink::new(
|
||||
"/instance-config#community-creation",
|
||||
"Community creation policy",
|
||||
)),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ impl AdminApiClient {
|
||||
acls: &[String],
|
||||
) -> ApiResult<CreateAdminApiKeyResponse> {
|
||||
let body = generated_types::CreateAdminApiKeyRequest {
|
||||
acls: parse_acls(acls)?,
|
||||
acls: parse_acls(acls),
|
||||
expires_in_days: None,
|
||||
name: generated_types::CreateAdminApiKeyRequestName::try_from(name)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?,
|
||||
@@ -44,11 +44,8 @@ impl AdminApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_acls(acls: &[String]) -> ApiResult<Vec<generated_types::AdminAclType>> {
|
||||
pub(super) fn parse_acls(acls: &[String]) -> Vec<generated_types::AdminAclType> {
|
||||
acls.iter()
|
||||
.map(|acl| {
|
||||
generated_types::AdminAclType::try_from(acl.as_str())
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
})
|
||||
.filter_map(|acl| generated_types::AdminAclType::try_from(acl.as_str()).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ impl AdminApiClient {
|
||||
"email",
|
||||
generated_types::AdminBlocklistEntryCreateRequest::from(
|
||||
generated_types::BanEmailRequest {
|
||||
email: generated_types::EmailType::from(email.to_owned()),
|
||||
email: generated_types::EmailBlocklistEntryType::from(email.to_owned()),
|
||||
},
|
||||
),
|
||||
audit_log_reason,
|
||||
|
||||
@@ -22,22 +22,6 @@ impl AdminApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn bulk_update_suspicious_activity_flags(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
add_flags: &[String],
|
||||
remove_flags: &[String],
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<BulkJobResponse> {
|
||||
let body = generated_types::AdminBulkJobCreateRequest::UpdateSuspiciousActivityFlags {
|
||||
add_flags: add_flags.to_vec(),
|
||||
remove_flags: remove_flags.to_vec(),
|
||||
user_ids: snowflakes(user_ids),
|
||||
};
|
||||
self.post_typed_with_reason("/admin/bulk-jobs", &body, audit_log_reason)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn bulk_update_guild_features(
|
||||
&self,
|
||||
guild_ids: &[String],
|
||||
|
||||
@@ -85,7 +85,6 @@ mod tests {
|
||||
"email": "[email protected]",
|
||||
"email_verified": true,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": false,
|
||||
"date_of_birth": "2000-01-15",
|
||||
"locale": "en-US",
|
||||
"premium_type": 2,
|
||||
@@ -93,8 +92,6 @@ mod tests {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -8,13 +8,18 @@ use super::types::SendSystemDmResponse;
|
||||
impl AdminApiClient {
|
||||
pub async fn send_system_dm(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
user_ids: Option<&[String]>,
|
||||
content: &str,
|
||||
) -> ApiResult<SendSystemDmResponse> {
|
||||
let body = generated_types::SendSystemDmRequest {
|
||||
content: generated_types::SendSystemDmRequestContent::try_from(content)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?,
|
||||
user_ids: user_ids.iter().map(|id| snowflake(id)).collect(),
|
||||
user_ids: user_ids
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|id| snowflake(id))
|
||||
.collect(),
|
||||
all_users: user_ids.is_none().then_some(true),
|
||||
};
|
||||
let response = self
|
||||
.generated()
|
||||
|
||||
@@ -108,15 +108,9 @@ pub struct AdminUser {
|
||||
pub premium_grace_ends_at: Option<String>,
|
||||
pub premium_lifetime_sequence: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub suspicious_activity_flags: i32,
|
||||
#[serde(default)]
|
||||
pub phone_verification_deferred: bool,
|
||||
#[serde(default)]
|
||||
pub has_totp: bool,
|
||||
#[serde(default)]
|
||||
pub authenticator_types: Vec<i32>,
|
||||
#[serde(default)]
|
||||
pub has_verified_phone: bool,
|
||||
pub temp_banned_until: Option<String>,
|
||||
pub pending_deletion_at: Option<String>,
|
||||
pub pending_bulk_message_deletion_at: Option<String>,
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub domain_migration: DomainMigrationConfigResponse,
|
||||
#[serde(default)]
|
||||
pub plutonium_page: PlutoniumPageConfigResponse,
|
||||
#[serde(default)]
|
||||
pub captcha: CaptchaConfigResponse,
|
||||
#[serde(default)]
|
||||
pub experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
@@ -43,6 +45,8 @@ pub struct InstancePolicyResponse {
|
||||
pub direct_messages_locked: bool,
|
||||
#[serde(default)]
|
||||
pub premium_mode: PremiumMode,
|
||||
#[serde(default = "default_guild_create_access")]
|
||||
pub guild_create_access: bool,
|
||||
#[serde(default)]
|
||||
pub services: InstanceServicesOverrides,
|
||||
#[serde(default)]
|
||||
@@ -51,6 +55,10 @@ pub struct InstancePolicyResponse {
|
||||
pub services_available: InstanceServicesAvailable,
|
||||
}
|
||||
|
||||
fn default_guild_create_access() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for InstancePolicyResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -59,6 +67,7 @@ impl Default for InstancePolicyResponse {
|
||||
direct_messages_disabled: false,
|
||||
direct_messages_locked: false,
|
||||
premium_mode: PremiumMode::Everyone,
|
||||
guild_create_access: default_guild_create_access(),
|
||||
services: InstanceServicesOverrides::default(),
|
||||
services_resolved: InstanceServicesResolved::default(),
|
||||
services_available: InstanceServicesAvailable::default(),
|
||||
@@ -423,6 +432,7 @@ impl VoiceE2eeScope {
|
||||
|
||||
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
|
||||
pub const DOMAIN_MIGRATION_DEFAULT_SALT: &str = "domain-migration-v1";
|
||||
pub const PLUTONIUM_PAGE_DEFAULT_SALT: &str = "plutonium-page-v1";
|
||||
pub const CAPTCHA_COST_RANGE: std::ops::RangeInclusive<u32> = 1_000..=20_000;
|
||||
pub const CAPTCHA_MAX_COUNTER_RANGE: std::ops::RangeInclusive<u32> = 100..=20_000;
|
||||
|
||||
@@ -494,6 +504,52 @@ pub struct DomainMigrationConfigUpdateRequest {
|
||||
pub standalone_forwarding: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct PlutoniumPageConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub rollout_basis_points: u32,
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub included_guild_ids: Vec<String>,
|
||||
pub include_premium_users: bool,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for PlutoniumPageConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
rollout_basis_points: 0,
|
||||
rollout_salt: PLUTONIUM_PAGE_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
included_guild_ids: Vec::new(),
|
||||
include_premium_users: false,
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct PlutoniumPageConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rollout_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub included_guild_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_premium_users: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct CaptchaConfigResponse {
|
||||
@@ -640,6 +696,8 @@ pub struct InstanceConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domain_migration: Option<DomainMigrationConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub plutonium_page: Option<PlutoniumPageConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub captcha: Option<CaptchaConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
|
||||
@@ -656,6 +714,8 @@ pub struct InstancePolicyUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub direct_messages_disabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_create_access: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub premium_mode: Option<PremiumMode>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub services: Option<InstanceServicesUpdateRequest>,
|
||||
@@ -940,17 +1000,24 @@ mod tests {
|
||||
.expect("admin schema");
|
||||
let domain_migration = serde_json::from_value::<DomainMigrationConfigResponse>(json!({}))
|
||||
.expect("default domain migration config");
|
||||
let plutonium_page = serde_json::from_value::<PlutoniumPageConfigResponse>(json!({}))
|
||||
.expect("default plutonium page config");
|
||||
let captcha = serde_json::from_value::<CaptchaConfigResponse>(json!({}))
|
||||
.expect("default captcha config");
|
||||
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
|
||||
.expect("default delivery config");
|
||||
let domain_migration =
|
||||
serde_json::to_value(domain_migration).expect("serializable domain migration config");
|
||||
let plutonium_page =
|
||||
serde_json::to_value(plutonium_page).expect("serializable plutonium page config");
|
||||
let captcha = serde_json::to_value(captcha).expect("serializable captcha config");
|
||||
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
|
||||
let generated_domain_migration: generated_types::DomainMigrationConfigResponse =
|
||||
serde_json::from_value(domain_migration.clone())
|
||||
.expect("generated domain migration config contract");
|
||||
let generated_plutonium_page: generated_types::PlutoniumPageConfigResponse =
|
||||
serde_json::from_value(plutonium_page.clone())
|
||||
.expect("generated plutonium page config contract");
|
||||
let generated_captcha: generated_types::CaptchaConfigResponse =
|
||||
serde_json::from_value(captcha.clone()).expect("generated captcha config contract");
|
||||
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
|
||||
@@ -960,6 +1027,11 @@ mod tests {
|
||||
.expect("serializable generated domain migration config"),
|
||||
domain_migration
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_plutonium_page)
|
||||
.expect("serializable generated plutonium page config"),
|
||||
plutonium_page
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(generated_captcha).expect("serializable generated captcha config"),
|
||||
captcha
|
||||
@@ -971,6 +1043,7 @@ mod tests {
|
||||
);
|
||||
for (name, value) in [
|
||||
("DomainMigrationConfigResponse", domain_migration),
|
||||
("PlutoniumPageConfigResponse", plutonium_page),
|
||||
("CaptchaConfigResponse", captcha),
|
||||
("ExperimentDeliveryConfigResponse", delivery),
|
||||
] {
|
||||
@@ -1005,4 +1078,25 @@ mod tests {
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plutonium_page_update_preserves_empty_lists_and_omitted_fields() {
|
||||
let update = PlutoniumPageConfigUpdateRequest {
|
||||
included_user_ids: Some(Vec::new()),
|
||||
excluded_user_ids: Some(Vec::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let value = serde_json::to_value(update).expect("serializable update");
|
||||
serde_json::from_value::<generated_types::PlutoniumPageConfigUpdateRequest>(value.clone())
|
||||
.expect("generated update contract");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({"included_user_ids": [], "excluded_user_ids": []})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(PlutoniumPageConfigUpdateRequest::default())
|
||||
.expect("serializable update"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SendSystemDmResponse {
|
||||
pub recipient_count: i64,
|
||||
pub recipient_count: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -231,22 +231,9 @@ impl AdminApiClient {
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn update_suspicious_flags(&self, user_id: &str, flags: i32) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserSuspiciousActivityFlagsRequest {
|
||||
flags: generated_types::SuspiciousActivityFlags::from(flags),
|
||||
};
|
||||
let response = self
|
||||
.generated()
|
||||
.update_admin_user_suspicious_activity_flags(&snowflake(user_id), &body)
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn set_user_acls(&self, user_id: &str, acls: &[String]) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserAclsRequest {
|
||||
acls: super::admin_api_keys::parse_acls(acls)?,
|
||||
acls: super::admin_api_keys::parse_acls(acls),
|
||||
};
|
||||
let response = self
|
||||
.generated()
|
||||
@@ -296,21 +283,6 @@ impl AdminApiClient {
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn update_has_verified_phone(
|
||||
&self,
|
||||
user_id: &str,
|
||||
has_verified_phone: bool,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserPhoneVerificationRequest { has_verified_phone };
|
||||
let response = self
|
||||
.generated()
|
||||
.update_admin_user_phone_verification(&snowflake(user_id), &body)
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn clear_user_fields(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod acl;
|
||||
pub mod admin_flags;
|
||||
pub mod admin_hints;
|
||||
pub mod api;
|
||||
pub mod config;
|
||||
pub mod fonts;
|
||||
|
||||
@@ -171,7 +171,8 @@ pub(crate) async fn system_dms_post(
|
||||
let flash = if let Some(content) = content.as_deref()
|
||||
&& !user_ids.is_empty()
|
||||
{
|
||||
match client.send_system_dm(&user_ids, content).await {
|
||||
let recipients = (user_ids != ["*"]).then_some(user_ids.as_slice());
|
||||
match client.send_system_dm(recipients, content).await {
|
||||
Ok(_) => FlashData::success("System DM sent"),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: send system DM");
|
||||
@@ -218,19 +219,6 @@ pub(crate) async fn bulk_actions_post(
|
||||
.bulk_update_user_flags(&user_ids, &add, &remove, audit_log_reason.as_deref())
|
||||
.await
|
||||
}
|
||||
"bulk-update-suspicious-activity-flags" => {
|
||||
let user_ids = form.list_values_any(&["user_ids[]", "user_ids"]);
|
||||
let add = form.list_values_any(&["add_flags[]", "add_flags"]);
|
||||
let remove = form.list_values_any(&["remove_flags[]", "remove_flags"]);
|
||||
client
|
||||
.bulk_update_suspicious_activity_flags(
|
||||
&user_ids,
|
||||
&add,
|
||||
&remove,
|
||||
audit_log_reason.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
"bulk-update-guild-features" => {
|
||||
let guild_ids = form.list_values_any(&["guild_ids[]", "guild_ids"]);
|
||||
let mut add = form.list_values_any(&["add_features[]", "add_features"]);
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::{
|
||||
InstanceMediaUpdateRequest, InstancePolicyUpdateRequest,
|
||||
InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest,
|
||||
InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule,
|
||||
LimitRuleFilters, PremiumMode, PushRelayConfigUpdateRequest, RegistrationMode,
|
||||
SsoConfigUpdateRequest, VoiceE2eeScope,
|
||||
LimitRuleFilters, PlutoniumPageConfigUpdateRequest, PremiumMode,
|
||||
PushRelayConfigUpdateRequest, RegistrationMode, SsoConfigUpdateRequest, VoiceE2eeScope,
|
||||
},
|
||||
},
|
||||
config::AdminConfig,
|
||||
@@ -220,6 +220,10 @@ pub async fn instance_config_post(
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_plutonium_page" => match build_plutonium_page_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_captcha" => match build_captcha_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
@@ -609,6 +613,41 @@ fn build_domain_migration_update(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_plutonium_page_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
plutonium_page: Some(PlutoniumPageConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("plutonium_page_enabled")),
|
||||
rollout_basis_points: parse_form_number(
|
||||
form,
|
||||
"plutonium_page_rollout_basis_points",
|
||||
"Rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
rollout_salt: parse_experiment_rollout_salt(form, "plutonium_page_rollout_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("plutonium_page_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
included_guild_ids: Some(parse_experiment_user_ids(
|
||||
form.first("plutonium_page_included_guild_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included guild IDs",
|
||||
)?),
|
||||
include_premium_users: Some(form.bool_value("plutonium_page_include_premium_users")),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("plutonium_page_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_captcha_update(form: &MultiValueForm) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
captcha: Some(CaptchaConfigUpdateRequest {
|
||||
@@ -734,6 +773,9 @@ fn build_policy_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
|
||||
let direct_messages_disabled = form
|
||||
.first("policy_direct_messages_disabled")
|
||||
.map(|value| value == "true");
|
||||
let guild_create_access = form
|
||||
.first("policy_guild_create_access")
|
||||
.map(|value| value == "true");
|
||||
let premium_mode = match form.first("policy_premium_mode") {
|
||||
Some("mirror") => Some(PremiumMode::Mirror),
|
||||
Some("everyone") => Some(PremiumMode::Everyone),
|
||||
@@ -745,6 +787,7 @@ fn build_policy_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
|
||||
single_community_enabled: None,
|
||||
single_community_name: None,
|
||||
direct_messages_disabled,
|
||||
guild_create_access,
|
||||
premium_mode,
|
||||
services,
|
||||
}),
|
||||
@@ -875,10 +918,7 @@ fn build_single_community_update(enabled: bool) -> InstanceConfigUpdateRequest {
|
||||
InstanceConfigUpdateRequest {
|
||||
policy: Some(InstancePolicyUpdateRequest {
|
||||
single_community_enabled: Some(enabled),
|
||||
single_community_name: None,
|
||||
direct_messages_disabled: None,
|
||||
premium_mode: None,
|
||||
services: None,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -1443,6 +1483,72 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_plutonium_page_update_reads_the_rollout_fields() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"plutonium_page_enabled=true&plutonium_page_rollout_basis_points=%20500%20&plutonium_page_rollout_salt=%20plutonium-page-v2%20&plutonium_page_included_user_ids=1500000000000000001&plutonium_page_excluded_user_ids=1500000000000000002&plutonium_page_included_guild_ids=1500000000000000005%0A1500000000000000006%2C1500000000000000005&plutonium_page_include_premium_users=true",
|
||||
);
|
||||
let update = build_plutonium_page_update(&form)
|
||||
.expect("valid form")
|
||||
.plutonium_page
|
||||
.expect("plutonium page update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.rollout_basis_points, Some(500));
|
||||
assert_eq!(update.rollout_salt, Some("plutonium-page-v2".to_owned()));
|
||||
assert_eq!(update.include_premium_users, Some(true));
|
||||
assert_eq!(
|
||||
update.included_guild_ids,
|
||||
Some(vec![
|
||||
"1500000000000000005".to_owned(),
|
||||
"1500000000000000006".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec!["1500000000000000001".to_owned()])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec!["1500000000000000002".to_owned()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_plutonium_page_update_leaves_the_feature_inert_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_plutonium_page_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"plutonium_page": {
|
||||
"enabled": false,
|
||||
"included_user_ids": [],
|
||||
"included_guild_ids": [],
|
||||
"include_premium_users": false,
|
||||
"excluded_user_ids": [],
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_plutonium_page_update_rejects_invalid_rollout_fields() {
|
||||
for (form, message) in [
|
||||
(
|
||||
"plutonium_page_rollout_basis_points=10001",
|
||||
"Rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"plutonium_page_included_guild_ids=1500000000000000005%0Anot-a-guild",
|
||||
"Included guild IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
] {
|
||||
let form = MultiValueForm::parse(form.as_bytes());
|
||||
assert_eq!(
|
||||
build_plutonium_page_update(&form).expect_err("invalid field"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
|
||||
@@ -134,19 +134,6 @@ pub async fn dispatch(
|
||||
"Failed to update premium flags",
|
||||
)
|
||||
}
|
||||
"update_suspicious_flags" => {
|
||||
let Ok(submitted) =
|
||||
form.parse_list_values::<i32>(&["suspicious_flags[]", "suspicious_flags"])
|
||||
else {
|
||||
return DispatchOutcome::error("Invalid suspicious activity flag value");
|
||||
};
|
||||
let flags = submitted.into_iter().fold(0, |acc, flag| acc | flag);
|
||||
DispatchOutcome::from_result(
|
||||
client.update_suspicious_flags(user_id, flags).await,
|
||||
"Suspicious activity flags updated successfully",
|
||||
"Failed to update suspicious activity flags",
|
||||
)
|
||||
}
|
||||
"update_acls" => {
|
||||
let acls = form.list_values_any(&["acls[]", "acls"]);
|
||||
DispatchOutcome::from_result(
|
||||
@@ -178,14 +165,6 @@ pub async fn dispatch(
|
||||
"Email verified successfully",
|
||||
"Failed to verify email",
|
||||
),
|
||||
"update_has_verified_phone" => {
|
||||
let val = form.bool_value("has_verified_phone");
|
||||
DispatchOutcome::from_result(
|
||||
client.update_has_verified_phone(user_id, val).await,
|
||||
"Phone verification status updated successfully",
|
||||
"Failed to update phone verification status",
|
||||
)
|
||||
}
|
||||
"terminate_sessions" => DispatchOutcome::from_result(
|
||||
client.terminate_user_sessions(user_id).await,
|
||||
"User sessions terminated successfully",
|
||||
|
||||
@@ -238,3 +238,28 @@ input:disabled + .checkbox-custom {
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
:target {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
scroll-margin-top: 6rem;
|
||||
animation: target-pulse 700ms ease-in-out 3;
|
||||
}
|
||||
|
||||
@keyframes target-pulse {
|
||||
0%,
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: hsl(242 70% 55% / 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:target {
|
||||
background-color: hsl(242 70% 55% / 0.12);
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub mod resource_link;
|
||||
pub mod section_card;
|
||||
pub mod stack;
|
||||
pub mod table;
|
||||
pub mod tooltip;
|
||||
pub mod typography;
|
||||
pub mod user_display;
|
||||
pub mod user_profile_badges;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::icons::paperclip_icon;
|
||||
use maud::{Markup, html};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static HINT_TOGGLE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub struct HintLink<'a> {
|
||||
href: &'a str,
|
||||
label: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> HintLink<'a> {
|
||||
pub fn new(href: &'a str, label: &'a str) -> Self {
|
||||
debug_assert!(
|
||||
href.starts_with('/'),
|
||||
"hint link href must be admin-absolute: {href:?}"
|
||||
);
|
||||
debug_assert!(
|
||||
href.contains('#'),
|
||||
"hint link href should point at an anchor: {href:?}"
|
||||
);
|
||||
debug_assert!(!label.trim().is_empty(), "hint link needs a label");
|
||||
Self { href, label }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hint<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
pub body: &'a str,
|
||||
pub link: Option<HintLink<'a>>,
|
||||
}
|
||||
|
||||
pub fn info(base: &str, hint: &Hint<'_>) -> Markup {
|
||||
let aria_label = match hint.name {
|
||||
Some(name) => format!("About {name}"),
|
||||
None => "More information".to_owned(),
|
||||
};
|
||||
let toggle_id = format!(
|
||||
"hint-toggle-{}",
|
||||
HINT_TOGGLE_ID.fetch_add(1, Ordering::Relaxed)
|
||||
);
|
||||
html! {
|
||||
span class="group relative inline-flex items-center" {
|
||||
input type="checkbox" id=(toggle_id) class="peer sr-only";
|
||||
label for=(toggle_id) tabindex="0" aria-label=(aria_label)
|
||||
class="flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded-full \
|
||||
font-semibold text-brand-primary leading-none active:scale-97 \
|
||||
hover:text-brand-primary-dark" {
|
||||
"?"
|
||||
}
|
||||
label for=(toggle_id) aria-hidden="true"
|
||||
class="invisible fixed inset-0 z-20 cursor-default peer-checked:visible" {}
|
||||
div class="invisible absolute bottom-full left-2 z-30 w-64 pb-3 pl-2 opacity-0 \
|
||||
transition-[opacity,visibility] duration-200 ease-out motion-reduce:transition-none \
|
||||
group-hover:visible group-hover:opacity-100 \
|
||||
group-focus-within:visible group-focus-within:opacity-100 \
|
||||
peer-checked:visible peer-checked:opacity-100" {
|
||||
div class="rounded-lg border border-neutral-200 bg-white p-3 text-neutral-600 \
|
||||
text-xs shadow-lg" {
|
||||
@if let Some(name) = hint.name {
|
||||
p class="font-semibold text-neutral-900" { (name) }
|
||||
}
|
||||
p class=[hint.name.is_some().then_some("mt-1")] { (hint.body) }
|
||||
@if let Some(link) = &hint.link {
|
||||
a href={(base) (link.href)} hx-boost="false"
|
||||
class="mt-2 inline-flex items-center gap-1 text-blue-600 hover:underline" {
|
||||
(paperclip_icon(""))(link.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HintLink;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "anchor")]
|
||||
fn rejects_a_link_that_points_at_no_anchor() {
|
||||
let _ = HintLink::new("/instance-config", "Instance policy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "label")]
|
||||
fn rejects_a_link_with_no_label() {
|
||||
let _ = HintLink::new("/instance-config#community-creation", " ");
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ pub const NAV_SECTIONS: &[NavSection] = &[
|
||||
"bulk-actions",
|
||||
[
|
||||
acl::BULK_UPDATE_USER_FLAGS,
|
||||
acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY,
|
||||
acl::BULK_UPDATE_GUILD_FEATURES,
|
||||
acl::BULK_ADD_GUILD_MEMBERS,
|
||||
acl::BULK_DELETE_USERS,
|
||||
@@ -268,7 +267,6 @@ mod tests {
|
||||
.expect("bulk actions nav item");
|
||||
for required in [
|
||||
acl::BULK_UPDATE_USER_FLAGS,
|
||||
acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY,
|
||||
acl::BULK_UPDATE_GUILD_FEATURES,
|
||||
acl::BULK_ADD_GUILD_MEMBERS,
|
||||
acl::BULK_DELETE_USERS,
|
||||
|
||||
@@ -19,11 +19,7 @@ pub fn format_action(action: &str) -> String {
|
||||
|
||||
pub fn action_badge_variant(action: &str) -> BadgeVariant {
|
||||
match action {
|
||||
"temp_ban"
|
||||
| "disable_suspicious_activity"
|
||||
| "schedule_deletion"
|
||||
| "ban_ip"
|
||||
| "ban_email" => BadgeVariant::Danger,
|
||||
"temp_ban" | "schedule_deletion" | "ban_ip" | "ban_email" => BadgeVariant::Danger,
|
||||
"unban" | "cancel_deletion" | "unban_ip" | "unban_email" => BadgeVariant::Success,
|
||||
"update_flags" | "update_features" | "set_acls" | "update_settings" | "annotate_ban" => {
|
||||
BadgeVariant::Info
|
||||
@@ -361,4 +357,12 @@ mod tests {
|
||||
assert!(!markup.contains("<a "));
|
||||
assert!(markup.contains("Email domain"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retired_action_names_still_render() {
|
||||
let mut retired = entry("user", "1500000000000000002");
|
||||
retired.action = "update_retired_toggle".to_string();
|
||||
let markup = audit_log_table_body("/admin", &[retired]).into_string();
|
||||
assert!(markup.contains("Update retired toggle"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
BanConfig {
|
||||
title: "Email Bans",
|
||||
route: "/email-bans",
|
||||
input_label: "Email Address",
|
||||
input_label: "Email Address or Domain",
|
||||
input_name: "email",
|
||||
input_type: "email",
|
||||
placeholder: "[email protected]",
|
||||
input_type: "text",
|
||||
placeholder: "[email protected] or @example.com",
|
||||
entity_name: "Email",
|
||||
active_page: "email-bans",
|
||||
show_bulk_tools: false,
|
||||
|
||||
@@ -80,10 +80,6 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
|
||||
name: "DELETED",
|
||||
value: 1 << 34,
|
||||
},
|
||||
UserFlag {
|
||||
name: "DISABLED_SUSPICIOUS_ACTIVITY",
|
||||
value: 1 << 35,
|
||||
},
|
||||
UserFlag {
|
||||
name: "SELF_DELETED",
|
||||
value: 1 << 36,
|
||||
@@ -108,6 +104,10 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
|
||||
name: "VERIFIED_NOT_UNDERAGE",
|
||||
value: 1 << 49,
|
||||
},
|
||||
UserFlag {
|
||||
name: "ACCOUNT_LIMITED",
|
||||
value: 1 << 50,
|
||||
},
|
||||
UserFlag {
|
||||
name: "HAS_DISMISSED_PREMIUM_ONBOARDING",
|
||||
value: 1 << 51,
|
||||
@@ -125,26 +125,11 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
|
||||
value: 1 << 60,
|
||||
},
|
||||
UserFlag {
|
||||
name: "FORCE_INBOUND_PHONE_VERIFICATION",
|
||||
value: 1 << 61,
|
||||
},
|
||||
UserFlag {
|
||||
name: "NOT_SUSPICIOUS",
|
||||
name: "LIMIT_EXEMPT",
|
||||
value: 1 << 62,
|
||||
},
|
||||
];
|
||||
|
||||
const SUSPICIOUS_ACTIVITY_FLAGS: &[&str] = &[
|
||||
"REQUIRE_VERIFIED_EMAIL",
|
||||
"REQUIRE_REVERIFIED_EMAIL",
|
||||
"REQUIRE_VERIFIED_PHONE",
|
||||
"REQUIRE_REVERIFIED_PHONE",
|
||||
"REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE",
|
||||
"REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE",
|
||||
"REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE",
|
||||
"REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE",
|
||||
"REQUIRE_INBOUND_PHONE_VERIFICATION",
|
||||
];
|
||||
const GUILD_FEATURES: &[&str] = &[
|
||||
"ANIMATED_ICON",
|
||||
"ANIMATED_BANNER",
|
||||
@@ -179,6 +164,7 @@ const GUILD_FEATURES: &[&str] = &[
|
||||
"VISIONARY",
|
||||
"LARGE_GUILD_OVERRIDE",
|
||||
"VERY_LARGE_GUILD",
|
||||
"ANNOUNCEMENT_CHANNELS_DISABLED",
|
||||
];
|
||||
|
||||
const DEPRECATED_GUILD_FEATURES: &[&str] = &["CLONE_EMOJI_DISABLED", "CLONE_STICKER_DISABLED"];
|
||||
@@ -205,9 +191,6 @@ pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &
|
||||
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_USER_FLAGS) {
|
||||
(bulk_update_user_flags_section(base, csrf_token))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY) {
|
||||
(bulk_update_suspicious_activity_section(base, csrf_token))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_GUILD_FEATURES) {
|
||||
(bulk_update_guild_features_section(base, csrf_token))
|
||||
}
|
||||
@@ -225,16 +208,6 @@ pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &
|
||||
admin_layout(config, auth, "Bulk Actions", "bulk-actions", None, content)
|
||||
}
|
||||
|
||||
fn flag_checkbox_grid(prefix: &str, flags: &[&str]) -> Markup {
|
||||
html! {
|
||||
div class="grid grid-cols-1 gap-3 sm:grid-cols-2" {
|
||||
@for flag in flags {
|
||||
(checkbox(prefix, flag, flag, false, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guild_feature_checkbox_grid(prefix: &str, include_deprecated: bool) -> Markup {
|
||||
html! {
|
||||
div class="grid grid-cols-1 gap-3 sm:grid-cols-2" {
|
||||
@@ -287,36 +260,6 @@ fn bulk_update_user_flags_section(base: &str, csrf_token: &str) -> Markup {
|
||||
)
|
||||
}
|
||||
|
||||
fn bulk_update_suspicious_activity_section(base: &str, csrf_token: &str) -> Markup {
|
||||
section_card_simple(
|
||||
"Bulk Update Suspicious Activity Flags",
|
||||
html! {
|
||||
form method="post" action={(base) "/bulk-actions?action=bulk-update-suspicious-activity-flags"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(textarea_input("user_ids", "User IDs (one per line)", "123456789\n987654321", "", 5, true))
|
||||
div {
|
||||
p class="font-semibold text-neutral-500 text-xs uppercase tracking-wide mb-2" {
|
||||
"Flags to Add"
|
||||
}
|
||||
(flag_checkbox_grid("add_flags[]", SUSPICIOUS_ACTIVITY_FLAGS))
|
||||
}
|
||||
div {
|
||||
p class="font-semibold text-neutral-500 text-xs uppercase tracking-wide mb-2" {
|
||||
"Flags to Remove"
|
||||
}
|
||||
(flag_checkbox_grid("remove_flags[]", SUSPICIOUS_ACTIVITY_FLAGS))
|
||||
}
|
||||
(text_input("audit_log_reason", "Audit Log Reason (optional)", "", "Reason for this bulk operation"))
|
||||
(form_actions(html! {
|
||||
(submit_button("Update Suspicious Activity Flags"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn bulk_update_guild_features_section(base: &str, csrf_token: &str) -> Markup {
|
||||
section_card_simple(
|
||||
"Bulk Update Guild Features",
|
||||
|
||||
@@ -25,6 +25,10 @@ pub(crate) fn stat_card(label: &str, value: &str) -> Markup {
|
||||
|
||||
pub(crate) fn node_stats_section(data: &serde_json::Value, expanded: bool, base: &str) -> Markup {
|
||||
let sessions = data.get("sessions").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let reconnects: u64 = data
|
||||
.get("session_resumes_total")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
let guilds = data.get("guilds").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let presences = data.get("presences").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let calls = data.get("calls").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@@ -64,6 +68,7 @@ pub(crate) fn node_stats_section(data: &serde_json::Value, expanded: bool, base:
|
||||
div class="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-3 lg:grid-cols-6" {
|
||||
(stat_card("Nodes", &node_count.to_string()))
|
||||
(stat_card("Sessions", &sessions.to_string()))
|
||||
(stat_card("Reconnects", &reconnects.to_string()))
|
||||
(stat_card("Guilds", &guilds.to_string()))
|
||||
(stat_card("Presences", &presences.to_string()))
|
||||
(stat_card("Calls", &calls.to_string()))
|
||||
@@ -83,6 +88,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
|
||||
tr {
|
||||
th class="px-6 py-3 text-left text-neutral-600 text-xs uppercase" { "Node" }
|
||||
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Sessions" }
|
||||
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Session Resumes" }
|
||||
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Guilds" }
|
||||
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Presences" }
|
||||
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Calls" }
|
||||
@@ -95,6 +101,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
|
||||
@let label = format_node_id(node_id, i);
|
||||
@let status = node.get("status").and_then(|v| v.as_str()).unwrap_or("-");
|
||||
@let ns = node.get("sessions").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@let nsr = node.get("session_resumes_total").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@let ng = node.get("guilds").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@let np = node.get("presences").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@let nc = node.get("calls").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@@ -105,6 +112,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
|
||||
div class="text-neutral-500 text-xs" { (status) }
|
||||
}
|
||||
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (ns) }
|
||||
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (nsr) }
|
||||
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (ng) }
|
||||
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (np) }
|
||||
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (nc) }
|
||||
|
||||
@@ -45,6 +45,7 @@ const GUILD_FEATURES: &[&str] = &[
|
||||
"VISIONARY",
|
||||
"LARGE_GUILD_OVERRIDE",
|
||||
"VERY_LARGE_GUILD",
|
||||
"ANNOUNCEMENT_CHANNELS_DISABLED",
|
||||
];
|
||||
|
||||
const HOSTED_ONLY: &[&str] = &["VISIONARY", "VIP_VOICE"];
|
||||
|
||||
@@ -32,6 +32,7 @@ fn channel_type_label(channel_type: i32) -> &'static str {
|
||||
0 => "Text",
|
||||
2 => "Voice",
|
||||
4 => "Category",
|
||||
5 => "Announcement",
|
||||
13 => "Link",
|
||||
_ => "Unknown",
|
||||
}
|
||||
|
||||
@@ -52,13 +52,12 @@ pub fn settings_tab(
|
||||
"guild-verification-level",
|
||||
"verification_level",
|
||||
"Verification Level",
|
||||
guild.verification_level.unwrap_or(0),
|
||||
guild.verification_level.unwrap_or(0).min(3),
|
||||
&[
|
||||
(0, "None"),
|
||||
(1, "Low (verified email)"),
|
||||
(2, "Medium (5+ minutes)"),
|
||||
(3, "High (10+ minutes)"),
|
||||
(4, "Very High (verified phone)"),
|
||||
],
|
||||
))
|
||||
(select_field(
|
||||
@@ -225,12 +224,11 @@ fn select_field(
|
||||
}
|
||||
|
||||
fn settings_tab_readonly(guild: &GuildDetailInfo) -> Markup {
|
||||
let verification_label = match guild.verification_level.unwrap_or(0) {
|
||||
let verification_label = match guild.verification_level.unwrap_or(0).min(3) {
|
||||
0 => "None",
|
||||
1 => "Low (verified email)",
|
||||
2 => "Medium (5+ minutes)",
|
||||
3 => "High (10+ minutes)",
|
||||
4 => "Very High (verified phone)",
|
||||
_ => "Unknown",
|
||||
};
|
||||
let mfa_label = match guild.mfa_level.unwrap_or(0) {
|
||||
|
||||
@@ -7,8 +7,9 @@ use crate::{
|
||||
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
|
||||
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
||||
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
||||
LimitConfigResponse, PendingRegistrationResponse, PushRelayConfigResponse,
|
||||
RegistrationUrlResponse, SsoConfigResponse,
|
||||
LimitConfigResponse, PLUTONIUM_PAGE_DEFAULT_SALT, PendingRegistrationResponse,
|
||||
PlutoniumPageConfigResponse, PushRelayConfigResponse, RegistrationUrlResponse,
|
||||
SsoConfigResponse,
|
||||
},
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
@@ -181,6 +182,7 @@ pub fn instance_config_page(
|
||||
html! {
|
||||
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
||||
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
|
||||
(plutonium_page_section(base, csrf_token, &instance_config.plutonium_page))
|
||||
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
||||
@if let Some(limit_config) = limit_config {
|
||||
(limit_config_section(base, limit_config))
|
||||
@@ -245,6 +247,7 @@ fn policy_config_section(
|
||||
(single_community_form(base, csrf_token, policy))
|
||||
(direct_messages_form(base, csrf_token, policy))
|
||||
(premium_mode_form(base, csrf_token, policy, premium_name))
|
||||
(community_creation_form(base, csrf_token, policy))
|
||||
(services_form(base, csrf_token, policy))
|
||||
}
|
||||
},
|
||||
@@ -364,6 +367,37 @@ fn premium_mode_form(
|
||||
}
|
||||
}
|
||||
|
||||
fn community_creation_form(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
policy: &InstancePolicyResponse,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div id="community-creation" class="space-y-4 border-t border-neutral-200 pt-6" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Community creation" }
|
||||
form method="post" action={(base) "/instance-config?action=update_policy"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(select_input("policy_guild_create_access", "Who can create communities", &[
|
||||
("true", "Everyone"),
|
||||
("false", "Restricted"),
|
||||
], if policy.guild_create_access { "true" } else { "false" }))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"When restricted, only admins with the wildcard ACL and users matched by a "
|
||||
a href={(base) "/limit-config"} class="text-blue-600 hover:underline" {
|
||||
"limit rule"
|
||||
}
|
||||
" that grants Community Creation Access can create communities."
|
||||
}
|
||||
(form_actions(html! {
|
||||
(submit_button("Save community creation policy"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn service_select(name: &str, label: &str, override_value: Option<bool>, resolved: bool) -> Markup {
|
||||
let selected = match override_value {
|
||||
None => "inherit",
|
||||
@@ -1175,6 +1209,147 @@ fn domain_migration_section(
|
||||
)
|
||||
}
|
||||
|
||||
fn plutonium_page_section(
|
||||
base: &str,
|
||||
csrf_token: &str,
|
||||
plutonium_page: &PlutoniumPageConfigResponse,
|
||||
) -> Markup {
|
||||
let status = if plutonium_page.enabled {
|
||||
("Live", BadgeVariant::Success)
|
||||
} else {
|
||||
("Inert", BadgeVariant::Default)
|
||||
};
|
||||
let included_user_ids = plutonium_page.included_user_ids.join("\n");
|
||||
let excluded_user_ids = plutonium_page.excluded_user_ids.join("\n");
|
||||
section_card_with_description(
|
||||
"Plutonium page",
|
||||
"Replaces the Plutonium settings tab with a full Plutonium page, makes app pages linkable \
|
||||
in chat, and uses a minimal gift purchase modal.",
|
||||
html! {
|
||||
form method="post" action={(base) "/instance-config?action=update_plutonium_page"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-6" {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
||||
(badge(status.0, status.1))
|
||||
span class="text-xs text-neutral-500" {
|
||||
"Config version " (plutonium_page.config_version)
|
||||
}
|
||||
}
|
||||
(checkbox(
|
||||
"plutonium_page_enabled",
|
||||
"true",
|
||||
"Serve the Plutonium page to the selected users",
|
||||
plutonium_page.enabled,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Off is the safe state and the kill switch. With this unchecked every \
|
||||
client keeps the Plutonium settings tab, so the rollout and targeting \
|
||||
fields below have no effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"plutonium_page_rollout_basis_points",
|
||||
"Rollout (basis points)",
|
||||
&plutonium_page.rollout_basis_points.to_string(),
|
||||
Some(0), Some(10000), "1",
|
||||
Some("Share of users bucketed into the Plutonium page, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
|
||||
))
|
||||
div class="flex flex-col gap-2" {
|
||||
(text_input(
|
||||
"plutonium_page_rollout_salt",
|
||||
"Rollout Salt",
|
||||
&plutonium_page.rollout_salt,
|
||||
PLUTONIUM_PAGE_DEFAULT_SALT,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Seeds the bucketing hash. Changing it reshuffles which users fall \
|
||||
inside the percentage above. Leave it alone to keep the current \
|
||||
cohort stable."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"plutonium_page_included_user_ids",
|
||||
"Always-on User IDs",
|
||||
"1500000000000000001\n1500000000000000002",
|
||||
&included_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
plutonium_page.included_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"One snowflake per line, or comma separated. These users are targeted \
|
||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
||||
IDs are ignored."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(checkbox(
|
||||
"plutonium_page_include_premium_users",
|
||||
"true",
|
||||
"Include premium users",
|
||||
plutonium_page.include_premium_users,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Includes every account with active premium perks, regardless of the \
|
||||
percentage above. The never-on list still wins."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"plutonium_page_included_guild_ids",
|
||||
"Always-on Guild IDs",
|
||||
"1500000000000000005\n1500000000000000006",
|
||||
&plutonium_page.included_guild_ids.join("\n"),
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
plutonium_page.included_guild_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format, with guild IDs. Every member of a listed guild is \
|
||||
included regardless of the percentage above, unless the user is \
|
||||
in the never-on list."
|
||||
}
|
||||
}
|
||||
div class="flex flex-col gap-2" {
|
||||
(textarea_input(
|
||||
"plutonium_page_excluded_user_ids",
|
||||
"Never-on User IDs",
|
||||
"1500000000000000003\n1500000000000000004",
|
||||
&excluded_user_ids,
|
||||
4,
|
||||
false,
|
||||
))
|
||||
(entry_count_hint(
|
||||
plutonium_page.excluded_user_ids.len(),
|
||||
EXPERIMENT_MAX_TARGETED_USERS,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Same format. Exclusion wins over both the always-on list and the \
|
||||
percentage."
|
||||
}
|
||||
}
|
||||
|
||||
(form_actions(html! {
|
||||
(submit_button("Save Plutonium Page Configuration"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn estimate_low_end_solve_seconds(cost: u32, max_counter: u32) -> f64 {
|
||||
0.75 * f64::from(cost) * f64::from(max_counter) / 1_050_000.0
|
||||
}
|
||||
@@ -1228,7 +1403,7 @@ fn captcha_section(base: &str, csrf_token: &str, captcha: &CaptchaConfigResponse
|
||||
))
|
||||
p class="text-sm text-neutral-700" {
|
||||
(format!(
|
||||
"Average solve: about {estimate:.1} s on a low-end Android phone, \
|
||||
"Average solve: about {estimate:.1} s on a low-end Android device, \
|
||||
well under a second in desktop browsers."
|
||||
))
|
||||
}
|
||||
@@ -1896,6 +2071,34 @@ mod tests {
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plutonium_page_section_shows_the_rollout_and_list_counts() {
|
||||
let plutonium_page = PlutoniumPageConfigResponse {
|
||||
enabled: true,
|
||||
config_version: 3,
|
||||
rollout_basis_points: 250,
|
||||
included_user_ids: vec!["1500000000000000001".to_owned()],
|
||||
excluded_user_ids: vec![
|
||||
"1500000000000000002".to_owned(),
|
||||
"1500000000000000003".to_owned(),
|
||||
],
|
||||
..PlutoniumPageConfigResponse::default()
|
||||
};
|
||||
let markup = plutonium_page_section("/admin", "csrf", &plutonium_page).into_string();
|
||||
assert!(markup.contains("Plutonium page"));
|
||||
assert!(markup.contains("action=update_plutonium_page"));
|
||||
assert!(markup.contains("name=\"plutonium_page_enabled\""));
|
||||
assert!(markup.contains("name=\"plutonium_page_rollout_basis_points\""));
|
||||
assert!(markup.contains("value=\"250\""));
|
||||
assert!(markup.contains("name=\"plutonium_page_include_premium_users\""));
|
||||
assert!(markup.contains("name=\"plutonium_page_included_guild_ids\""));
|
||||
assert!(markup.contains("Config version 3"));
|
||||
assert!(markup.contains("1 of 1000 stored"));
|
||||
assert!(markup.contains("2 of 1000 stored"));
|
||||
assert!(!markup.contains("anonymous_rollout_basis_points"));
|
||||
assert!(!markup.contains("standalone_forwarding"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_relay_section_shows_the_consent_toggle() {
|
||||
let accepted = PushRelayConfigResponse {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::{
|
||||
acl::{self, INSTANCE_LIMIT_CONFIG_UPDATE},
|
||||
admin_hints,
|
||||
api::types::{LimitConfigResponse, LimitKeyMetadata, LimitRule},
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
@@ -9,6 +10,7 @@ use crate::{
|
||||
components::{
|
||||
form::{FORM_INPUT_CLASS, csrf_input, danger_button, form_actions, submit_button},
|
||||
page_container::{card_with_header, page_header},
|
||||
tooltip,
|
||||
},
|
||||
layout::admin_layout,
|
||||
},
|
||||
@@ -208,7 +210,7 @@ fn rule_editor(
|
||||
@for category in CATEGORY_ORDER {
|
||||
@let keys = keys_for_category(response, category);
|
||||
@if !keys.is_empty() {
|
||||
(category_section(response, rule, category, &keys, can_update))
|
||||
(category_section(&config.base_path, response, rule, category, &keys, can_update))
|
||||
}
|
||||
}
|
||||
@if can_update {
|
||||
@@ -274,6 +276,7 @@ fn keys_for_category(response: &LimitConfigResponse, category: &str) -> Vec<Stri
|
||||
}
|
||||
|
||||
fn category_section(
|
||||
base: &str,
|
||||
response: &LimitConfigResponse,
|
||||
rule: &LimitRule,
|
||||
category: &str,
|
||||
@@ -292,7 +295,7 @@ fn category_section(
|
||||
div class="space-y-4" {
|
||||
@for key in keys {
|
||||
@if let Some(metadata) = response.metadata.get(key) {
|
||||
(limit_field(response, rule, key, metadata, can_update))
|
||||
(limit_field(base, response, rule, key, metadata, can_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +304,7 @@ fn category_section(
|
||||
}
|
||||
|
||||
fn limit_field(
|
||||
base: &str,
|
||||
response: &LimitConfigResponse,
|
||||
rule: &LimitRule,
|
||||
key: &str,
|
||||
@@ -319,9 +323,10 @@ fn limit_field(
|
||||
.as_ref()
|
||||
.is_some_and(|fields| fields.iter().any(|field| field == key));
|
||||
if metadata.is_toggle {
|
||||
toggle_field(key, metadata, current_value, modified, can_update)
|
||||
toggle_field(base, key, metadata, current_value, modified, can_update)
|
||||
} else {
|
||||
numeric_field(
|
||||
base,
|
||||
key,
|
||||
metadata,
|
||||
current_value,
|
||||
@@ -333,6 +338,7 @@ fn limit_field(
|
||||
}
|
||||
|
||||
fn toggle_field(
|
||||
base: &str,
|
||||
key: &str,
|
||||
metadata: &LimitKeyMetadata,
|
||||
current_value: Option<u64>,
|
||||
@@ -343,7 +349,7 @@ fn toggle_field(
|
||||
html! {
|
||||
div class={(field_class(modified, false))} {
|
||||
div class="flex-1 space-y-1" {
|
||||
(field_label_row(key, metadata, modified))
|
||||
(field_label_row(base, key, metadata, modified))
|
||||
p class="text-xs text-neutral-500" { (metadata.description) }
|
||||
}
|
||||
div class="shrink-0" {
|
||||
@@ -369,6 +375,7 @@ fn toggle_field(
|
||||
}
|
||||
|
||||
fn numeric_field(
|
||||
base: &str,
|
||||
key: &str,
|
||||
metadata: &LimitKeyMetadata,
|
||||
current_value: Option<u64>,
|
||||
@@ -385,7 +392,7 @@ fn numeric_field(
|
||||
html! {
|
||||
div class={(field_class(modified, true))} {
|
||||
div class="flex flex-wrap items-center justify-between gap-2" {
|
||||
(field_label_row(key, metadata, modified))
|
||||
(field_label_row(base, key, metadata, modified))
|
||||
}
|
||||
p class="text-xs text-neutral-500" {
|
||||
(metadata.description)
|
||||
@@ -417,10 +424,13 @@ fn numeric_field(
|
||||
}
|
||||
}
|
||||
|
||||
fn field_label_row(key: &str, metadata: &LimitKeyMetadata, modified: bool) -> Markup {
|
||||
fn field_label_row(base: &str, key: &str, metadata: &LimitKeyMetadata, modified: bool) -> Markup {
|
||||
html! {
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
label for=(key) class="font-medium text-neutral-900 text-sm" { (metadata.label) }
|
||||
@if let Some(hint) = admin_hints::limit_key_hint(key) {
|
||||
(tooltip::info(base, &hint))
|
||||
}
|
||||
span class=(scope_class(&metadata.scope)) { (scope_label(&metadata.scope)) }
|
||||
@if modified {
|
||||
span class="rounded bg-neutral-100 px-1.5 py-0.5 text-neutral-700 text-xs" { "Modified" }
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn system_dm_page(
|
||||
(form_field_group(
|
||||
"Recipient user IDs", "system-dm-user-ids",
|
||||
true, None,
|
||||
Some("One per line. Snowflake IDs only."),
|
||||
Some("One per line. Snowflake IDs only, or a single * to send to every user."),
|
||||
html! {
|
||||
textarea id="system-dm-user-ids" name="user_ids"
|
||||
required rows="10"
|
||||
|
||||
@@ -154,10 +154,6 @@ fn session_entry(base: &str, s: &UserSession, is_tombstone: bool) -> Markup {
|
||||
}
|
||||
|
||||
fn quick_actions_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
|
||||
let phone_action = format!(
|
||||
"{base}/users/{}?action=update_has_verified_phone&tab=account",
|
||||
user.id
|
||||
);
|
||||
html! {
|
||||
(card_with_header("Quick Actions", html! {
|
||||
div class="flex flex-wrap gap-3" {
|
||||
@@ -165,20 +161,6 @@ fn quick_actions_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup
|
||||
(action_form(base, &user.id, "verify_email", "account", None,
|
||||
"Verify Email", csrf_token))
|
||||
}
|
||||
form method="post"
|
||||
action=(&phone_action)
|
||||
hx-post=(&phone_action)
|
||||
hx-target="#flash-container"
|
||||
hx-swap="none"
|
||||
hx-push-url="false" {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="has_verified_phone"
|
||||
value=@if user.has_verified_phone { "false" } @else { "true" };
|
||||
button type="submit" class=(BTN_CLS) {
|
||||
@if user.has_verified_phone { "Clear Phone Verified" }
|
||||
@else { "Mark Phone Verified" }
|
||||
}
|
||||
}
|
||||
(action_form(base, &user.id, "send_password_reset", "account", None,
|
||||
"Send Password Reset", csrf_token))
|
||||
}
|
||||
|
||||
@@ -139,13 +139,6 @@ fn render_overview_tab(
|
||||
}
|
||||
}))
|
||||
}
|
||||
(detail_row("Phone", html! {
|
||||
@if user.has_verified_phone {
|
||||
span class="text-green-700" { "Verified" }
|
||||
} @else {
|
||||
span class="text-neutral-400" { "Not verified" }
|
||||
}
|
||||
}))
|
||||
@if acl::has_permission(admin_acls, acl::USER_VIEW_DOB) {
|
||||
(detail_row("Date of Birth", html! {
|
||||
(user.date_of_birth.as_deref().unwrap_or("Not set"))
|
||||
@@ -270,8 +263,6 @@ fn flags_card(
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let can_update_flags = acl::has_permission(admin_acls, acl::USER_UPDATE_FLAGS);
|
||||
let can_update_suspicious =
|
||||
acl::has_permission(admin_acls, acl::USER_UPDATE_SUSPICIOUS_ACTIVITY);
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
(u64_flag_form(
|
||||
@@ -298,23 +289,6 @@ fn flags_card(
|
||||
can_update_flags,
|
||||
Some(acl::USER_UPDATE_FLAGS),
|
||||
))
|
||||
(i32_flag_form(
|
||||
config,
|
||||
&user.id,
|
||||
"Suspicious Activity Flags",
|
||||
"update_suspicious_flags",
|
||||
"suspicious_flags[]",
|
||||
user.suspicious_activity_flags,
|
||||
admin_flags::SUSPICIOUS_ACTIVITY_FLAGS,
|
||||
csrf_token,
|
||||
can_update_suspicious,
|
||||
Some(acl::USER_UPDATE_SUSPICIOUS_ACTIVITY),
|
||||
))
|
||||
@if user.phone_verification_deferred {
|
||||
p class="text-sm text-amber-700 dark:text-amber-400" {
|
||||
"Phone verification is deferred: the requirement above is stored but not enforced."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ fn deserialize_admin_users_me_response() {
|
||||
"email": "[email protected]",
|
||||
"email_verified": true,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": true,
|
||||
"date_of_birth": "2003-02-25",
|
||||
"locale": "en-US",
|
||||
"premium_type": 2,
|
||||
@@ -31,7 +30,6 @@ fn deserialize_admin_users_me_response() {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": 1,
|
||||
"suspicious_activity_flags": 0,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
@@ -64,9 +62,7 @@ fn deserialize_admin_users_me_response() {
|
||||
assert_eq!(user.acls, vec!["super_admin"]);
|
||||
assert_eq!(user.traits, vec!["beta_tester"]);
|
||||
assert_eq!(user.premium_type, Some(2));
|
||||
assert_eq!(user.suspicious_activity_flags, 0);
|
||||
assert!(user.has_totp);
|
||||
assert!(user.has_verified_phone);
|
||||
assert_eq!(user.last_active_ip.as_deref(), Some("1.2.3.4"));
|
||||
}
|
||||
|
||||
@@ -79,10 +75,10 @@ fn deserialize_flags_as_string_and_number() {
|
||||
"premium_flags": 0, "avatar": null, "banner": null, "bio": null,
|
||||
"pronouns": null, "accent_color": null, "email": null,
|
||||
"email_verified": false, "email_bounced": false,
|
||||
"has_verified_phone": false, "date_of_birth": null, "locale": null,
|
||||
"date_of_birth": null, "locale": null,
|
||||
"premium_type": null, "premium_since": null, "premium_until": null,
|
||||
"premium_grace_ends_at": null, "premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0, "temp_banned_until": null,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null, "pending_bulk_message_deletion_at": null,
|
||||
"deletion_reason_code": null, "deletion_public_reason": null, "deletion_audit_log_reason": null,
|
||||
"deletion_scheduled_by": null, "deletion_scheduled_at": null,
|
||||
@@ -111,10 +107,10 @@ fn deserialize_discriminator_int_and_string() {
|
||||
"bot": true, "system": false, "flags": "0", "premium_flags": 0,
|
||||
"avatar": null, "banner": null, "bio": null, "pronouns": null,
|
||||
"accent_color": null, "email": null, "email_verified": false,
|
||||
"email_bounced": false, "has_verified_phone": false, "date_of_birth": null,
|
||||
"email_bounced": false, "date_of_birth": null,
|
||||
"locale": null, "premium_type": null, "premium_since": null,
|
||||
"premium_until": null, "premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null, "suspicious_activity_flags": 0,
|
||||
"premium_lifetime_sequence": null,
|
||||
"temp_banned_until": null, "pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null, "deletion_reason_code": null,
|
||||
"deletion_public_reason": null, "deletion_audit_log_reason": null,
|
||||
@@ -161,7 +157,6 @@ fn deserialize_search_users_response() {
|
||||
"email": null,
|
||||
"email_verified": false,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": false,
|
||||
"date_of_birth": null,
|
||||
"locale": null,
|
||||
"premium_type": null,
|
||||
@@ -169,7 +164,6 @@ fn deserialize_search_users_response() {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
@@ -422,6 +416,17 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"anonymous_rollout_basis_points": 100,
|
||||
"standalone_forwarding": true
|
||||
},
|
||||
"plutonium_page": {
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
"rollout_basis_points": 500,
|
||||
"rollout_salt": "plutonium-page-v1",
|
||||
"included_user_ids": ["1500000000000000001"],
|
||||
"excluded_user_ids": ["1500000000000000002"],
|
||||
"included_guild_ids": ["1500000000000000005"],
|
||||
"include_premium_users": true,
|
||||
"future_plutonium_page_knob": true
|
||||
},
|
||||
"captcha": {
|
||||
"enabled": true,
|
||||
"cost": 5000,
|
||||
@@ -461,6 +466,7 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"single_community_guild_id": null,
|
||||
"direct_messages_disabled": false,
|
||||
"direct_messages_locked": false,
|
||||
"guild_create_access": false,
|
||||
"premium_mode": "mirror",
|
||||
"services": {
|
||||
"gif_enabled": true,
|
||||
@@ -577,6 +583,14 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
assert_eq!(resp.domain_migration.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.domain_migration.anonymous_rollout_basis_points, 100);
|
||||
assert!(resp.domain_migration.standalone_forwarding);
|
||||
assert!(resp.plutonium_page.enabled);
|
||||
assert_eq!(resp.plutonium_page.config_version, 3);
|
||||
assert_eq!(resp.plutonium_page.rollout_basis_points, 500);
|
||||
assert_eq!(*resp.plutonium_page.rollout_salt, "plutonium-page-v1");
|
||||
assert_eq!(resp.plutonium_page.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.plutonium_page.excluded_user_ids.len(), 1);
|
||||
assert_eq!(resp.plutonium_page.included_guild_ids.len(), 1);
|
||||
assert!(resp.plutonium_page.include_premium_users);
|
||||
assert!(resp.push_relay.relay_consent_accepted);
|
||||
assert!(resp.captcha.enabled);
|
||||
assert_eq!(resp.captcha.max_counter, 1000);
|
||||
@@ -861,10 +875,10 @@ fn deserialize_user_mutation_response() {
|
||||
"flags": "1", "premium_flags": 0, "avatar": null, "banner": null,
|
||||
"bio": null, "pronouns": null, "accent_color": null, "email": null,
|
||||
"email_verified": false, "email_bounced": false,
|
||||
"has_verified_phone": false, "date_of_birth": null, "locale": null,
|
||||
"date_of_birth": null, "locale": null,
|
||||
"premium_type": null, "premium_since": null, "premium_until": null,
|
||||
"premium_grace_ends_at": null, "premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0, "temp_banned_until": null,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null, "pending_bulk_message_deletion_at": null,
|
||||
"deletion_reason_code": null, "deletion_public_reason": null, "deletion_audit_log_reason": null,
|
||||
"deletion_scheduled_by": null, "deletion_scheduled_at": null,
|
||||
|
||||
@@ -263,11 +263,8 @@ fn admin_user() -> Value {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"has_totp": false,
|
||||
"authenticator_types": [],
|
||||
"has_verified_phone": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -363,7 +363,9 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
|
||||
assert!(body.contains("__fluxerAdminActionForms"), "{body}");
|
||||
assert!(!body.contains(&native_confirm), "{body}");
|
||||
assert!(
|
||||
body.contains(r#"hx-post="/users/1500000000000000001?action=update_has_verified_phone&tab=account""#),
|
||||
body.contains(
|
||||
r#"hx-post="/users/1500000000000000001?action=send_password_reset&tab=account""#
|
||||
),
|
||||
"{body}"
|
||||
);
|
||||
assert!(body.contains(r##"hx-target="#flash-container""##), "{body}");
|
||||
@@ -374,7 +376,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
|
||||
.unwrap_or_else(|| panic!("account page did not set csrf_token cookie\n{body}"));
|
||||
let (status, response_headers, response_body) = post_form_with_headers(
|
||||
&app,
|
||||
"/users/1500000000000000001?action=update_has_verified_phone&tab=account",
|
||||
"/users/1500000000000000001?action=send_password_reset&tab=account",
|
||||
&[
|
||||
("HX-Request", "true"),
|
||||
("HX-Target", "flash-container"),
|
||||
@@ -383,7 +385,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
|
||||
&format!("{}; csrf_token={}", app.session_cookie, csrf_token),
|
||||
),
|
||||
],
|
||||
&format!("_csrf={csrf_token}&has_verified_phone=true"),
|
||||
&format!("_csrf={csrf_token}"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "{response_body}");
|
||||
@@ -399,7 +401,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
|
||||
.unwrap_or_else(|| panic!("missing toast header\n{response_body}"));
|
||||
assert!(toast.contains("success"), "{toast}");
|
||||
assert!(
|
||||
toast.contains("Phone verification status updated successfully"),
|
||||
toast.contains("Password reset sent successfully"),
|
||||
"{toast}"
|
||||
);
|
||||
}
|
||||
@@ -467,6 +469,7 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() {
|
||||
"/instance-config?action=update_gateway_rollout",
|
||||
"/instance-config?action=update_sso",
|
||||
"/instance-config?action=update_domain_migration",
|
||||
"/instance-config?action=update_plutonium_page",
|
||||
"/instance-config?action=update_experiment_delivery",
|
||||
][..],
|
||||
),
|
||||
@@ -838,8 +841,8 @@ async fn mock_api(method: Method, uri: Uri) -> Response {
|
||||
(Method::GET, "/admin/users/1500000000000000001") => {
|
||||
json_response(json!({ "users": [searched_user()] }))
|
||||
}
|
||||
(Method::PUT, "/admin/users/1500000000000000001/phone-verification") => {
|
||||
json_response(json!({ "user": searched_user() }))
|
||||
(Method::POST, "/admin/users/1500000000000000001/password-reset") => {
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
(Method::GET, "/admin/guilds") => {
|
||||
json_response(json!({ "guilds": [searched_guild()], "total": 1 }))
|
||||
@@ -946,11 +949,8 @@ fn user(id: &str, username: &str) -> Value {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"has_totp": false,
|
||||
"authenticator_types": [],
|
||||
"has_verified_phone": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
@@ -1193,6 +1193,14 @@ fn instance_config() -> Value {
|
||||
"anonymous_rollout_basis_points": 0,
|
||||
"standalone_forwarding": false
|
||||
},
|
||||
"plutonium_page": {
|
||||
"enabled": false,
|
||||
"config_version": 0,
|
||||
"rollout_basis_points": 0,
|
||||
"rollout_salt": "plutonium-page-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
},
|
||||
"experiment_delivery": {
|
||||
"poll_interval_seconds": 300,
|
||||
"poll_jitter_percent": 15
|
||||
|
||||
@@ -322,11 +322,8 @@ fn admin_user() -> Value {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"has_totp": false,
|
||||
"authenticator_types": [],
|
||||
"has_verified_phone": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
@@ -354,12 +351,10 @@ fn test_config(api_endpoint: String) -> AdminConfig {
|
||||
static_cdn_endpoint: "https://static.example.test".to_owned(),
|
||||
admin_endpoint: "https://admin.example.test".to_owned(),
|
||||
web_app_endpoint: "https://app.example.test".to_owned(),
|
||||
kv_url: String::new(),
|
||||
oauth_client_id: "admin-client".to_owned(),
|
||||
oauth_client_secret: "admin-secret".to_owned(),
|
||||
oauth_redirect_uri: "https://admin.example.test/callback".to_owned(),
|
||||
build_version: "test".to_owned(),
|
||||
release_channel: "test".to_owned(),
|
||||
self_hosted: false,
|
||||
proxy: ProxyConfig {
|
||||
trust_client_ip_header: false,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"email": "[email protected]",
|
||||
"email_verified": true,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": true,
|
||||
"date_of_birth": "2000-01-01",
|
||||
"locale": "en-US",
|
||||
"premium_type": null,
|
||||
@@ -24,8 +23,6 @@
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"email": "[email protected]",
|
||||
"email_verified": true,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": false,
|
||||
"date_of_birth": null,
|
||||
"locale": "en-US",
|
||||
"premium_type": null,
|
||||
@@ -25,8 +24,6 @@
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"email": "[email protected]",
|
||||
"email_verified": true,
|
||||
"email_bounced": false,
|
||||
"has_verified_phone": false,
|
||||
"date_of_birth": null,
|
||||
"locale": "en-US",
|
||||
"premium_type": null,
|
||||
@@ -25,8 +24,6 @@
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -294,11 +294,8 @@ fn admin_user() -> Value {
|
||||
"premium_until": null,
|
||||
"premium_grace_ends_at": null,
|
||||
"premium_lifetime_sequence": null,
|
||||
"suspicious_activity_flags": 0,
|
||||
"phone_verification_deferred": false,
|
||||
"has_totp": false,
|
||||
"authenticator_types": [],
|
||||
"has_verified_phone": false,
|
||||
"temp_banned_until": null,
|
||||
"pending_deletion_at": null,
|
||||
"pending_bulk_message_deletion_at": null,
|
||||
|
||||
@@ -131,7 +131,6 @@ export const ELASTICSEARCH_INDEX_DEFINITIONS: Record<FluxerSearchIndexName, Elas
|
||||
id: keyword(),
|
||||
username: textWithKeyword(),
|
||||
email: textWithKeyword(),
|
||||
phone: textWithKeyword(),
|
||||
discriminator: integer(),
|
||||
isBot: bool(),
|
||||
isSystem: bool(),
|
||||
@@ -139,7 +138,6 @@ export const ELASTICSEARCH_INDEX_DEFINITIONS: Record<FluxerSearchIndexName, Elas
|
||||
premiumType: integer(),
|
||||
emailVerified: bool(),
|
||||
emailBounced: bool(),
|
||||
suspiciousActivityFlags: integer(),
|
||||
acls: keyword(),
|
||||
createdAt: long(),
|
||||
lastActiveAt: long(),
|
||||
|
||||
@@ -36,9 +36,6 @@ function buildUserFilters(filters: UserSearchFilters): Array<ElasticsearchFilter
|
||||
if (filters.hasAcl && filters.hasAcl.length > 0) {
|
||||
clauses.push(...esAndTerms('acls', filters.hasAcl));
|
||||
}
|
||||
if (filters.minSuspiciousActivityFlags !== undefined) {
|
||||
clauses.push(esRangeFilter('suspiciousActivityFlags', {gte: filters.minSuspiciousActivityFlags}));
|
||||
}
|
||||
if (filters.createdAtGreaterThanOrEqual !== undefined) {
|
||||
clauses.push(esRangeFilter('createdAt', {gte: filters.createdAtGreaterThanOrEqual}));
|
||||
}
|
||||
@@ -65,7 +62,7 @@ export class ElasticsearchUserAdapter extends ElasticsearchIndexAdapter<UserSear
|
||||
super({
|
||||
client: options.client,
|
||||
index: ELASTICSEARCH_INDEX_DEFINITIONS.users,
|
||||
searchableFields: ['username', 'email', 'phone', 'id'],
|
||||
searchableFields: ['username', 'email', 'id'],
|
||||
buildFilters: buildUserFilters,
|
||||
buildSort: buildUserSort,
|
||||
lock: options.lock,
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface EmailConfig {
|
||||
enabled: boolean;
|
||||
fromEmail: string;
|
||||
fromName: string;
|
||||
replyTo?: string | null;
|
||||
appBaseUrl: string;
|
||||
marketingBaseUrl: string;
|
||||
}
|
||||
@@ -14,6 +15,7 @@ export interface EmailMessage {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
replyTo?: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EmailI18nService} from '@pkgs/email/src/EmailI18nService';
|
||||
import type {EmailConfig, EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import {EmailService} from '@pkgs/email/src/EmailService';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const CONFIG: EmailConfig = {
|
||||
enabled: true,
|
||||
fromEmail: '[email protected]',
|
||||
fromName: 'Fluxer',
|
||||
appBaseUrl: 'https://example.com',
|
||||
marketingBaseUrl: 'https://example.com',
|
||||
};
|
||||
|
||||
async function sendWith(config: EmailConfig): Promise<EmailMessage> {
|
||||
const sent: Array<EmailMessage> = [];
|
||||
const provider: IEmailProvider = {
|
||||
sendEmail: async (message) => {
|
||||
sent.push(message);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const service = new EmailService(config, new EmailI18nService(), provider);
|
||||
await expect(service.sendRegistrationApprovedEmail('[email protected]', 'testuser', 'en-US')).resolves.toBe(true);
|
||||
expect(sent).toHaveLength(1);
|
||||
return sent[0];
|
||||
}
|
||||
|
||||
describe('EmailService reply-to', () => {
|
||||
it('sets the configured reply-to address on every message', async () => {
|
||||
const message = await sendWith({...CONFIG, replyTo: '[email protected]'});
|
||||
expect(message.replyTo).toBe('[email protected]');
|
||||
expect(message.from).toEqual({email: '[email protected]', name: 'Fluxer'});
|
||||
});
|
||||
|
||||
it.each([undefined, null, ''])('omits the reply-to address when it is %j', async (replyTo) => {
|
||||
const message = await sendWith({...CONFIG, replyTo});
|
||||
expect(message).not.toHaveProperty('replyTo');
|
||||
});
|
||||
});
|
||||
@@ -79,19 +79,6 @@ export class EmailService implements IEmailService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendAccountDisabledForSuspiciousActivityEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'account_disabled_suspicious', locale, {
|
||||
username,
|
||||
reason: optionalReason(reason),
|
||||
forgotUrl: `${this.config.appBaseUrl}/forgot`,
|
||||
});
|
||||
}
|
||||
|
||||
async sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
@@ -388,6 +375,7 @@ export class EmailService implements IEmailService {
|
||||
return this.provider.sendEmail({
|
||||
to: email,
|
||||
from: {email: this.config.fromEmail, name: this.config.fromName},
|
||||
...(this.config.replyTo ? {replyTo: this.config.replyTo} : {}),
|
||||
subject,
|
||||
text: body,
|
||||
});
|
||||
|
||||
@@ -16,12 +16,6 @@ export interface IEmailService {
|
||||
location: string,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountDisabledForSuspiciousActivityEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const {sendMail} = vi.hoisted(() => ({sendMail: vi.fn()}));
|
||||
|
||||
vi.mock('nodemailer', () => ({
|
||||
default: {createTransport: () => ({sendMail, verify: vi.fn()})},
|
||||
}));
|
||||
|
||||
const MESSAGE = {
|
||||
to: '[email protected]',
|
||||
from: {email: '[email protected]', name: 'Fluxer'},
|
||||
subject: 'Subject',
|
||||
text: 'Body',
|
||||
};
|
||||
|
||||
function createProvider(): SmtpEmailProvider {
|
||||
return new SmtpEmailProvider({host: 'smtp.example.com', port: 587, username: 'user', password: 'pass'});
|
||||
}
|
||||
|
||||
describe('SmtpEmailProvider', () => {
|
||||
beforeEach(() => {
|
||||
sendMail.mockReset();
|
||||
sendMail.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('passes the reply-to address to nodemailer', async () => {
|
||||
await expect(createProvider().sendEmail({...MESSAGE, replyTo: '[email protected]'})).resolves.toBe(true);
|
||||
expect(sendMail).toHaveBeenCalledWith({
|
||||
to: '[email protected]',
|
||||
from: 'Fluxer <[email protected]>',
|
||||
replyTo: '[email protected]',
|
||||
subject: 'Subject',
|
||||
text: 'Body',
|
||||
});
|
||||
});
|
||||
|
||||
it('omits the reply-to address when the message has none', async () => {
|
||||
await expect(createProvider().sendEmail(MESSAGE)).resolves.toBe(true);
|
||||
expect(sendMail.mock.calls[0][0]).not.toHaveProperty('replyTo');
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,7 @@ export class SmtpEmailProvider implements IEmailProvider {
|
||||
await this.transporter.sendMail({
|
||||
to: message.to,
|
||||
from: `${message.from.name} <${message.from.email}>`,
|
||||
...(message.replyTo ? {replyTo: message.replyTo} : {}),
|
||||
subject: message.subject,
|
||||
text: message.text,
|
||||
});
|
||||
|
||||
@@ -69,16 +69,6 @@ export class TestEmailService implements ITestEmailService {
|
||||
return this.record(email, 'ip_authorization', {token: authorizationToken, ip: ipAddress, location});
|
||||
}
|
||||
|
||||
async sendAccountDisabledForSuspiciousActivityEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
this.logger.info(`Account disabled email sent to ${email} for user ${username}, reason: ${reason ?? 'none'}`);
|
||||
return this.record(email, 'account_disabled_suspicious', {reason: reason ?? ''});
|
||||
}
|
||||
|
||||
async sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
|
||||
@@ -19,7 +19,6 @@ const FIXTURE: {[K in EmailTemplateKey]: EmailTemplateVariables[K]} = {
|
||||
account_deletion_cancelled: {username: 'testuser'},
|
||||
account_deletion_scheduled_inactivity: {username: 'testuser', reason: 'Inactive', deletionDate: DATE},
|
||||
account_deletion_scheduled_requested: {username: 'testuser', reason: 'Requested', deletionDate: DATE},
|
||||
account_disabled_suspicious: {username: 'testuser', reason: 'Spam', forgotUrl: 'https://example.com/forgot'},
|
||||
account_scheduled_deletion: {
|
||||
username: 'testuser',
|
||||
reason: 'Spam',
|
||||
|
||||
@@ -15,10 +15,6 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
subject: 'Your {product_name} account deletion is scheduled',
|
||||
body: "Hello {username},\n\nAs you requested, your {product_name} account is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}{reason, select, null {} other {\n\nReason: {reason}}}\n\nYour account is locked until then. If you didn't request this, or you want to keep your account, contact {safety_email} from this email address before that date.\n\n– {product_name} Team",
|
||||
},
|
||||
account_disabled_suspicious: {
|
||||
subject: 'Your {product_name} account has been temporarily disabled',
|
||||
body: "Hello {username},\n\nWe temporarily disabled your {product_name} account because we detected suspicious activity.\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nTo regain access to your account, you'll need to reset your password:\n\n{forgotUrl}\n\nAfter you reset your password, you'll be able to log in again.\n\nIf you believe this was done in error, please contact our support team.\n\n– {product_name} Safety Team",
|
||||
},
|
||||
account_scheduled_deletion: {
|
||||
subject: 'Your {product_name} account will be permanently deleted',
|
||||
body: 'Hello {username},\n\nYour {product_name} account has been scheduled for permanent deletion due to violations of our Terms of Service or Community Guidelines.\n\nScheduled deletion: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nThis is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\nPlease review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\nAppeals process:\nIf you believe this enforcement decision was incorrect or unjustified, you have 60 days to submit an appeal. Email {appeals_email} from this email address.\n\nIn your appeal:\n- Clearly explain why you believe the enforcement decision was incorrect or unjustified\n- Provide any relevant evidence or context\n\nA member of the {product_name} Safety Team will review your appeal and may pause the pending deletion until a final decision has been reached.\n\n– {product_name} Safety Team',
|
||||
@@ -49,7 +45,7 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
},
|
||||
email_change_revert: {
|
||||
subject: 'Your {product_name} email was changed',
|
||||
body: "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, remove linked phone numbers, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team",
|
||||
body: "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team",
|
||||
},
|
||||
email_verification: {
|
||||
subject: 'Verify your {product_name} email address',
|
||||
|
||||
@@ -4,7 +4,6 @@ export type EmailTemplateKey =
|
||||
| 'account_deletion_cancelled'
|
||||
| 'account_deletion_scheduled_inactivity'
|
||||
| 'account_deletion_scheduled_requested'
|
||||
| 'account_disabled_suspicious'
|
||||
| 'account_scheduled_deletion'
|
||||
| 'account_temp_banned'
|
||||
| 'donation_confirmation'
|
||||
|
||||
@@ -14,11 +14,6 @@ export interface EmailTemplateVariables {
|
||||
reason: string | null;
|
||||
deletionDate: Date;
|
||||
};
|
||||
account_disabled_suspicious: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
forgotUrl: string;
|
||||
};
|
||||
account_scheduled_deletion: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
|
||||
@@ -15,10 +15,6 @@ const EMAIL_I18N_AR_MESSAGES = defineEmailI18nLocaleMessages({
|
||||
"subject": "تمت جدولة حذف حسابك في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nبناءً على طلبك، تمت جدولة حسابك في {product_name} للحذف الدائم في:\n\n{deletionDate, date, full} الساعة {deletionDate, time, short}{reason, select, null {} other {\n\nالسبب: {reason}}}\n\nسيظل حسابك مقفلًا حتى ذلك الحين. إذا لم تطلب هذا، أو كنت ترغب في الاحتفاظ بحسابك، فاتصل بـ{safety_email} من عنوان البريد الإلكتروني هذا قبل ذلك التاريخ.\n\n– فريق {product_name}"
|
||||
},
|
||||
"account_disabled_suspicious": {
|
||||
"subject": "تم تعطيل حسابك في {product_name} مؤقتًا",
|
||||
"body": "مرحبًا {username}،\n\nلقد قمنا بتعطيل حسابك في {product_name} مؤقتًا لأننا اكتشفنا نشاطًا مشبوهًا.\n\n{reason, select,\n null {}\n other {السبب: {reason}}\n}\n\nلاستعادة الوصول إلى حسابك، ستحتاج إلى إعادة تعيين كلمة المرور الخاصة بك:\n\n{forgotUrl}\n\nبعد إعادة تعيين كلمة المرور الخاصة بك، ستتمكن من تسجيل الدخول مرة أخرى.\n\nإذا كنت تعتقد أن هذا حدث عن طريق الخطأ، يرجى الاتصال بفريق الدعم لدينا.\n\n– فريق أمان {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "سيتم حذف حسابك في {product_name} نهائيًا",
|
||||
"body": "مرحبًا {username}،\n\nتمت جدولة حسابك في {product_name} للحذف الدائم بسبب انتهاكات لشروط الخدمة أو إرشادات المجتمع الخاصة بنا.\n\nالحذف المجدول: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {السبب: {reason}}\n}\n\nهذا إجراء إنفاذ جاد. سيتم حذف بيانات حسابك نهائيًا في التاريخ المحدد.\n\nيرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n- إرشادات المجتمع: {guidelinesUrl}\n\nعملية الاستئناف:\nإذا كنت تعتقد أن قرار الإنفاذ هذا كان غير صحيح أو غير مبرر، لديك 60 يومًا لتقديم استئناف. أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا.\n\nفي استئنافك:\n- اشرح بوضوح سبب اعتقادك أن قرار الإنفاذ كان غير صحيح أو غير مبرر\n- قدم أي دليل أو سياق ذي صلة\n\nسيقوم عضو من فريق أمان {product_name} بمراجعة استئنافك وقد يوقف الحذف المعلق مؤقتًا إلى حين التوصل إلى قرار نهائي.\n\n– فريق أمان {product_name}"
|
||||
@@ -49,7 +45,7 @@ const EMAIL_I18N_AR_MESSAGES = defineEmailI18nLocaleMessages({
|
||||
},
|
||||
"email_change_revert": {
|
||||
"subject": "تم تغيير بريدك الإلكتروني في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nتم تغيير عنوان البريد الإلكتروني لحسابك في {product_name} إلى {newEmail}.\n\nإذا قمت بهذا التغيير، فلا داعي لاتخاذ أي إجراء. إذا لم تقم بذلك، يمكنك التراجع عن التغيير وتأمين حسابك باستخدام هذا الرابط:\n\n{revertUrl}\n\nسيؤدي هذا إلى استعادة بريدك الإلكتروني السابق، وتسجيل خروجك من جميع الأجهزة، وإزالة أرقام الهواتف المرتبطة، وتعطيل المصادقة متعددة العوامل، وسيُطلب منك تعيين كلمة مرور جديدة.\n\n– فريق أمان {product_name}"
|
||||
"body": "مرحبًا {username}،\n\nتم تغيير عنوان البريد الإلكتروني لحسابك في {product_name} إلى {newEmail}.\n\nإذا قمت بهذا التغيير، فلا داعي لاتخاذ أي إجراء. إذا لم تقم بذلك، يمكنك التراجع عن التغيير وتأمين حسابك باستخدام هذا الرابط:\n\n{revertUrl}\n\nسيؤدي هذا إلى استعادة بريدك الإلكتروني السابق، وتسجيل خروجك من جميع الأجهزة، وتعطيل المصادقة متعددة العوامل، وسيُطلب منك تعيين كلمة مرور جديدة.\n\n– فريق أمان {product_name}"
|
||||
},
|
||||
"email_verification": {
|
||||
"subject": "تحقق من بريدك الإلكتروني في {product_name}",
|
||||
|
||||
@@ -15,10 +15,6 @@ const EMAIL_I18N_BG_MESSAGES = defineEmailI18nLocaleMessages({
|
||||
"subject": "Изтриването на акаунта ти във {product_name} е насрочено",
|
||||
"body": "Здравей, {username},\n\nКакто поиска, акаунтът ти във {product_name} е насрочен за постоянно изтриване на:\n\n{deletionDate, date, full} в {deletionDate, time, short}{reason, select, null {} other {\n\nПричина: {reason}}}\n\nДотогава акаунтът ти е заключен. Ако не си поискал това или искаш да запазиш акаунта си, свържи се с {safety_email} от този имейл адрес преди тази дата.\n\n– Екип на {product_name}"
|
||||
},
|
||||
"account_disabled_suspicious": {
|
||||
"subject": "Акаунтът ти във {product_name} е временно деактивиран",
|
||||
"body": "Здравей, {username},\n\nВременно деактивирахме акаунта ти във {product_name}, защото открихме подозрителна активност.\n\n{reason, select,\n null {}\n other {Причина: {reason}}\n}\n\nЗа да възстановиш достъпа до акаунта си, трябва да нулираш паролата си:\n\n{forgotUrl}\n\nСлед като нулираш паролата си, ще можеш да влезеш отново.\n\nАко смяташ, че това е грешка, свържи се с нашия екип за поддръжка.\n\n– Екип за безопасност на {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Акаунтът ти във {product_name} ще бъде изтрит за постоянно",
|
||||
"body": "Здравей, {username},\n\nАкаунтът ти във {product_name} е насрочен за постоянно изтриване поради нарушения на нашите Общи условия или Насоки на общността.\n\nНасрочено изтриване: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Причина: {reason}}\n}\n\nТова е сериозна мярка. Данните на акаунта ти ще бъдат изтрити за постоянно на насрочената дата.\n\nПрегледай:\n- Общи условия: {termsUrl}\n- Насоки на общността: {guidelinesUrl}\n\nПроцес на обжалване:\nАко смяташ, че това решение е неправилно или необосновано, имаш 60 дни да подадеш обжалване. Изпрати имейл на {appeals_email} от този имейл адрес.\n\nВ обжалването си:\n- Обясни ясно защо смяташ, че решението е неправилно или необосновано\n- Приложи всички релевантни доказателства или контекст\n\nЧлен на екипа за безопасност на {product_name} ще прегледа обжалването ти и може да спре предстоящото изтриване, докато не бъде взето окончателно решение.\n\n– Екип за безопасност на {product_name}"
|
||||
@@ -49,7 +45,7 @@ const EMAIL_I18N_BG_MESSAGES = defineEmailI18nLocaleMessages({
|
||||
},
|
||||
"email_change_revert": {
|
||||
"subject": "Имейлът ти във {product_name} беше променен",
|
||||
"body": "Здравей, {username},\n\nИмейл адресът на акаунта ти във {product_name} беше променен на {newEmail}.\n\nАко ти си направил тази промяна, не е необходимо да предприемаш никакви действия. Ако не си, можеш да отмениш промяната и да защитиш акаунта си, като използваш този линк:\n\n{revertUrl}\n\nТова ще възстанови предишния ти имейл, ще прекрати всички активни сесии, ще премахне свързаните телефонни номера, ще деактивира MFA и ще изиска да зададеш нова парола.\n\n– Екип за безопасност на {product_name}"
|
||||
"body": "Здравей, {username},\n\nИмейл адресът на акаунта ти във {product_name} беше променен на {newEmail}.\n\nАко ти си направил тази промяна, не е необходимо да предприемаш никакви действия. Ако не си, можеш да отмениш промяната и да защитиш акаунта си, като използваш този линк:\n\n{revertUrl}\n\nТова ще възстанови предишния ти имейл, ще прекрати всички активни сесии, ще деактивира MFA и ще изиска да зададеш нова парола.\n\n– Екип за безопасност на {product_name}"
|
||||
},
|
||||
"email_verification": {
|
||||
"subject": "Потвърди имейл адреса си във {product_name}",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user