mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c748c8af4e | ||
|
|
ba59a13149 | ||
|
|
3f4160b138 | ||
|
|
5f4295e399 | ||
|
|
4e730832c7 | ||
|
|
d7c00d4556 | ||
|
|
154b65afe5 | ||
|
|
fcc2a3f64b | ||
|
|
80456861ac | ||
|
|
0c4f016ba2 | ||
|
|
cc5545c333 | ||
|
|
6e28092cdc | ||
|
|
8b6910d505 | ||
|
|
d87e31efaf | ||
|
|
6618a6baf4 | ||
|
|
22b8f5454b | ||
|
|
801bd3f106 | ||
|
|
e26c8c870d | ||
|
|
f4e545e090 | ||
|
|
2006fc0d8d | ||
|
|
d456048e69 | ||
|
|
fd35b4da24 | ||
|
|
283d179b05 | ||
|
|
3093e7334b | ||
|
|
02c82f0038 | ||
|
|
e1eecc3b6c | ||
|
|
bf3d73a5f7 | ||
|
|
05257d6439 | ||
|
|
532e828fe6 | ||
|
|
12a407aca8 | ||
|
|
5ca458dada | ||
|
|
0aeff01c2d | ||
|
|
2ac164d5b8 | ||
|
|
14d475df9a | ||
|
|
f3c777b244 | ||
|
|
1544e58e76 | ||
|
|
5d0c9c7cbe | ||
|
|
8f58fcc4c4 | ||
|
|
5799ef705d | ||
|
|
0de7dde1ce | ||
|
|
7b39e5a79d | ||
|
|
583c791016 | ||
|
|
bc5dcdfe21 | ||
|
|
973aaced96 | ||
|
|
3e9ee908f8 | ||
|
|
e7347b582c | ||
|
|
71b7cffabc | ||
|
|
da9e9ff0be | ||
|
|
c6941d5905 | ||
|
|
a3cf960660 | ||
|
|
7eebfca20b | ||
|
|
e9167d96ec | ||
|
|
1664050ef7 | ||
|
|
7fa00c0e89 | ||
|
|
81d69c41f5 | ||
|
|
4e6b837ccc | ||
|
|
a9f7a23c0d | ||
|
|
07301adc6d | ||
|
|
c6630008b5 | ||
|
|
cdcaba34ce | ||
|
|
09b9a57e38 | ||
|
|
79d7c85832 | ||
|
|
eb0e8366bc | ||
|
|
cf9752db4f | ||
|
|
d6fb3b2c50 | ||
|
|
b04fdc68df | ||
|
|
706c41aad9 | ||
|
|
db9ec0605e | ||
|
|
a95172bf88 | ||
|
|
597116a0b4 | ||
|
|
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 |
@@ -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
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Contributing to Fluxer
|
||||
|
||||
This policy applies to all issues, discussions, commits and pull requests.
|
||||
This policy applies to all commits and pull requests.
|
||||
|
||||
## Scope
|
||||
|
||||
To prevent spam, only approved contributors may submit pull requests.
|
||||
|
||||
To request approval, comment on an existing issue and ask to implement it. For work that extends beyond a defect fix, open a [discussion](https://github.com/orgs/fluxerapp/discussions) first.
|
||||
To request approval, comment on the [feedback.fluxer.com](https://feedback.fluxer.com) post you want to implement and ask to work on it. For work that extends beyond a defect fix, post a feature request there first.
|
||||
|
||||
Every pull request must:
|
||||
|
||||
- Target the repository's default branch.
|
||||
- Include a closing reference for each repository issue it resolves.
|
||||
- Link each feedback.fluxer.com post it resolves.
|
||||
- Receive approval from a maintainer before it is merged.
|
||||
|
||||
Place each closing reference on a separate line:
|
||||
Place each link on a separate line:
|
||||
|
||||
```text
|
||||
Closes #123
|
||||
Closes #456
|
||||
Resolves https://feedback.fluxer.com/p/123
|
||||
Resolves https://feedback.fluxer.com/p/456
|
||||
```
|
||||
|
||||
## Authorship
|
||||
@@ -78,11 +78,11 @@ Complete every section of the pull request template. Clearly describe:
|
||||
|
||||
## Reports and other contributions
|
||||
|
||||
Use the [bug report form](https://github.com/fluxerapp/fluxer/issues/new?template=bug-report.yaml) to report reproducible defects.
|
||||
Report bugs and request features at [feedback.fluxer.com](https://feedback.fluxer.com).
|
||||
|
||||
Report security vulnerabilities privately through the channels specified in the [security policy](https://github.com/fluxerapp/fluxer/blob/main/.github/SECURITY.md). Do not report vulnerabilities in public issues or discussions.
|
||||
Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security). Never post them publicly.
|
||||
|
||||
Use [discussions](https://github.com/orgs/fluxerapp/discussions) for feature proposals and self-hosting questions.
|
||||
Read the [operator documentation](https://fluxer.dev) for self-hosting questions.
|
||||
|
||||
Submit translations through [Weblate](https://weblate.fluxer.tools), not through pull requests.
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-discussion.json
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Search existing discussions before posting a feature proposal.
|
||||
|
||||
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Current problem
|
||||
description: State what you are trying to do and what prevents it.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed change
|
||||
description: State the expected behaviour.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Optional. Include constraints, trade-offs, related discussions, screenshots or mockups.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched existing discussions.
|
||||
required: true
|
||||
@@ -16,4 +16,10 @@ Every commit made by a contributor must include the [Developer Certificate of Or
|
||||
|
||||
## Name and marks
|
||||
|
||||
The AGPL does not grant permission to use the Fluxer name, logo or other branding. Forks must use a distinct name and branding unless Fluxer Platform AB grants permission otherwise.
|
||||
Fluxer and the Fluxer logo are trademarks of Fluxer Platform AB. Neither the AGPL nor the CC BY-SA 4.0 licence on Fluxer artwork grants trademark rights. Fluxer Platform AB grants everyone the following permissions.
|
||||
|
||||
- You may distribute unmodified builds of Fluxer, or builds with light patches, under the Fluxer name and logo. Light patches are changes for packaging, portability, security and bug fixes, configuration defaults and translations. Linux distributions, nixpkgs, Flathub and container images are all covered.
|
||||
- A self-hosted instance running such a build may show the Fluxer name and logo under the instance's own name and domain, as long as it does not imply affiliation with or endorsement by Fluxer Platform AB.
|
||||
- You may refer to Fluxer by name to describe compatibility, for example "works with Fluxer".
|
||||
|
||||
Forks with substantive functional changes must use their own name and logo. Any other use needs permission from Fluxer Platform AB. Contact support@fluxer.com.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
|
||||
name: Bug report
|
||||
description: Report a reproducible defect in Fluxer.
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Search [open and closed issues](https://github.com/fluxerapp/fluxer/issues?q=is%3Aissue) before filing a report.
|
||||
|
||||
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>. Send account and billing requests to <[email protected]>.
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Observed behaviour
|
||||
description: State what happened and what you expected.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Give numbered steps starting from a fresh app or session.
|
||||
placeholder: |
|
||||
1. Go to ...
|
||||
2. Select ...
|
||||
3. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: build
|
||||
attributes:
|
||||
label: Build information
|
||||
description: >-
|
||||
Open User Settings, scroll to the bottom of the left sidebar, and select
|
||||
the build information. Fluxer copies it to the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: surface
|
||||
attributes:
|
||||
label: Affected surface
|
||||
multiple: true
|
||||
options:
|
||||
- Desktop app
|
||||
- Web app
|
||||
- Voice, video, or Go Live
|
||||
- Self-hosted instance
|
||||
- HTTP API or Gateway
|
||||
- Documentation site
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: instance
|
||||
attributes:
|
||||
label: Instance
|
||||
description: For a self-hosted instance, include the release tag and database backend.
|
||||
placeholder: fluxer.app
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Evidence
|
||||
description: Attach relevant logs, screenshots or recordings. Remove tokens, keys, private messages and other personal data. Configuration files may contain secrets.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched open and closed issues.
|
||||
required: true
|
||||
- label: I removed secrets and unrelated personal data from the report.
|
||||
required: true
|
||||
@@ -1,18 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Mobile client bugs
|
||||
url: https://github.com/fluxerapp/flutter_client#bug-reporting
|
||||
about: Read the reporting instructions for the Fluxer mobile client.
|
||||
- name: Account and billing support
|
||||
url: https://fluxer.app/help
|
||||
about: Find account help and support contact details.
|
||||
- name: Feature proposals
|
||||
url: https://github.com/orgs/fluxerapp/discussions
|
||||
about: Propose a feature in a discussion.
|
||||
- name: Translations
|
||||
url: https://weblate.fluxer.tools
|
||||
about: Improve an existing locale or start a new one.
|
||||
- name: Self-hosting support
|
||||
url: https://fluxer.dev
|
||||
about: Read the operator documentation, then open a discussion if the problem remains.
|
||||
@@ -1,44 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
|
||||
name: Documentation
|
||||
description: Report incorrect, missing or unclear documentation.
|
||||
type: Task
|
||||
labels:
|
||||
- docs
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This form covers <https://fluxer.dev> and operator documentation.
|
||||
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: Documentation defect
|
||||
description: State what the page says and what is correct. For missing content, state what information you needed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: location
|
||||
attributes:
|
||||
label: Location
|
||||
description: Provide the page URL or file path and heading.
|
||||
placeholder: https://fluxer.dev/gateway/overview/
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: Proposed wording
|
||||
description: Optional.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched open and closed issues.
|
||||
required: true
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Security policy
|
||||
|
||||
Do not report a vulnerability in an issue, pull request, or discussion.
|
||||
Do not report a vulnerability in a pull request, on feedback.fluxer.com, in a Fluxer community, or in a direct message to staff.
|
||||
|
||||
Submit a report through [GitHub private vulnerability reporting](https://github.com/fluxerapp/fluxer/security/advisories/new) or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
|
||||
Submit a report through <https://fluxer.app/security> or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
|
||||
|
||||
The programme scope, testing rules, safe harbour, disclosure process, and reward terms are published at <https://fluxer.app/security>. That page is authoritative.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Closes #
|
||||
Resolves https://feedback.fluxer.com/p/
|
||||
|
||||
<!-- Repeat this line for each resolved issue, up to 20. Remove the placeholder only if no issue is resolved and the approval gate does not apply. -->
|
||||
<!-- Repeat this line for each feedback.fluxer.com post this resolves, up to 20. Remove the placeholder only if no post is resolved and the approval gate does not apply. -->
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
Generated
+7
@@ -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",
|
||||
@@ -1982,11 +1986,13 @@ dependencies = [
|
||||
"fluxer-svc",
|
||||
"fluxer_common",
|
||||
"futures",
|
||||
"hmac 0.13.0",
|
||||
"moka",
|
||||
"rmp-serde",
|
||||
"scylla",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2038,6 +2044,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
|
||||
# Fluxer
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Bug reports and feature requests have moved to [feedback.fluxer.com](https://feedback.fluxer.com). Sign in with your Fluxer account to post, vote and follow updates. GitHub Issues and Discussions are closed. Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security).
|
||||
|
||||
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
|
||||
|
||||
<p align="center">
|
||||
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer running side by side on a desktop monitor and a phone" width="640">
|
||||
<img src="https://fluxer.app/static/img/screenshots-desktop-readme-1920w.70cb6ce340007e0a.png" alt="Fluxer running side by side on a desktop monitor and a phone" width="640">
|
||||
</p>
|
||||
|
||||
## Download
|
||||
@@ -143,14 +146,13 @@ Full setup notes, including canary, are in the [Linux repositories documentation
|
||||
|
||||
The source is licensed under the [AGPL-3.0-or-later](./LICENSE) license.
|
||||
|
||||
Fluxer branding, icons, default avatars, badge artwork, screenshots and marketing
|
||||
imagery are copyright Fluxer, all rights reserved, as set out in
|
||||
[fluxer_static/LICENSE](./fluxer_static/LICENSE). Third-party material keeps its own
|
||||
terms, listed in
|
||||
Fluxer artwork, such as the logo, icons, badges and default avatars, is
|
||||
licensed under [CC BY-SA 4.0](./fluxer_static/LICENSE). Third-party material
|
||||
keeps its own terms, listed in
|
||||
[fluxer_static/THIRD_PARTY_LICENSES.md](./fluxer_static/THIRD_PARTY_LICENSES.md).
|
||||
|
||||
Public availability of this repository does not grant trademark, brand, or
|
||||
endorsement rights.
|
||||
Use of the Fluxer name and logo is covered by the
|
||||
[name and marks policy](./.github/GOVERNANCE.md#name-and-marks).
|
||||
|
||||
[win-setup-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/setup
|
||||
[win-setup-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/setup
|
||||
|
||||
@@ -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"}}},
|
||||
|
||||
Vendored
+1
@@ -89,6 +89,7 @@ FLUXER_ADMIN_OAUTH_REDIRECT_URI=http://localhost:8088/admin/oauth2_callback
|
||||
|
||||
FLUXER_SUDO_MODE_SECRET=dev-sudo-secret
|
||||
FLUXER_CONNECTION_INITIATION_SECRET=dev-connection-initiation-secret
|
||||
FLUXER_PROFILE_PSEUDONYM_SECRET=fluxer-dev-profile-pseudonym-secret
|
||||
FLUXER_VAPID_PUBLIC_KEY=BHIbdKs24FdPkOQS7hbeg3adceLS0IqlKsn71ywEe6kbeopeFFiG3lkvJac7BVqkuk7mxwEa555O2FXV3HLt56w
|
||||
FLUXER_VAPID_PRIVATE_KEY=cs24JvXSxHiqJQgkJNocJFAdzJpPmpfU9xD-fDpn3tw
|
||||
FLUXER_VAPID_EMAIL=dev@localhost
|
||||
|
||||
@@ -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
|
||||
@@ -138,6 +138,7 @@ MEILI_MASTER_KEY=CHANGE_ME
|
||||
#FLUXER_STRIPE_PRICES={}
|
||||
#FLUXER_STRIPE_LEGACY_PRICES={}
|
||||
#FLUXER_API_DONATION_PROXY_KEY=
|
||||
#FLUXER_API_TRUSTED_CALLERS=[]
|
||||
#FLUXER_VISIONARIES_GUILD_ID=
|
||||
#FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID=
|
||||
|
||||
@@ -183,6 +184,7 @@ FLUXER_S3_SECRET_KEY=CHANGE_ME
|
||||
|
||||
FLUXER_SUDO_MODE_SECRET=CHANGE_ME
|
||||
FLUXER_CONNECTION_INITIATION_SECRET=CHANGE_ME
|
||||
FLUXER_PROFILE_PSEUDONYM_SECRET=CHANGE_ME
|
||||
FLUXER_GATEWAY_RPC_AUTH_TOKEN=CHANGE_ME
|
||||
FLUXER_ERLANG_COOKIE=CHANGE_ME
|
||||
FLUXER_MEDIA_PROXY_SECRET_KEY=CHANGE_ME
|
||||
@@ -217,6 +219,9 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
#FLUXER_PUSH_SERVICE_FCM_BASE_URL=https://fcm.googleapis.com
|
||||
#FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS=push.fluxer.com
|
||||
#FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS=
|
||||
# Push hosts on your own network, such as a ntfy server, that may resolve to
|
||||
# private addresses. Comma separated.
|
||||
#FLUXER_PUSH_SERVICE_PRIVATE_HOSTS=ntfy.example.com
|
||||
#FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED=false
|
||||
|
||||
# Direct mobile push through your own APNs and FCM credentials, off by default.
|
||||
@@ -258,7 +263,7 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
|
||||
# only when a browser must reach an origin the defaults do not cover. Separate
|
||||
# several with spaces or commas. The three values below are illustrations.
|
||||
#FLUXER_CSP_EXTRA_DEFAULT_SRC=
|
||||
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
|
||||
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com
|
||||
#FLUXER_CSP_EXTRA_IMG_SRC=https://cdn.example.com
|
||||
#FLUXER_CSP_EXTRA_MEDIA_SRC=
|
||||
#FLUXER_CSP_EXTRA_FONT_SRC=
|
||||
@@ -305,10 +310,12 @@ FLUXER_KLIPY_API_KEY=
|
||||
# Hosts the api never unfurls, comma separated.
|
||||
#FLUXER_API_UNFURL_IGNORED_HOSTS=
|
||||
|
||||
# Email delivery. Only an instance where members sign in with email needs it.
|
||||
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
|
||||
@@ -331,6 +338,10 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_APP_STATUS_PAGE_URL=
|
||||
#FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL=
|
||||
#FLUXER_INSTANCE_SETUP_CONFIGURED=false
|
||||
# How members sign in on a new instance, username or email. Unset means username. Read only on the first start.
|
||||
#FLUXER_ACCOUNT_IDENTITY=
|
||||
# Username tags on a new email instance. none gives unique names with no tag, random gives name#4821. Unset means none. A username instance always uses none. Read only on the first start.
|
||||
#FLUXER_TAG_STYLE=
|
||||
#FLUXER_AUTO_JOIN_INVITE_CODE=
|
||||
#FLUXER_DELETION_GRACE_PERIOD_HOURS=336
|
||||
|
||||
@@ -356,9 +367,8 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_GIFT_ENDPOINT=
|
||||
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT=
|
||||
#PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT=
|
||||
# These follow FLUXER_STATIC_CDN_ENDPOINT first, then the public origin.
|
||||
# This follows FLUXER_STATIC_CDN_ENDPOINT first, then the public origin.
|
||||
#FLUXER_GATEWAY_STATIC_CDN_ENDPOINT=
|
||||
#FLUXER_UNFURL_STATIC_CDN_ENDPOINT=
|
||||
# These follow FLUXER_MEDIA_ENDPOINT first, then the public origin.
|
||||
#FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT=
|
||||
#FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT=
|
||||
@@ -411,7 +421,7 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_POSTGRES_MEMORY_RESERVATION=3gb
|
||||
#FLUXER_VALKEY_MEMORY_LIMIT=256mb
|
||||
#FLUXER_NATS_MEMORY_LIMIT=256mb
|
||||
#FLUXER_MEILISEARCH_MEMORY_LIMIT=768mb
|
||||
#FLUXER_MEILISEARCH_MEMORY_LIMIT=1536mb
|
||||
#FLUXER_SEAWEEDFS_MEMORY_LIMIT=2gb
|
||||
#FLUXER_SEAWEEDFS_INIT_MEMORY_LIMIT=128mb
|
||||
#FLUXER_LIVEKIT_MEMORY_LIMIT=512mb
|
||||
@@ -437,8 +447,11 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_UNFURL_SHARD_MEMORY_LIMIT=256mb
|
||||
#FLUXER_ADMIN_MEMORY_LIMIT=256mb
|
||||
|
||||
# Meilisearch indexing memory. Keep it well under the container limit above.
|
||||
#FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=384mb
|
||||
# Meilisearch indexing memory and threads. Each indexing thread needs its own
|
||||
# buffers on top of the indexing memory, so raise the threads only together with
|
||||
# the container limit above.
|
||||
#FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=256mb
|
||||
#FLUXER_MEILISEARCH_MAX_INDEXING_THREADS=2
|
||||
#FLUXER_MEILISEARCH_ENV=production
|
||||
#FLUXER_MEILISEARCH_NO_ANALYTICS=true
|
||||
|
||||
@@ -448,6 +461,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
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
reverse_proxy admin:8080
|
||||
}
|
||||
|
||||
@staticAssets path /web/* /emoji/* /libs/* /avatars/* /badges/* /desktop/* /embeds/*
|
||||
@staticAssets path /web/* /emoji/* /libs/* /avatars/* /badges/* /desktop/*
|
||||
handle @staticAssets {
|
||||
reverse_proxy static-proxy:8080
|
||||
}
|
||||
|
||||
@@ -113,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:-}
|
||||
@@ -127,6 +128,7 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_STRIPE_PRICES: ${FLUXER_STRIPE_PRICES:-}
|
||||
FLUXER_STRIPE_LEGACY_PRICES: ${FLUXER_STRIPE_LEGACY_PRICES:-}
|
||||
FLUXER_API_DONATION_PROXY_KEY: ${FLUXER_API_DONATION_PROXY_KEY:-}
|
||||
FLUXER_API_TRUSTED_CALLERS: ${FLUXER_API_TRUSTED_CALLERS:-}
|
||||
FLUXER_VISIONARIES_GUILD_ID: ${FLUXER_VISIONARIES_GUILD_ID:-}
|
||||
FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID: ${FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID:-}
|
||||
|
||||
@@ -150,6 +152,8 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_APP_STATUS_PAGE_URL: ${FLUXER_APP_STATUS_PAGE_URL:-}
|
||||
FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: ${FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL:-}
|
||||
FLUXER_INSTANCE_SETUP_CONFIGURED: ${FLUXER_INSTANCE_SETUP_CONFIGURED:-}
|
||||
FLUXER_ACCOUNT_IDENTITY: ${FLUXER_ACCOUNT_IDENTITY:-}
|
||||
FLUXER_TAG_STYLE: ${FLUXER_TAG_STYLE:-}
|
||||
FLUXER_AUTO_JOIN_INVITE_CODE: ${FLUXER_AUTO_JOIN_INVITE_CODE:-}
|
||||
FLUXER_DISCOVERY_ENABLED: ${FLUXER_DISCOVERY_ENABLED:-}
|
||||
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: ${FLUXER_DISCOVERY_MIN_MEMBER_COUNT:-}
|
||||
@@ -173,6 +177,7 @@ x-fluxer-env: &fluxer-env
|
||||
|
||||
FLUXER_SUDO_MODE_SECRET: ${FLUXER_SUDO_MODE_SECRET:?set FLUXER_SUDO_MODE_SECRET in .env}
|
||||
FLUXER_CONNECTION_INITIATION_SECRET: ${FLUXER_CONNECTION_INITIATION_SECRET:?set FLUXER_CONNECTION_INITIATION_SECRET in .env}
|
||||
FLUXER_PROFILE_PSEUDONYM_SECRET: ${FLUXER_PROFILE_PSEUDONYM_SECRET:?set FLUXER_PROFILE_PSEUDONYM_SECRET in .env}
|
||||
FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES: ${FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES:-}
|
||||
FLUXER_VAPID_PUBLIC_KEY: ${FLUXER_VAPID_PUBLIC_KEY:?set FLUXER_VAPID_PUBLIC_KEY in .env}
|
||||
FLUXER_VAPID_PRIVATE_KEY: ${FLUXER_VAPID_PRIVATE_KEY:?set FLUXER_VAPID_PRIVATE_KEY in .env}
|
||||
@@ -329,12 +334,13 @@ services:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: ${FLUXER_MEILISEARCH_MEMORY_LIMIT:-768mb}
|
||||
memory: ${FLUXER_MEILISEARCH_MEMORY_LIMIT:-1536mb}
|
||||
environment:
|
||||
MEILI_ENV: ${FLUXER_MEILISEARCH_ENV:-production}
|
||||
MEILI_NO_ANALYTICS: "${FLUXER_MEILISEARCH_NO_ANALYTICS:-true}"
|
||||
MEILI_UPGRADE_DB: "true"
|
||||
MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-384mb}
|
||||
MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-256mb}
|
||||
MEILI_MAX_INDEXING_THREADS: ${FLUXER_MEILISEARCH_MAX_INDEXING_THREADS:-2}
|
||||
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
|
||||
volumes:
|
||||
- meilisearch-data:/meili_data
|
||||
@@ -353,6 +359,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
|
||||
@@ -608,6 +615,7 @@ services:
|
||||
FLUXER_PUSH_SERVICE_FCM_BASE_URL: ${FLUXER_PUSH_SERVICE_FCM_BASE_URL:-}
|
||||
FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS: ${FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS:-}
|
||||
FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS: ${FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS:-}
|
||||
FLUXER_PUSH_SERVICE_PRIVATE_HOSTS: ${FLUXER_PUSH_SERVICE_PRIVATE_HOSTS:-}
|
||||
FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED: ${FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED:-}
|
||||
FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT: ${FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT:-}
|
||||
FLUXER_PUSH_FCM_ENABLED: ${FLUXER_PUSH_FCM_ENABLED:-}
|
||||
@@ -842,8 +850,6 @@ services:
|
||||
FLUXER_SVC_MODE: shard
|
||||
FLUXER_SVC_SHARD_ID: "0"
|
||||
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
|
||||
FLUXER_UNFURL_STATIC_CDN_ENDPOINT: ${FLUXER_UNFURL_STATIC_CDN_ENDPOINT:-}
|
||||
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_STATIC_CDN_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
|
||||
healthcheck: *fluxer-svc-healthcheck
|
||||
depends_on:
|
||||
nats: {condition: service_healthy}
|
||||
|
||||
+19
-1
@@ -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,
|
||||
@@ -582,7 +600,7 @@ fn select_faces(package_dir: &Path) -> Vec<Face> {
|
||||
}
|
||||
assert!(
|
||||
face["unicodeRange"].is_null(),
|
||||
"{wanted} face {} carries a unicode-range; Latin-core faces must not",
|
||||
"{wanted} face {} has a unicode-range; Latin-core faces must not",
|
||||
face["file"]
|
||||
);
|
||||
faces.push(Face {
|
||||
|
||||
+1071
-602
File diff suppressed because it is too large
Load Diff
+4
-10
@@ -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";
|
||||
@@ -77,8 +76,9 @@ pub const REPORT_VIEW: &str = "report:view";
|
||||
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_CREATE_PASSWORD_RESET_LINK: &str = "user:create:password_reset_link";
|
||||
pub const USER_DELETE_RECOVERY_KIT: &str = "user:delete:recovery_kit";
|
||||
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";
|
||||
@@ -90,14 +90,11 @@ pub const USER_VIEW_DOB: &str = "user:view:dob";
|
||||
pub const USER_VIEW_EMAIL: &str = "user:view:email";
|
||||
pub const USER_VIEW_IP: &str = "user:view:ip";
|
||||
pub const USER_TEMP_BAN: &str = "user:temp_ban";
|
||||
pub const USER_UPDATE_BOT_STATUS: &str = "user:update:bot_status";
|
||||
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 +148,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,
|
||||
@@ -186,8 +182,9 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
REPORT_VIEW_REPORTER_PII,
|
||||
SYSTEM_DM_SEND,
|
||||
USER_CANCEL_BULK_MESSAGE_DELETION,
|
||||
USER_CREATE_PASSWORD_RESET_LINK,
|
||||
USER_DELETE_RECOVERY_KIT,
|
||||
USER_DELETE,
|
||||
USER_DISABLE_SUSPICIOUS,
|
||||
USER_LIST_DM_CHANNELS,
|
||||
USER_LIST_GUILDS,
|
||||
USER_LIST_RELATIONSHIPS,
|
||||
@@ -199,14 +196,11 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
USER_VIEW_EMAIL,
|
||||
USER_VIEW_IP,
|
||||
USER_TEMP_BAN,
|
||||
USER_UPDATE_BOT_STATUS,
|
||||
USER_UPDATE_DOB,
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::api::generated::{snowflake, types as generated_types};
|
||||
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{BanAvatarResult, BanCheckResult, BulkBanResult};
|
||||
use super::types::{BanAvatarResult, BanCheckResult, BlocklistEntryPage, BulkBanResult};
|
||||
|
||||
impl AdminApiClient {
|
||||
pub async fn ban_email(&self, email: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
|
||||
@@ -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,
|
||||
@@ -28,11 +28,23 @@ impl AdminApiClient {
|
||||
self.check_blocklist_entry("email", email, None).await
|
||||
}
|
||||
|
||||
pub async fn ban_ip(&self, ip: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
|
||||
pub async fn ban_ip(
|
||||
&self,
|
||||
ip: &str,
|
||||
duration_hours: u32,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<()> {
|
||||
self.create_blocklist_entry(
|
||||
"ip",
|
||||
generated_types::AdminBlocklistEntryCreateRequest::from(
|
||||
generated_types::BanIpRequest { ip: ip.to_owned() },
|
||||
generated_types::BanIpRequest {
|
||||
duration_hours: Some(
|
||||
i32::try_from(duration_hours)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?
|
||||
.into(),
|
||||
),
|
||||
ip: ip.to_owned(),
|
||||
},
|
||||
),
|
||||
audit_log_reason,
|
||||
)
|
||||
@@ -136,6 +148,19 @@ impl AdminApiClient {
|
||||
self.check_blocklist_entry("url-domain", domain, None).await
|
||||
}
|
||||
|
||||
pub async fn list_url_domain_entries(
|
||||
&self,
|
||||
after: Option<&str>,
|
||||
) -> ApiResult<BlocklistEntryPage> {
|
||||
let list_type = blocklist_list_type("url-domain")?;
|
||||
let response = self
|
||||
.generated()
|
||||
.list_admin_blocklist_entries(list_type, after, Some(BLOCKLIST_PAGE_SIZE), None)
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn ban_file_sha(
|
||||
&self,
|
||||
sha256_hex: &str,
|
||||
@@ -323,6 +348,8 @@ impl AdminApiClient {
|
||||
|
||||
const PROFILE_SUBSTRING_LIST: &str = "profile-substring";
|
||||
|
||||
const BLOCKLIST_PAGE_SIZE: &str = "200";
|
||||
|
||||
fn blocklist_list_type(list_type: &str) -> ApiResult<generated_types::AdminBlocklistListType> {
|
||||
generated_types::AdminBlocklistListType::try_from(list_type)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -432,7 +432,7 @@ mod tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
fn audit_log_reason_header_carries_utf8_bytes() {
|
||||
fn audit_log_reason_header_keeps_utf8_bytes() {
|
||||
let reason = "§ 3 Regel – wiederholt 日本";
|
||||
let value = audit_log_reason_header(reason).expect("valid reason header");
|
||||
assert_eq!(value.as_bytes(), reason.as_bytes());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::api::generated::snowflake;
|
||||
|
||||
use super::client::{AdminApiClient, ApiResult};
|
||||
use super::types::ListGuildThreadsResponse;
|
||||
|
||||
impl AdminApiClient {
|
||||
pub async fn list_guild_threads(&self, guild_id: &str) -> ApiResult<ListGuildThreadsResponse> {
|
||||
let response = self
|
||||
.generated()
|
||||
.list_admin_guild_threads(&snowflake(guild_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn delete_thread_channel(&self, channel_id: &str) -> ApiResult<()> {
|
||||
self.generated()
|
||||
.delete_admin_thread_channel(&snowflake(channel_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use super::client::{AdminApiClient, ApiResult};
|
||||
use super::types::{
|
||||
CreateRegistrationUrlRequest, CreateRegistrationUrlResponse, InstanceConfigResponse,
|
||||
InstanceConfigUpdateRequest, InstanceEmailSmtpTestRequest, InstanceEmailSmtpTestResponse,
|
||||
InstancePremiumDiscovery,
|
||||
AccountIdentitySettings, CreateRegistrationUrlRequest, CreateRegistrationUrlResponse,
|
||||
InstanceAccountIdentityDiscovery, InstanceConfigResponse, InstanceConfigUpdateRequest,
|
||||
InstanceEmailSmtpTestRequest, InstanceEmailSmtpTestResponse, InstancePremiumDiscovery,
|
||||
};
|
||||
|
||||
impl AdminApiClient {
|
||||
@@ -16,6 +16,16 @@ impl AdminApiClient {
|
||||
self.get("/.well-known/fluxer", None).await
|
||||
}
|
||||
|
||||
pub async fn get_instance_account_identity(&self) -> ApiResult<AccountIdentitySettings> {
|
||||
let discovery: InstanceAccountIdentityDiscovery =
|
||||
self.get("/.well-known/fluxer", None).await?;
|
||||
let mode = discovery.features.account_identity;
|
||||
Ok(AccountIdentitySettings {
|
||||
mode,
|
||||
tag_style: discovery.features.tag_style,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_instance_config(
|
||||
&self,
|
||||
update: &InstanceConfigUpdateRequest,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod client;
|
||||
pub mod codes;
|
||||
pub mod discovery;
|
||||
pub mod guild_assets;
|
||||
pub mod guild_threads;
|
||||
pub mod guilds;
|
||||
pub mod instance_config;
|
||||
pub mod jobs;
|
||||
|
||||
@@ -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>,
|
||||
@@ -261,9 +255,30 @@ pub enum FlashLevel {
|
||||
pub struct BanCheckResult {
|
||||
pub banned: bool,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub entries: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BlocklistEntry {
|
||||
pub value: String,
|
||||
#[serde(default)]
|
||||
pub match_subdomains: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub category: Option<String>,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BlocklistEntryPage {
|
||||
pub items: Vec<BlocklistEntry>,
|
||||
pub has_more: bool,
|
||||
#[serde(default)]
|
||||
pub next_after: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BulkBanResult {
|
||||
pub job_id: String,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GuildThreadMetadata {
|
||||
pub archived: bool,
|
||||
pub locked: bool,
|
||||
pub auto_archive_duration: i32,
|
||||
pub archive_timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GuildThreadItem {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub channel_type: i32,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub parent_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub owner_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub member_count: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub message_count: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub thread_metadata: Option<GuildThreadMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListGuildThreadsResponse {
|
||||
pub threads: Vec<GuildThreadItem>,
|
||||
}
|
||||
@@ -13,6 +13,8 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub self_hosted: bool,
|
||||
#[serde(default)]
|
||||
pub account_identity: AccountIdentityConfigResponse,
|
||||
#[serde(default)]
|
||||
pub app_public: AppPublicConfigResponse,
|
||||
#[serde(default)]
|
||||
pub policy: InstancePolicyResponse,
|
||||
@@ -25,13 +27,90 @@ pub struct InstanceConfigResponse {
|
||||
#[serde(default)]
|
||||
pub domain_migration: DomainMigrationConfigResponse,
|
||||
#[serde(default)]
|
||||
pub plutonium_page: PlutoniumPageConfigResponse,
|
||||
#[serde(default)]
|
||||
pub captcha: CaptchaConfigResponse,
|
||||
#[serde(default)]
|
||||
pub channel_threads: ChannelThreadsConfigResponse,
|
||||
#[serde(default)]
|
||||
pub experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
#[serde(default)]
|
||||
pub billing: InstanceBillingResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AccountIdentityMode {
|
||||
#[default]
|
||||
Email,
|
||||
Username,
|
||||
}
|
||||
|
||||
impl AccountIdentityMode {
|
||||
pub fn is_username(self) -> bool {
|
||||
matches!(self, Self::Username)
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Email => "Email",
|
||||
Self::Username => "Username",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TagStyle {
|
||||
None,
|
||||
#[default]
|
||||
#[serde(other)]
|
||||
Random,
|
||||
}
|
||||
|
||||
impl TagStyle {
|
||||
pub fn is_none(self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "No tags",
|
||||
Self::Random => "Random tags",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct AccountIdentityConfigResponse {
|
||||
#[serde(default)]
|
||||
pub mode: AccountIdentityMode,
|
||||
#[serde(default)]
|
||||
pub locked: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub tag_style: TagStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct AccountIdentitySettings {
|
||||
pub mode: AccountIdentityMode,
|
||||
pub tag_style: TagStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct InstanceAccountIdentityDiscovery {
|
||||
#[serde(default)]
|
||||
pub features: InstanceAccountIdentityDiscoveryFeatures,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct InstanceAccountIdentityDiscoveryFeatures {
|
||||
#[serde(default)]
|
||||
pub account_identity: AccountIdentityMode,
|
||||
#[serde(default)]
|
||||
pub tag_style: TagStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstancePolicyResponse {
|
||||
#[serde(default)]
|
||||
@@ -430,8 +509,11 @@ 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;
|
||||
pub const CHANNEL_THREADS_DEFAULT_GUILD_SALT: &str = "channel-threads-guild-v1";
|
||||
pub const CHANNEL_THREADS_DEFAULT_USER_SALT: &str = "channel-threads-user-v1";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
@@ -501,6 +583,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 {
|
||||
@@ -529,6 +657,62 @@ pub struct CaptchaConfigUpdateRequest {
|
||||
pub max_counter: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ChannelThreadsConfigResponse {
|
||||
pub enabled: bool,
|
||||
pub config_version: u64,
|
||||
pub ever_enabled: bool,
|
||||
pub guild_basis_points: u32,
|
||||
pub guild_salt: String,
|
||||
pub enabled_guild_ids: Vec<String>,
|
||||
pub disabled_guild_ids: Vec<String>,
|
||||
pub user_basis_points: u32,
|
||||
pub user_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ChannelThreadsConfigResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_version: 0,
|
||||
ever_enabled: false,
|
||||
guild_basis_points: 0,
|
||||
guild_salt: CHANNEL_THREADS_DEFAULT_GUILD_SALT.to_owned(),
|
||||
enabled_guild_ids: Vec::new(),
|
||||
disabled_guild_ids: Vec::new(),
|
||||
user_basis_points: 0,
|
||||
user_salt: CHANNEL_THREADS_DEFAULT_USER_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct ChannelThreadsConfigUpdateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guild_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled_guild_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disabled_guild_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_basis_points: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_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 excluded_user_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ExperimentDeliveryConfigResponse {
|
||||
@@ -647,8 +831,12 @@ 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 channel_threads: Option<ChannelThreadsConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub billing: Option<InstanceBillingUpdateRequest>,
|
||||
@@ -949,17 +1137,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 =
|
||||
@@ -969,6 +1164,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
|
||||
@@ -980,6 +1180,7 @@ mod tests {
|
||||
);
|
||||
for (name, value) in [
|
||||
("DomainMigrationConfigResponse", domain_migration),
|
||||
("PlutoniumPageConfigResponse", plutonium_page),
|
||||
("CaptchaConfigResponse", captcha),
|
||||
("ExperimentDeliveryConfigResponse", delivery),
|
||||
] {
|
||||
@@ -1014,4 +1215,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!({})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod codes;
|
||||
mod common;
|
||||
mod discovery;
|
||||
mod guild_assets;
|
||||
mod guild_threads;
|
||||
mod instance_billing;
|
||||
mod instance_config;
|
||||
mod jobs;
|
||||
@@ -29,6 +30,7 @@ pub use codes::*;
|
||||
pub use common::*;
|
||||
pub use discovery::*;
|
||||
pub use guild_assets::*;
|
||||
pub use guild_threads::*;
|
||||
pub use instance_billing::*;
|
||||
pub use instance_config::*;
|
||||
pub use jobs::*;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -232,3 +232,9 @@ pub struct WebAuthnCredential {
|
||||
}
|
||||
|
||||
pub type WebAuthnCredentialListResponse = Vec<WebAuthnCredential>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PasswordResetLinkResponse {
|
||||
pub url: String,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::api::generated::{snowflake, types as generated_types};
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{
|
||||
AdminUser, AdminUserMeResponse, GuildInfo, ListUserGuildsResponse, LookupUserResponse,
|
||||
SearchUsersResponse, TerminateSessionsResponse, UserMutationResponse,
|
||||
PasswordResetLinkResponse, SearchUsersResponse, TerminateSessionsResponse,
|
||||
UserMutationResponse,
|
||||
};
|
||||
|
||||
impl AdminApiClient {
|
||||
@@ -231,22 +232,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 +284,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,
|
||||
@@ -333,28 +306,6 @@ impl AdminApiClient {
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn set_bot_status(&self, user_id: &str, is_bot: bool) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserBotStatusRequest { bot: is_bot };
|
||||
let response = self
|
||||
.generated()
|
||||
.set_admin_user_bot_status(&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_system_status(&self, user_id: &str, is_system: bool) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserSystemStatusRequest { system: is_system };
|
||||
let response = self
|
||||
.generated()
|
||||
.set_admin_user_system_status(&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 change_username(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -523,6 +474,26 @@ impl AdminApiClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_password_reset_link(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> ApiResult<PasswordResetLinkResponse> {
|
||||
let response = self
|
||||
.generated()
|
||||
.create_admin_user_password_reset_link(&snowflake(user_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn revoke_recovery_kit(&self, user_id: &str) -> ApiResult<()> {
|
||||
self.generated()
|
||||
.revoke_admin_user_recovery_kit(&snowflake(user_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_relationship(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::client::AdminApiClient, middleware::auth::AuthContext, state::AppState,
|
||||
utils::user_tag::with_unique_usernames,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
|
||||
pub async fn scope_account_identity(
|
||||
State(state): State<AppState>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let Some(auth) = request.extensions().get::<AuthContext>() else {
|
||||
return next.run(request).await;
|
||||
};
|
||||
let client = AdminApiClient::new(state.http_client(), state.config(), &auth.session);
|
||||
let settings = state.account_identity_settings(&client).await;
|
||||
let unique_usernames = settings.mode.is_username() || settings.tag_style.is_none();
|
||||
with_unique_usernames(unique_usernames, next.run(request)).await
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod account_identity;
|
||||
pub mod auth;
|
||||
pub mod csrf;
|
||||
pub mod error_handler;
|
||||
|
||||
+107
-44
@@ -1,7 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::client::AdminApiClient,
|
||||
api::{
|
||||
client::{AdminApiClient, ApiError},
|
||||
types::FlashMessage,
|
||||
},
|
||||
middleware::{auth::AuthContext, csrf, htmx},
|
||||
state::AppState,
|
||||
templates,
|
||||
@@ -13,10 +16,12 @@ use axum::{
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::ActionQuery;
|
||||
use super::bans_actions::{
|
||||
BanFormData, custom_flash, execute_ban, extract_value, flash_response, render_inline_flash,
|
||||
to_flash,
|
||||
};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
@@ -43,17 +48,41 @@ pub fn router() -> Router<AppState> {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_ban_page(state: &AppState, auth: &AuthContext, key: &str, req: &Request) -> Response {
|
||||
async fn render_ban_page(
|
||||
state: &AppState,
|
||||
auth: &AuthContext,
|
||||
key: &str,
|
||||
csrf_token: String,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let ban_cfg = match templates::pages::bans::get_ban_config(key) {
|
||||
Some(c) => c,
|
||||
None => return axum::http::StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
let csrf_token = csrf::get_csrf_token(req);
|
||||
let markup = templates::pages::bans::bans_page(config, auth, ban_cfg, None, &csrf_token);
|
||||
let username_sign_in = email_bans_on_username_instance(state, auth, key).await;
|
||||
let markup = templates::pages::bans::bans_page(
|
||||
config,
|
||||
auth,
|
||||
ban_cfg,
|
||||
None,
|
||||
&csrf_token,
|
||||
username_sign_in,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn email_bans_on_username_instance(state: &AppState, auth: &AuthContext, key: &str) -> bool {
|
||||
key == "email-bans"
|
||||
&& state
|
||||
.account_identity(&AdminApiClient::new(
|
||||
state.http_client(),
|
||||
state.config(),
|
||||
&auth.session,
|
||||
))
|
||||
.await
|
||||
.is_username()
|
||||
}
|
||||
|
||||
macro_rules! ban_get {
|
||||
($name:ident, $key:expr) => {
|
||||
async fn $name(
|
||||
@@ -61,7 +90,8 @@ macro_rules! ban_get {
|
||||
auth: axum::Extension<AuthContext>,
|
||||
request: Request,
|
||||
) -> Response {
|
||||
render_ban_page(&state, &auth.0, $key, &request)
|
||||
let csrf_token = csrf::get_csrf_token(&request);
|
||||
render_ban_page(&state, &auth.0, $key, csrf_token).await
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -90,17 +120,18 @@ async fn generic_ban_post(
|
||||
};
|
||||
let value = extract_value(form, ban_cfg.input_name);
|
||||
let is_htmx = htmx::is_htmx_request(headers);
|
||||
let (level, msg) = execute_ban(
|
||||
&client,
|
||||
ban_key,
|
||||
action,
|
||||
&value,
|
||||
form.hashes.as_deref(),
|
||||
form.sha256_list.as_deref(),
|
||||
form.audit_log_reason.as_deref(),
|
||||
let (level, msg) = execute_ban(&client, ban_key, action, &value, form).await;
|
||||
let username_sign_in = !is_htmx && email_bans_on_username_instance(state, auth, ban_key).await;
|
||||
flash_response(
|
||||
config,
|
||||
auth,
|
||||
is_htmx,
|
||||
level,
|
||||
&msg,
|
||||
ban_cfg,
|
||||
csrf_token,
|
||||
username_sign_in,
|
||||
)
|
||||
.await;
|
||||
flash_response(config, auth, is_htmx, level, &msg, ban_cfg, csrf_token)
|
||||
}
|
||||
|
||||
macro_rules! ban_post {
|
||||
@@ -118,14 +149,18 @@ macro_rules! ban_post {
|
||||
let form: BanFormData = match Form::from_request(request, &state).await {
|
||||
Ok(Form(f)) => f,
|
||||
Err(_) => {
|
||||
let is_htmx = htmx::is_htmx_request(&headers);
|
||||
let username_sign_in =
|
||||
!is_htmx && email_bans_on_username_instance(&state, &auth.0, $key).await;
|
||||
return flash_response(
|
||||
state.config(),
|
||||
&auth.0,
|
||||
htmx::is_htmx_request(&headers),
|
||||
is_htmx,
|
||||
"error",
|
||||
"Invalid form data",
|
||||
templates::pages::bans::get_ban_config($key).unwrap(),
|
||||
&csrf_token,
|
||||
username_sign_in,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -141,16 +176,56 @@ ban_post!(url_bans_post, "url-bans");
|
||||
ban_post!(file_sha_bans_post, "file-sha-bans");
|
||||
ban_post!(avatar_hash_bans_post, "avatar-hash-bans");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UrlDomainListQuery {
|
||||
after: Option<String>,
|
||||
}
|
||||
|
||||
async fn render_url_domain_page(
|
||||
state: &AppState,
|
||||
auth: &AuthContext,
|
||||
flash: Option<&FlashMessage>,
|
||||
csrf_token: &str,
|
||||
after: Option<&str>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.session);
|
||||
let entries = match client.list_url_domain_entries(after).await {
|
||||
Ok(page) => Some(page),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: list URL domain blocklist");
|
||||
None
|
||||
}
|
||||
};
|
||||
let markup = templates::pages::url_domain_bans::url_domain_bans_page(
|
||||
config,
|
||||
auth,
|
||||
flash,
|
||||
csrf_token,
|
||||
entries.as_ref(),
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
fn ban_url_domain_error(domain: &str, error: &ApiError) -> String {
|
||||
match error {
|
||||
ApiError::Http { status: 400, .. } => {
|
||||
format!("Failed to ban {domain}: not a valid domain, or the pattern is too broad")
|
||||
}
|
||||
_ => format!("Failed to ban {domain}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn url_domain_bans(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
request: Request,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let csrf_token = csrf::get_csrf_token(&request);
|
||||
let markup =
|
||||
templates::pages::url_domain_bans::url_domain_bans_page(config, &auth.0, None, &csrf_token);
|
||||
Html(markup.into_string()).into_response()
|
||||
let Query(query): Query<UrlDomainListQuery> =
|
||||
Query::try_from_uri(request.uri()).unwrap_or(Query(UrlDomainListQuery { after: None }));
|
||||
let after = query.after.as_deref().filter(|value| !value.is_empty());
|
||||
render_url_domain_page(&state, &auth.0, None, &csrf_token, after).await
|
||||
}
|
||||
|
||||
async fn url_domain_bans_post(
|
||||
@@ -181,10 +256,10 @@ async fn url_domain_bans_post(
|
||||
.ban_url_domain(&domain, m_sub, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("Domain {domain} banned successfully")),
|
||||
Ok(()) => ("success", format!("{domain} banned successfully")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, domain, "admin API request failed: ban URL domain");
|
||||
("error", format!("Failed to ban domain {domain}"))
|
||||
("error", ban_url_domain_error(&domain, &error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,15 +267,15 @@ async fn url_domain_bans_post(
|
||||
.unban_url_domain(&domain, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("Domain {domain} unbanned")),
|
||||
Ok(()) => ("success", format!("{domain} unbanned")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, domain, "admin API request failed: unban URL domain");
|
||||
("error", format!("Failed to unban domain {domain}"))
|
||||
("error", format!("Failed to unban {domain}"))
|
||||
}
|
||||
},
|
||||
"check" => match client.check_url_domain_ban(&domain).await {
|
||||
Ok(r) if r.banned => ("info", format!("Domain {domain} is banned")),
|
||||
Ok(_) => ("info", format!("Domain {domain} is NOT banned")),
|
||||
Ok(r) if r.banned => ("info", format!("{domain} is blocked")),
|
||||
Ok(_) => ("info", format!("{domain} is NOT blocked")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, domain, "admin API request failed: check URL domain ban");
|
||||
("error", "Error checking ban status".into())
|
||||
@@ -208,15 +283,11 @@ async fn url_domain_bans_post(
|
||||
},
|
||||
_ => ("error", "Unknown action".into()),
|
||||
};
|
||||
custom_flash(
|
||||
config,
|
||||
&auth.0,
|
||||
is_htmx,
|
||||
level,
|
||||
&msg,
|
||||
&csrf_token,
|
||||
"url-domain",
|
||||
)
|
||||
if is_htmx {
|
||||
return render_inline_flash(level, &msg);
|
||||
}
|
||||
let flash = to_flash(level, &msg);
|
||||
render_url_domain_page(&state, &auth.0, Some(&flash), &csrf_token, None).await
|
||||
}
|
||||
|
||||
async fn profile_substring_bans(
|
||||
@@ -288,13 +359,5 @@ async fn profile_substring_bans_post(
|
||||
},
|
||||
_ => ("error", "Unknown action".into()),
|
||||
};
|
||||
custom_flash(
|
||||
config,
|
||||
&auth.0,
|
||||
is_htmx,
|
||||
level,
|
||||
&msg,
|
||||
&csrf_token,
|
||||
"profile-substring",
|
||||
)
|
||||
custom_flash(config, &auth.0, is_htmx, level, &msg, &csrf_token)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct BanFormData {
|
||||
#[serde(default)]
|
||||
pub substring: Option<String>,
|
||||
#[serde(default)]
|
||||
pub duration_hours: Option<String>,
|
||||
#[serde(default)]
|
||||
pub audit_log_reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub _csrf: Option<String>,
|
||||
@@ -58,10 +60,12 @@ pub async fn execute_ban(
|
||||
ban_type: &str,
|
||||
action: &str,
|
||||
value: &str,
|
||||
bulk_hashes: Option<&str>,
|
||||
bulk_sha256_list: Option<&str>,
|
||||
audit_log_reason: Option<&str>,
|
||||
form: &BanFormData,
|
||||
) -> (&'static str, String) {
|
||||
let bulk_hashes = form.hashes.as_deref();
|
||||
let bulk_sha256_list = form.sha256_list.as_deref();
|
||||
let duration_hours = form.duration_hours.as_deref();
|
||||
let audit_log_reason = form.audit_log_reason.as_deref();
|
||||
if (action == "bulk-ban" || action == "bulk-ban-files") && ban_type == "file-sha-bans" {
|
||||
let raw_hashes = if action == "bulk-ban-files" {
|
||||
bulk_sha256_list
|
||||
@@ -74,6 +78,9 @@ pub async fn execute_ban(
|
||||
return ("error", "Value is required".into());
|
||||
}
|
||||
match action {
|
||||
"ban" if ban_type == "ip-bans" => {
|
||||
execute_ip_ban(client, value, duration_hours, audit_log_reason).await
|
||||
}
|
||||
"ban" => execute_single_ban(client, ban_type, value, audit_log_reason).await,
|
||||
"unban" => execute_single_unban(client, ban_type, value, audit_log_reason).await,
|
||||
"check" => execute_check(client, ban_type, value).await,
|
||||
@@ -107,6 +114,34 @@ async fn execute_bulk_ban(
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_ip_ban(
|
||||
client: &AdminApiClient,
|
||||
value: &str,
|
||||
duration_hours: Option<&str>,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (&'static str, String) {
|
||||
let duration_hours = match duration_hours.map(str::trim).filter(|v| !v.is_empty()) {
|
||||
None => 0,
|
||||
Some(raw) => match raw.parse::<u32>() {
|
||||
Ok(hours) => hours,
|
||||
Err(_) => return ("error", "Invalid ban duration".into()),
|
||||
},
|
||||
};
|
||||
let success_message = if duration_hours == 0 {
|
||||
format!("{value} banned permanently")
|
||||
} else {
|
||||
format!(
|
||||
"{value} banned for {}",
|
||||
crate::templates::pages::bans::ip_ban_duration_label(duration_hours)
|
||||
)
|
||||
};
|
||||
ban_action_result(
|
||||
client.ban_ip(value, duration_hours, audit_log_reason).await,
|
||||
success_message,
|
||||
format!("Failed to ban {value}"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute_single_ban(
|
||||
client: &AdminApiClient,
|
||||
ban_type: &str,
|
||||
@@ -114,7 +149,6 @@ async fn execute_single_ban(
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (&'static str, String) {
|
||||
let result = match ban_type {
|
||||
"ip-bans" => client.ban_ip(value, audit_log_reason).await,
|
||||
"email-bans" => client.ban_email(value, audit_log_reason).await,
|
||||
"phrase-bans" => client.ban_phrase(value, audit_log_reason).await,
|
||||
"url-bans" => client.ban_url(value, audit_log_reason).await,
|
||||
@@ -166,7 +200,10 @@ async fn execute_check(
|
||||
_ => return ("error", "Unknown ban type".into()),
|
||||
};
|
||||
match result {
|
||||
Ok(r) if r.banned => ("info", format!("{value} is banned")),
|
||||
Ok(r) if r.banned => match r.expires_at {
|
||||
Some(expires_at) => ("info", format!("{value} is banned until {expires_at}")),
|
||||
None => ("info", format!("{value} is banned")),
|
||||
},
|
||||
Ok(_) => ("info", format!("{value} is NOT banned")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, ban_type, value, "admin API request failed: check ban status");
|
||||
@@ -189,6 +226,7 @@ fn ban_action_result(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn flash_response(
|
||||
config: &crate::config::AdminConfig,
|
||||
auth: &AuthContext,
|
||||
@@ -197,13 +235,20 @@ pub fn flash_response(
|
||||
message: &str,
|
||||
ban_cfg: &templates::pages::bans::BanConfig,
|
||||
csrf_token: &str,
|
||||
username_sign_in: bool,
|
||||
) -> Response {
|
||||
if is_htmx {
|
||||
render_inline_flash(level, message)
|
||||
} else {
|
||||
let flash = to_flash(level, message);
|
||||
let markup =
|
||||
templates::pages::bans::bans_page(config, auth, ban_cfg, Some(&flash), csrf_token);
|
||||
let markup = templates::pages::bans::bans_page(
|
||||
config,
|
||||
auth,
|
||||
ban_cfg,
|
||||
Some(&flash),
|
||||
csrf_token,
|
||||
username_sign_in,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
}
|
||||
@@ -243,25 +288,16 @@ pub fn custom_flash(
|
||||
level: &str,
|
||||
message: &str,
|
||||
csrf_token: &str,
|
||||
page_type: &str,
|
||||
) -> Response {
|
||||
if is_htmx {
|
||||
return render_inline_flash(level, message);
|
||||
}
|
||||
let flash = to_flash(level, message);
|
||||
let markup = match page_type {
|
||||
"url-domain" => templates::pages::url_domain_bans::url_domain_bans_page(
|
||||
config,
|
||||
auth,
|
||||
Some(&flash),
|
||||
csrf_token,
|
||||
),
|
||||
_ => templates::pages::profile_substring_bans::profile_substring_bans_page(
|
||||
config,
|
||||
auth,
|
||||
Some(&flash),
|
||||
csrf_token,
|
||||
),
|
||||
};
|
||||
let markup = templates::pages::profile_substring_bans::profile_substring_bans_page(
|
||||
config,
|
||||
auth,
|
||||
Some(&flash),
|
||||
csrf_token,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
@@ -96,6 +96,33 @@ pub async fn render(
|
||||
config, &guild, &stickers, csrf_token,
|
||||
))
|
||||
}
|
||||
"threads" => {
|
||||
if !acl::has_permission(admin_acls, acl::GUILD_LOOKUP) {
|
||||
return None;
|
||||
}
|
||||
let threads = client
|
||||
.list_guild_threads(guild_id)
|
||||
.await
|
||||
.map(|response| response.threads)
|
||||
.map_err(|error| tracing::warn!(%error, guild_id, "admin API request failed: list guild threads"))
|
||||
.unwrap_or_default();
|
||||
let threads_enabled = client
|
||||
.get_instance_config()
|
||||
.await
|
||||
.map(|instance| instance.channel_threads.enabled)
|
||||
.map_err(
|
||||
|error| tracing::warn!(%error, "admin API request failed: get instance config"),
|
||||
)
|
||||
.unwrap_or(false);
|
||||
Some(tabs::threads::threads_tab(
|
||||
config,
|
||||
&guild,
|
||||
&threads,
|
||||
acl::has_permission(admin_acls, acl::MESSAGE_DELETE_ALL),
|
||||
threads_enabled,
|
||||
csrf_token,
|
||||
))
|
||||
}
|
||||
"audit_log" | "audit-log" => {
|
||||
if !acl::has_permission(admin_acls, acl::GUILD_AUDIT_LOG_VIEW) {
|
||||
return None;
|
||||
|
||||
@@ -134,6 +134,7 @@ async fn guild_detail(
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let username_sign_in = state.account_identity(&client).await.is_username();
|
||||
let tab_body = if let Some(guild) = guild.as_ref() {
|
||||
guild_tabs::render(
|
||||
&client,
|
||||
@@ -152,6 +153,7 @@ async fn guild_detail(
|
||||
active_tab,
|
||||
&csrf_token,
|
||||
admin_acls,
|
||||
username_sign_in,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
@@ -166,6 +168,7 @@ async fn guild_detail(
|
||||
active_tab,
|
||||
tab_body,
|
||||
is_detail_fragment,
|
||||
username_sign_in,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
@@ -430,6 +433,16 @@ async fn dispatch_guild_action(
|
||||
"Failed to delete sticker",
|
||||
)
|
||||
}
|
||||
"delete_thread" => {
|
||||
let Some(thread_id) = get("thread_id") else {
|
||||
return FlashData::error("Thread ID is required");
|
||||
};
|
||||
action_result(
|
||||
client.delete_thread_channel(&thread_id).await,
|
||||
"Thread deleted",
|
||||
"Failed to delete thread",
|
||||
)
|
||||
}
|
||||
"trigger_archive" => {
|
||||
let inc = form.bool_value("include_attachments");
|
||||
action_result(
|
||||
@@ -508,6 +521,7 @@ async fn guild_tab(
|
||||
normalize_guild_tab(&tab),
|
||||
&csrf_token,
|
||||
admin_acls,
|
||||
state.account_identity(&client).await.is_username(),
|
||||
),
|
||||
None => maud::html! {
|
||||
div class="p-4 text-red-600 text-sm" {
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -201,6 +201,13 @@ async fn bulk_actions_page(
|
||||
csrf: axum::Extension<CsrfToken>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let markup = templates::pages::bulk_actions::bulk_actions_page(config, &auth.0, &csrf.0.0);
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let account_identity = state.account_identity(&client).await;
|
||||
let markup = templates::pages::bulk_actions::bulk_actions_page(
|
||||
config,
|
||||
&auth.0,
|
||||
&csrf.0.0,
|
||||
account_identity.is_username(),
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ pub fn build_router(config: AdminConfig) -> Router {
|
||||
.merge(admin::router())
|
||||
.route("/", get(dashboard))
|
||||
.route("/dashboard", get(dashboard))
|
||||
.layer(from_fn_with_state(
|
||||
state.clone(),
|
||||
middleware::account_identity::scope_account_identity,
|
||||
))
|
||||
.layer(from_fn(middleware::htmx::flash_redirect_to_toast))
|
||||
.layer(from_fn_with_state(
|
||||
state.clone(),
|
||||
|
||||
@@ -7,19 +7,20 @@ use crate::{
|
||||
AppBrandingConfigUpdateRequest, AppLegalConfigUpdateRequest,
|
||||
AppPublicConfigUpdateRequest, AppRegistrationConfigUpdateRequest,
|
||||
AppSetupConfigUpdateRequest, CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE,
|
||||
CaptchaConfigUpdateRequest, CreateRegistrationUrlRequest,
|
||||
DomainMigrationConfigUpdateRequest, EXPERIMENT_MAX_TARGETED_USERS,
|
||||
ExperimentDeliveryConfigUpdateRequest, GatewayRolloutConfigUpdateRequest,
|
||||
GatewayRolloutMode, InstanceAttachmentDecayUpdateRequest,
|
||||
InstanceBlueskyIntegrationUpdateRequest, InstanceBlueskyKeyIntegrationUpdateRequest,
|
||||
InstanceConfigUpdateRequest, InstanceEmailIntegrationUpdateRequest,
|
||||
InstanceEmailSmtpIntegrationUpdateRequest, InstanceEmailSmtpTestRequest,
|
||||
InstanceGifIntegrationUpdateRequest, InstanceIntegrationsUpdateRequest,
|
||||
InstanceMediaUpdateRequest, InstancePolicyUpdateRequest,
|
||||
InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest,
|
||||
InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule,
|
||||
LimitRuleFilters, PremiumMode, PushRelayConfigUpdateRequest, RegistrationMode,
|
||||
SsoConfigUpdateRequest, VoiceE2eeScope,
|
||||
CaptchaConfigUpdateRequest, ChannelThreadsConfigUpdateRequest,
|
||||
CreateRegistrationUrlRequest, DomainMigrationConfigUpdateRequest,
|
||||
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigUpdateRequest,
|
||||
GatewayRolloutConfigUpdateRequest, GatewayRolloutMode,
|
||||
InstanceAttachmentDecayUpdateRequest, InstanceBlueskyIntegrationUpdateRequest,
|
||||
InstanceBlueskyKeyIntegrationUpdateRequest, InstanceConfigUpdateRequest,
|
||||
InstanceEmailIntegrationUpdateRequest, InstanceEmailSmtpIntegrationUpdateRequest,
|
||||
InstanceEmailSmtpTestRequest, InstanceGifIntegrationUpdateRequest,
|
||||
InstanceIntegrationsUpdateRequest, InstanceMediaUpdateRequest,
|
||||
InstancePolicyUpdateRequest, InstanceRegistrationConfigUpdateRequest,
|
||||
InstanceServicesUpdateRequest, InstanceYoutubeIntegrationUpdateRequest,
|
||||
LimitConfigUpdateRequest, LimitRule, LimitRuleFilters,
|
||||
PlutoniumPageConfigUpdateRequest, PremiumMode, PushRelayConfigUpdateRequest,
|
||||
RegistrationMode, SsoConfigUpdateRequest, VoiceE2eeScope,
|
||||
},
|
||||
},
|
||||
config::AdminConfig,
|
||||
@@ -220,10 +221,18 @@ 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),
|
||||
},
|
||||
"update_channel_threads" => match build_channel_threads_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
},
|
||||
"update_experiment_delivery" => match build_experiment_delivery_update(&form) {
|
||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||
Err(message) => FlashData::error(message),
|
||||
@@ -609,6 +618,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 {
|
||||
@@ -632,6 +676,53 @@ fn build_captcha_update(form: &MultiValueForm) -> Result<InstanceConfigUpdateReq
|
||||
})
|
||||
}
|
||||
|
||||
fn build_channel_threads_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
Ok(InstanceConfigUpdateRequest {
|
||||
channel_threads: Some(ChannelThreadsConfigUpdateRequest {
|
||||
enabled: Some(form.bool_value("channel_threads_enabled")),
|
||||
guild_basis_points: parse_form_number(
|
||||
form,
|
||||
"channel_threads_guild_basis_points",
|
||||
"Guild rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
guild_salt: parse_experiment_rollout_salt(form, "channel_threads_guild_salt")?,
|
||||
enabled_guild_ids: Some(parse_experiment_user_ids(
|
||||
form.first("channel_threads_enabled_guild_ids")
|
||||
.unwrap_or_default(),
|
||||
"Enabled guild IDs",
|
||||
)?),
|
||||
disabled_guild_ids: Some(parse_experiment_user_ids(
|
||||
form.first("channel_threads_disabled_guild_ids")
|
||||
.unwrap_or_default(),
|
||||
"Disabled guild IDs",
|
||||
)?),
|
||||
user_basis_points: parse_form_number(
|
||||
form,
|
||||
"channel_threads_user_basis_points",
|
||||
"User rollout basis points",
|
||||
0,
|
||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
||||
)?,
|
||||
user_salt: parse_experiment_rollout_salt(form, "channel_threads_user_salt")?,
|
||||
included_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("channel_threads_included_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Included user IDs",
|
||||
)?),
|
||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
||||
form.first("channel_threads_excluded_user_ids")
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn build_experiment_delivery_update(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
||||
@@ -799,7 +890,9 @@ fn build_integrations_update(form: &MultiValueForm) -> InstanceConfigUpdateReque
|
||||
youtube: Some(InstanceYoutubeIntegrationUpdateRequest {
|
||||
api_key: clean("integration_youtube_api_key"),
|
||||
}),
|
||||
email: Some(InstanceEmailIntegrationUpdateRequest {
|
||||
email: (form.has_key_starting_with("integration_email_")
|
||||
|| form.has_key_starting_with("integration_smtp_"))
|
||||
.then(|| InstanceEmailIntegrationUpdateRequest {
|
||||
enabled: Some(form.bool_value("integration_email_enabled")),
|
||||
provider: Some("smtp".to_owned()),
|
||||
from_email: clean("integration_email_from_email"),
|
||||
@@ -1156,6 +1249,37 @@ pub async fn limit_config_post(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_integrations_update_leaves_email_alone_when_its_fields_are_hidden() {
|
||||
let hidden = build_integrations_update(&MultiValueForm::parse(
|
||||
b"integration_klipy_api_key=&integration_youtube_api_key=",
|
||||
));
|
||||
let integrations = hidden.integrations.expect("integrations update");
|
||||
assert!(integrations.email.is_none());
|
||||
assert!(integrations.gif.is_some());
|
||||
|
||||
let shown = build_integrations_update(&MultiValueForm::parse(
|
||||
b"integration_email_present=1&integration_smtp_host=smtp.example.com",
|
||||
));
|
||||
let email = shown
|
||||
.integrations
|
||||
.and_then(|integrations| integrations.email)
|
||||
.expect("email update");
|
||||
assert_eq!(email.enabled, Some(false));
|
||||
|
||||
let from_an_older_page = build_integrations_update(&MultiValueForm::parse(
|
||||
b"integration_klipy_api_key=&integration_smtp_host=smtp.example.com",
|
||||
));
|
||||
let email = from_an_older_page
|
||||
.integrations
|
||||
.and_then(|integrations| integrations.email)
|
||||
.expect("email update from a page without the presence marker");
|
||||
assert_eq!(
|
||||
email.smtp.and_then(|smtp| smtp.host).as_deref(),
|
||||
Some("smtp.example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_sso_update_keeps_repeated_allowed_domains() {
|
||||
let form = MultiValueForm::parse(
|
||||
@@ -1364,6 +1488,111 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_channel_threads_update_reads_both_rollout_dimensions() {
|
||||
let form = MultiValueForm::parse(
|
||||
b"channel_threads_enabled=true&channel_threads_guild_basis_points=%2010%20&channel_threads_guild_salt=%20channel-threads-guild-v2%20&channel_threads_enabled_guild_ids=1600000000000000001%0A1600000000000000002%0A1600000000000000001&channel_threads_disabled_guild_ids=1600000000000000003&channel_threads_user_basis_points=10000&channel_threads_user_salt=channel-threads-user-v2&channel_threads_included_user_ids=1500000000000000001&channel_threads_excluded_user_ids=1500000000000000003%2C%201500000000000000004",
|
||||
);
|
||||
let update = build_channel_threads_update(&form)
|
||||
.expect("valid form")
|
||||
.channel_threads
|
||||
.expect("channel threads update");
|
||||
assert_eq!(update.enabled, Some(true));
|
||||
assert_eq!(update.guild_basis_points, Some(10));
|
||||
assert_eq!(
|
||||
update.guild_salt,
|
||||
Some("channel-threads-guild-v2".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
update.enabled_guild_ids,
|
||||
Some(vec![
|
||||
"1600000000000000001".to_owned(),
|
||||
"1600000000000000002".to_owned()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
update.disabled_guild_ids,
|
||||
Some(vec!["1600000000000000003".to_owned()])
|
||||
);
|
||||
assert_eq!(update.user_basis_points, Some(10000));
|
||||
assert_eq!(update.user_salt, Some("channel-threads-user-v2".to_owned()));
|
||||
assert_eq!(
|
||||
update.included_user_ids,
|
||||
Some(vec!["1500000000000000001".to_owned()])
|
||||
);
|
||||
assert_eq!(
|
||||
update.excluded_user_ids,
|
||||
Some(vec![
|
||||
"1500000000000000003".to_owned(),
|
||||
"1500000000000000004".to_owned()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_channel_threads_update_leaves_the_experiment_off_when_nothing_is_submitted() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
let request = build_channel_threads_update(&form).expect("valid form");
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).expect("serializable update"),
|
||||
serde_json::json!({"channel_threads": {
|
||||
"enabled": false,
|
||||
"enabled_guild_ids": [],
|
||||
"disabled_guild_ids": [],
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_channel_threads_update_rejects_invalid_targeting() {
|
||||
let too_many_guilds = (0..=EXPERIMENT_MAX_TARGETED_USERS)
|
||||
.map(|index| index.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("%2C");
|
||||
for (form, message) in [
|
||||
(
|
||||
"channel_threads_guild_basis_points=10001".to_owned(),
|
||||
"Guild rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"channel_threads_user_basis_points=-1".to_owned(),
|
||||
"User rollout basis points must be a whole number between 0 and 10000",
|
||||
),
|
||||
(
|
||||
"channel_threads_guild_salt=%20%20".to_owned(),
|
||||
"Rollout salt must be between 1 and 64 characters",
|
||||
),
|
||||
(
|
||||
"channel_threads_user_salt=caf%C3%A9".to_owned(),
|
||||
"Rollout salt must use printable ASCII",
|
||||
),
|
||||
(
|
||||
"channel_threads_enabled_guild_ids=123%2Cinvalid".to_owned(),
|
||||
"Enabled guild IDs entry 2 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
"channel_threads_disabled_guild_ids=123456789012345678901".to_owned(),
|
||||
"Disabled guild IDs entry 1 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
"channel_threads_excluded_user_ids=abc".to_owned(),
|
||||
"Excluded user IDs entry 1 must contain 1 to 20 decimal digits",
|
||||
),
|
||||
(
|
||||
format!("channel_threads_enabled_guild_ids={too_many_guilds}"),
|
||||
"Enabled guild IDs must contain at most 1000 unique IDs",
|
||||
),
|
||||
] {
|
||||
let form = MultiValueForm::parse(form.as_bytes());
|
||||
assert_eq!(
|
||||
build_channel_threads_update(&form).expect_err("invalid targeting"),
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_push_relay_update_reads_the_consent_checkbox() {
|
||||
let unchecked = build_push_relay_update(&MultiValueForm::parse(b"_csrf=token"));
|
||||
@@ -1444,6 +1673,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",
|
||||
@@ -199,22 +178,6 @@ pub async fn dispatch(
|
||||
"Failed to clear user fields",
|
||||
)
|
||||
}
|
||||
"set_bot_status" => {
|
||||
let val = form.bool_value("bot");
|
||||
DispatchOutcome::from_result(
|
||||
client.set_bot_status(user_id, val).await,
|
||||
"Bot status updated successfully",
|
||||
"Failed to update bot status",
|
||||
)
|
||||
}
|
||||
"set_system_status" => {
|
||||
let val = form.bool_value("system");
|
||||
DispatchOutcome::from_result(
|
||||
client.set_system_status(user_id, val).await,
|
||||
"System status updated successfully",
|
||||
"Failed to update system status",
|
||||
)
|
||||
}
|
||||
"change_username" => {
|
||||
let Some(username) = get("username") else {
|
||||
return DispatchOutcome::error("Username is required");
|
||||
@@ -280,8 +243,11 @@ pub async fn dispatch(
|
||||
let Some(ip) = get("ip") else {
|
||||
return DispatchOutcome::error("IP address is required");
|
||||
};
|
||||
let Ok(duration) = form.parse_value::<u32>("duration_hours") else {
|
||||
return DispatchOutcome::error("Invalid ban duration");
|
||||
};
|
||||
DispatchOutcome::from_result(
|
||||
client.ban_ip(&ip, None).await,
|
||||
client.ban_ip(&ip, duration.unwrap_or(0), None).await,
|
||||
"IP banned successfully",
|
||||
"Failed to ban IP",
|
||||
)
|
||||
@@ -401,6 +367,11 @@ pub async fn dispatch(
|
||||
"Password reset sent successfully",
|
||||
"Failed to send password reset",
|
||||
),
|
||||
"revoke_recovery_kit" => DispatchOutcome::from_result(
|
||||
client.revoke_recovery_kit(user_id).await,
|
||||
"Recovery kit revoked",
|
||||
"Failed to revoke recovery kit",
|
||||
),
|
||||
"remove_relationship" => {
|
||||
let Some(target_id) = get("target_user_id").or_else(|| get("target_id")) else {
|
||||
return DispatchOutcome::error("Target user ID is required");
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
api::{
|
||||
audit::SearchAuditLogsParams,
|
||||
client::{AdminApiClient, ApiResultExt},
|
||||
types::AccountIdentityMode,
|
||||
},
|
||||
config::AdminConfig,
|
||||
templates::{
|
||||
@@ -26,6 +27,7 @@ pub struct TabQuery {
|
||||
pub delete_all_messages_message_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn render(
|
||||
client: &AdminApiClient,
|
||||
config: &AdminConfig,
|
||||
@@ -34,6 +36,7 @@ pub async fn render(
|
||||
tab: &str,
|
||||
query: &TabQuery,
|
||||
admin_acls: &[String],
|
||||
account_identity: AccountIdentityMode,
|
||||
) -> Option<maud::Markup> {
|
||||
match tab {
|
||||
"overview" => {
|
||||
@@ -60,31 +63,22 @@ pub async fn render(
|
||||
csrf_token,
|
||||
change_log.as_ref(),
|
||||
limit_config.as_ref(),
|
||||
account_identity.is_username(),
|
||||
))
|
||||
}
|
||||
"account" => {
|
||||
let u = client
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.log_error("load user account")?;
|
||||
let s = client
|
||||
.list_user_sessions(user_id)
|
||||
.await
|
||||
.map(|r| r.sessions)
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list user sessions"))
|
||||
.unwrap_or_default();
|
||||
let webauthn_credentials = client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default();
|
||||
Some(tabs::account::account_tab(
|
||||
render_account(
|
||||
client,
|
||||
config,
|
||||
&u,
|
||||
&s,
|
||||
&webauthn_credentials,
|
||||
csrf_token,
|
||||
))
|
||||
user_id,
|
||||
&tabs::account::AccountTabOptions {
|
||||
admin_acls,
|
||||
account_identity,
|
||||
password_reset_link: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
"moderation" => {
|
||||
let u = client
|
||||
@@ -145,6 +139,7 @@ pub async fn render(
|
||||
let context = tabs::moderation::ModerationContext {
|
||||
deletion_scheduler: deletion_scheduler.as_ref(),
|
||||
current_ban: tabs::moderation::find_current_ban(&u, &ban_logs),
|
||||
username_sign_in: account_identity.is_username(),
|
||||
};
|
||||
Some(tabs::moderation::moderation_tab(
|
||||
config,
|
||||
@@ -298,6 +293,40 @@ pub async fn render(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn render_account(
|
||||
client: &AdminApiClient,
|
||||
config: &AdminConfig,
|
||||
csrf_token: &str,
|
||||
user_id: &str,
|
||||
options: &tabs::account::AccountTabOptions<'_>,
|
||||
) -> Option<maud::Markup> {
|
||||
let u = client
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.log_error("load user account")?;
|
||||
let s = client
|
||||
.list_user_sessions(user_id)
|
||||
.await
|
||||
.map(|r| r.sessions)
|
||||
.map_err(
|
||||
|error| tracing::warn!(%error, user_id, "admin API request failed: list user sessions"),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let webauthn_credentials = client
|
||||
.list_webauthn_credentials(user_id)
|
||||
.await
|
||||
.map_err(|error| tracing::warn!(%error, user_id, "admin API request failed: list webauthn credentials"))
|
||||
.unwrap_or_default();
|
||||
Some(tabs::account::account_tab(
|
||||
config,
|
||||
&u,
|
||||
&s,
|
||||
&webauthn_credentials,
|
||||
csrf_token,
|
||||
options,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_bool_flag(value: &str) -> Option<bool> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"1" | "true" => Some(true),
|
||||
|
||||
@@ -9,7 +9,12 @@ use crate::{
|
||||
middleware::{auth::AuthContext, csrf::CsrfToken, flash, htmx},
|
||||
routes::user_tabs,
|
||||
state::AppState,
|
||||
templates,
|
||||
templates::{
|
||||
self,
|
||||
pages::user_detail_tabs::account::{
|
||||
PASSWORD_RESET_LINK_RESULT_ID, password_reset_link_result,
|
||||
},
|
||||
},
|
||||
utils::forms::MultiValueForm,
|
||||
};
|
||||
use axum::{
|
||||
@@ -86,8 +91,9 @@ async fn users_list(
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let can_view_email = acl::has_permission(admin_acls, acl::USER_VIEW_EMAIL);
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let username_sign_in = state.account_identity(&client).await.is_username();
|
||||
let can_view_email = acl::has_permission(admin_acls, acl::USER_VIEW_EMAIL) && !username_sign_in;
|
||||
let searching = params.has_id_lookup() || params.has_search();
|
||||
let results = async {
|
||||
if params.has_id_lookup() {
|
||||
@@ -136,6 +142,7 @@ async fn users_list(
|
||||
result_users,
|
||||
has_more,
|
||||
can_view_email,
|
||||
username_sign_in,
|
||||
premium_badge_name.as_deref(),
|
||||
is_results_fragment,
|
||||
);
|
||||
@@ -204,8 +211,16 @@ async fn user_detail(
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let tab_body = if user.is_some() {
|
||||
let account_identity = state.account_identity(&client).await;
|
||||
user_tabs::render(
|
||||
&client, config, &csrf.0.0, &user_id, active_tab, &tq, admin_acls,
|
||||
&client,
|
||||
config,
|
||||
&csrf.0.0,
|
||||
&user_id,
|
||||
active_tab,
|
||||
&tq,
|
||||
admin_acls,
|
||||
account_identity,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
@@ -229,6 +244,7 @@ async fn user_detail_post(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
csrf: axum::Extension<CsrfToken>,
|
||||
Path(user_id): Path<String>,
|
||||
Query(aq): Query<ActionQuery>,
|
||||
request: Request,
|
||||
@@ -252,6 +268,10 @@ async fn user_detail_post(
|
||||
};
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let action = aq.action.as_deref().unwrap_or("");
|
||||
if action == "create_password_reset_link" {
|
||||
return create_password_reset_link(&state, &headers, &auth.0, &csrf.0.0, &client, &user_id)
|
||||
.await;
|
||||
}
|
||||
let outcome = super::user_actions::dispatch(&client, &user_id, action, &form).await;
|
||||
let mut redirect = if tab.is_empty() {
|
||||
format!("{base}/users/{user_id}")
|
||||
@@ -268,6 +288,74 @@ async fn user_detail_post(
|
||||
flash::redirect_with_flash(&redirect, outcome.flash, config.secure_cookies())
|
||||
}
|
||||
|
||||
async fn create_password_reset_link(
|
||||
state: &AppState,
|
||||
headers: &HeaderMap,
|
||||
auth: &AuthContext,
|
||||
csrf_token: &str,
|
||||
client: &AdminApiClient,
|
||||
user_id: &str,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let account_url = format!("{}/users/{user_id}?tab=account", config.base_path);
|
||||
let link = match client.create_password_reset_link(user_id).await {
|
||||
Ok(link) => link,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: create password reset link");
|
||||
let flash = flash::FlashData::error("Failed to create password reset link");
|
||||
if htmx::is_htmx_request(headers)
|
||||
&& (htmx::targets(headers, "flash-container")
|
||||
|| htmx::targets(headers, PASSWORD_RESET_LINK_RESULT_ID))
|
||||
{
|
||||
return htmx::toast_response(&flash);
|
||||
}
|
||||
return flash::redirect_with_flash(&account_url, flash, config.secure_cookies());
|
||||
}
|
||||
};
|
||||
if htmx::is_htmx_request(headers) && htmx::targets(headers, PASSWORD_RESET_LINK_RESULT_ID) {
|
||||
return Html(password_reset_link_result(Some(&link)).into_string()).into_response();
|
||||
}
|
||||
let admin_acls = auth
|
||||
.admin_user
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let (user, badge_name, account_identity) = tokio::join!(
|
||||
async {
|
||||
client
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.log_error("load user after creating password reset link")
|
||||
},
|
||||
self_hosted_premium_badge_name(state, client),
|
||||
state.account_identity(client)
|
||||
);
|
||||
let tab_body = user_tabs::render_account(
|
||||
client,
|
||||
config,
|
||||
csrf_token,
|
||||
user_id,
|
||||
&templates::pages::user_detail_tabs::account::AccountTabOptions {
|
||||
admin_acls,
|
||||
account_identity,
|
||||
password_reset_link: Some(&link),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let premium_badge_name = user.as_ref().and(badge_name);
|
||||
let markup = templates::pages::user_detail::user_detail_with_tab(
|
||||
config,
|
||||
auth,
|
||||
user.as_ref(),
|
||||
user_id,
|
||||
"account",
|
||||
tab_body,
|
||||
premium_badge_name.as_deref(),
|
||||
htmx::targets(headers, "main-content"),
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn user_tab(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
@@ -299,14 +387,20 @@ async fn user_tab(
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let account_identity = state.account_identity(&client).await;
|
||||
let markup = match user {
|
||||
Some(ref u) => {
|
||||
user_tabs::render(&client, config, &csrf.0.0, &user_id, &tab, &tq, admin_acls)
|
||||
.await
|
||||
.unwrap_or_else(|| {
|
||||
templates::pages::user_detail::simple_tab_content(config, u, &tab)
|
||||
})
|
||||
}
|
||||
Some(ref u) => user_tabs::render(
|
||||
&client,
|
||||
config,
|
||||
&csrf.0.0,
|
||||
&user_id,
|
||||
&tab,
|
||||
&tq,
|
||||
admin_acls,
|
||||
account_identity,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| templates::pages::user_detail::simple_tab_content(config, u, &tab)),
|
||||
None => maud::html! {
|
||||
div class="p-4 text-red-600 text-sm" { "Failed to load user data." }
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::{
|
||||
api::{
|
||||
client::{AdminApiClient, ApiResultExt},
|
||||
types::PremiumBranding,
|
||||
types::{AccountIdentityMode, AccountIdentitySettings, PremiumBranding},
|
||||
},
|
||||
config::AdminConfig,
|
||||
};
|
||||
@@ -13,6 +13,8 @@ use std::{
|
||||
};
|
||||
|
||||
const PREMIUM_BRANDING_TTL: Duration = Duration::from_secs(60);
|
||||
const ACCOUNT_IDENTITY_TTL: Duration = Duration::from_secs(60);
|
||||
const ACCOUNT_IDENTITY_RETRY_TTL: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
@@ -23,6 +25,7 @@ struct AppStateInner {
|
||||
pub config: AdminConfig,
|
||||
pub http_client: reqwest::Client,
|
||||
premium_branding: Mutex<Option<(Instant, PremiumBranding)>>,
|
||||
account_identity: Mutex<Option<(Instant, AccountIdentitySettings)>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -36,6 +39,7 @@ impl AppState {
|
||||
config,
|
||||
http_client,
|
||||
premium_branding: Mutex::new(None),
|
||||
account_identity: Mutex::new(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -81,6 +85,47 @@ impl AppState {
|
||||
self.remember_premium_branding(branding.clone());
|
||||
Some(branding)
|
||||
}
|
||||
|
||||
pub async fn account_identity(&self, client: &AdminApiClient) -> AccountIdentityMode {
|
||||
self.account_identity_settings(client).await.mode
|
||||
}
|
||||
|
||||
pub async fn account_identity_settings(
|
||||
&self,
|
||||
client: &AdminApiClient,
|
||||
) -> AccountIdentitySettings {
|
||||
if !self.config().self_hosted {
|
||||
return AccountIdentitySettings::default();
|
||||
}
|
||||
let previous = *self
|
||||
.inner
|
||||
.account_identity
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if let Some((expires_at, settings)) = previous
|
||||
&& Instant::now() < expires_at
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
let (settings, ttl) = match client
|
||||
.get_instance_account_identity()
|
||||
.await
|
||||
.log_error("load account identity mode")
|
||||
{
|
||||
Some(settings) => (settings, ACCOUNT_IDENTITY_TTL),
|
||||
None => (
|
||||
previous.map_or(AccountIdentitySettings::default(), |(_, settings)| settings),
|
||||
ACCOUNT_IDENTITY_RETRY_TTL,
|
||||
),
|
||||
};
|
||||
*self
|
||||
.inner
|
||||
.account_identity
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
|
||||
Some((Instant::now() + ttl, settings));
|
||||
settings
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<AppState> for AdminConfig {
|
||||
|
||||
@@ -198,6 +198,7 @@ fn message_row(
|
||||
msg.author_global_name.as_deref(),
|
||||
Some(&msg.author_username),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let row_class = format!(
|
||||
"group relative mt-4 py-0.5 pr-4 pl-4 transition-colors first:mt-0{hover}{highlight}"
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::utils::user_tag::user_tag;
|
||||
|
||||
pub fn format_user_display(
|
||||
global_name: Option<&str>,
|
||||
username: Option<&str>,
|
||||
discriminator: Option<&str>,
|
||||
is_bot: bool,
|
||||
) -> String {
|
||||
match (global_name, username, discriminator) {
|
||||
(Some(gn), Some(un), Some("0")) => format!("{gn} (@{un})"),
|
||||
(Some(gn), _, _) => gn.to_owned(),
|
||||
(None, Some(un), Some(d)) if d != "0" => format!("{un}#{d}"),
|
||||
(None, Some(un), Some(d)) if d != "0" => user_tag(un, d, is_bot),
|
||||
(None, Some(un), _) => format!("@{un}"),
|
||||
_ => "Unknown".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::user_tag::sync_with_unique_usernames;
|
||||
|
||||
#[test]
|
||||
fn username_instances_show_bare_human_names_and_keep_bot_tags() {
|
||||
sync_with_unique_usernames(true, || {
|
||||
assert_eq!(
|
||||
format_user_display(None, Some("alice"), Some("0000"), false),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
format_user_display(None, Some("helper"), Some("4363"), true),
|
||||
"helper#4363"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_instances_keep_the_zero_tag() {
|
||||
assert_eq!(
|
||||
format_user_display(None, Some("alice"), Some("0000"), false),
|
||||
"alice#0000"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
config::AdminConfig, middleware::auth::AuthContext,
|
||||
templates::components::media::user_avatar_url, utils::bigint::format_discriminator,
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
templates::components::media::user_avatar_url,
|
||||
utils::{bigint::format_discriminator, user_tag::user_tag},
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
@@ -32,7 +34,7 @@ pub fn render_header(config: &AdminConfig, auth: &AuthContext, csrf_token: &str)
|
||||
(display)
|
||||
}
|
||||
div class="truncate text-neutral-500 text-xs" {
|
||||
(admin.username) "#" (format_discriminator(&admin.discriminator))
|
||||
(user_tag(&admin.username, &format_discriminator(&admin.discriminator), admin.bot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -114,6 +114,7 @@ fn overview_card(config: &AdminConfig, app: &Application, can_list_by_owner: boo
|
||||
app.owner_global_name.as_deref(),
|
||||
app.owner_username.as_deref(),
|
||||
app.owner_discriminator.as_deref(),
|
||||
false,
|
||||
);
|
||||
section_card_simple(
|
||||
"Overview",
|
||||
@@ -157,6 +158,7 @@ fn bot_display_markup(config: &AdminConfig, app: &Application) -> Markup {
|
||||
app.bot_global_name.as_deref(),
|
||||
app.bot_username.as_deref(),
|
||||
app.bot_discriminator.as_deref(),
|
||||
true,
|
||||
);
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
|
||||
@@ -189,7 +189,12 @@ fn render_application_card(config: &AdminConfig, base: &str, app: &Application)
|
||||
|
||||
fn format_owner_display(app: &Application) -> String {
|
||||
if let (Some(un), Some(disc)) = (&app.owner_username, &app.owner_discriminator) {
|
||||
format_user_display(app.owner_global_name.as_deref(), Some(un), Some(disc))
|
||||
format_user_display(
|
||||
app.owner_global_name.as_deref(),
|
||||
Some(un),
|
||||
Some(disc),
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
app.owner_user_id.clone()
|
||||
}
|
||||
@@ -199,7 +204,7 @@ fn format_bot_display(app: &Application) -> String {
|
||||
if let (Some(_bid), Some(un), Some(disc)) =
|
||||
(&app.bot_user_id, &app.bot_username, &app.bot_discriminator)
|
||||
{
|
||||
format_user_display(app.bot_global_name.as_deref(), Some(un), Some(disc))
|
||||
format_user_display(app.bot_global_name.as_deref(), Some(un), Some(disc), true)
|
||||
} else {
|
||||
app.bot_user_id.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
resource_link::{ResourceType, resource_link},
|
||||
table::{table_body, table_cell, table_head, table_header_cell, table_row},
|
||||
},
|
||||
utils::bigint::format_discriminator,
|
||||
utils::{bigint::format_discriminator, user_tag::user_tag},
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
@@ -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
|
||||
@@ -46,10 +42,10 @@ fn type_label(target_type: &str) -> String {
|
||||
}
|
||||
|
||||
fn user_label(user: &AuditLogUserSummary) -> String {
|
||||
let tag = format!(
|
||||
"{}#{}",
|
||||
user.username,
|
||||
format_discriminator(&user.discriminator)
|
||||
let tag = user_tag(
|
||||
&user.username,
|
||||
&format_discriminator(&user.discriminator),
|
||||
false,
|
||||
);
|
||||
match user
|
||||
.global_name
|
||||
@@ -355,10 +351,32 @@ mod tests {
|
||||
assert!(!markup.contains("/admin/users/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_labels_drop_the_zero_tag_only_without_tags() {
|
||||
let admin = AuditLogUserSummary {
|
||||
id: "1500000000000000001".to_owned(),
|
||||
username: "lilith".to_owned(),
|
||||
discriminator: "0".to_owned(),
|
||||
global_name: Some("Lilith".to_owned()),
|
||||
};
|
||||
assert_eq!(user_label(&admin), "Lilith (lilith#0000)");
|
||||
crate::utils::user_tag::sync_with_unique_usernames(true, || {
|
||||
assert_eq!(user_label(&admin), "Lilith (lilith)");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_target_types_stay_unlinked() {
|
||||
let markup = target_cell("/admin", &entry("email_domain", "spam.example")).into_string();
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
templates::{
|
||||
components::{form::csrf_input, page_container::page_header},
|
||||
components::{alert::alert_info, form::csrf_input, page_container::page_header},
|
||||
layout::admin_layout,
|
||||
},
|
||||
};
|
||||
@@ -20,6 +20,24 @@ pub struct BanConfig {
|
||||
pub entity_name: &'static str,
|
||||
pub active_page: &'static str,
|
||||
pub show_bulk_tools: bool,
|
||||
pub show_duration: bool,
|
||||
}
|
||||
|
||||
const IP_BAN_DURATIONS: &[(u32, &str)] = &[
|
||||
(24, "1 day"),
|
||||
(168, "7 days"),
|
||||
(720, "30 days"),
|
||||
(0, "Permanent"),
|
||||
];
|
||||
|
||||
pub fn ip_ban_duration_label(hours: u32) -> String {
|
||||
IP_BAN_DURATIONS
|
||||
.iter()
|
||||
.find(|(value, _)| *value == hours)
|
||||
.map_or_else(
|
||||
|| format!("{hours} hours"),
|
||||
|(_, label)| (*label).to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
@@ -33,17 +51,19 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "IP/CIDR",
|
||||
active_page: "ip-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: true,
|
||||
},
|
||||
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,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Phrase Bans",
|
||||
@@ -55,6 +75,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "Phrase",
|
||||
active_page: "phrase-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "URL Blocklist",
|
||||
@@ -66,6 +87,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "URL",
|
||||
active_page: "url-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "File SHA Blocklist",
|
||||
@@ -77,6 +99,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "SHA-256",
|
||||
active_page: "file-sha-bans",
|
||||
show_bulk_tools: true,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Avatar Hash Blocklist",
|
||||
@@ -88,6 +111,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "Avatar Hash",
|
||||
active_page: "avatar-hash-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "URL Domain Blocklist",
|
||||
@@ -99,6 +123,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "Domain",
|
||||
active_page: "url-domain-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Profile Substring Blocklist",
|
||||
@@ -110,6 +135,7 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
entity_name: "Substring",
|
||||
active_page: "profile-substring-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -123,10 +149,16 @@ pub fn bans_page(
|
||||
ban_cfg: &BanConfig,
|
||||
flash: Option<&crate::api::types::FlashMessage>,
|
||||
csrf_token: &str,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let content = html! {
|
||||
(page_header(ban_cfg.title, None))
|
||||
@if username_sign_in && ban_cfg.active_page == "email-bans" {
|
||||
div class="mb-6" {
|
||||
(alert_info(html! { "People sign in with a username on this instance. Accounts have no email address, so email bans have no effect." }))
|
||||
}
|
||||
}
|
||||
div class="grid gap-6 lg:grid-cols-2" {
|
||||
(ban_card(base, ban_cfg, csrf_token))
|
||||
(check_ban_card(base, ban_cfg, csrf_token))
|
||||
@@ -163,6 +195,9 @@ fn ban_card(base: &str, cfg: &BanConfig, csrf_token: &str) -> Markup {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(form_field(cfg.input_name, cfg.input_label, cfg.input_type, cfg.placeholder, true))
|
||||
@if cfg.show_duration {
|
||||
(duration_field())
|
||||
}
|
||||
(form_field("audit_log_reason", "Private reason (audit log, optional)", "text", "Why is this ban being applied?", false))
|
||||
(submit_btn("Ban", cfg.entity_name, false))
|
||||
}
|
||||
@@ -394,6 +429,24 @@ fn form_field(
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_field() -> Markup {
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
label for="duration_hours" class="block text-sm font-medium text-neutral-700" {
|
||||
"Duration"
|
||||
}
|
||||
select id="duration_hours" name="duration_hours"
|
||||
class="block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm \
|
||||
shadow-sm focus:border-brand-primary focus:outline-none focus:ring-1 \
|
||||
focus:ring-brand-primary" {
|
||||
@for &(value, label) in IP_BAN_DURATIONS {
|
||||
option value=(value) { (label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn textarea_field(name: &str, label: &str, required: bool) -> Markup {
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
|
||||
@@ -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"];
|
||||
@@ -191,7 +177,12 @@ fn guild_feature_label(feature: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &str) -> Markup {
|
||||
pub fn bulk_actions_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
csrf_token: &str,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let admin_acls = auth
|
||||
.admin_user
|
||||
@@ -205,9 +196,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))
|
||||
}
|
||||
@@ -215,7 +203,7 @@ pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &
|
||||
(bulk_add_guild_members_section(base, csrf_token))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_DELETE_USERS) {
|
||||
(bulk_schedule_deletion_section(base, csrf_token))
|
||||
(bulk_schedule_deletion_section(base, csrf_token, username_sign_in))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_DELETE_USER_MESSAGES) {
|
||||
(bulk_delete_user_messages_section(base, csrf_token))
|
||||
@@ -225,16 +213,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 +265,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",
|
||||
@@ -388,7 +336,7 @@ fn bulk_add_guild_members_section(base: &str, csrf_token: &str) -> Markup {
|
||||
)
|
||||
}
|
||||
|
||||
fn bulk_schedule_deletion_section(base: &str, csrf_token: &str) -> Markup {
|
||||
fn bulk_schedule_deletion_section(base: &str, csrf_token: &str, username_sign_in: bool) -> Markup {
|
||||
section_card_simple(
|
||||
"Bulk Schedule User Deletion",
|
||||
html! {
|
||||
@@ -427,7 +375,11 @@ fn bulk_schedule_deletion_section(base: &str, csrf_token: &str) -> Markup {
|
||||
},
|
||||
))
|
||||
(text_input("audit_log_reason", "Audit Log Reason (optional)", "", "Reason for this bulk operation"))
|
||||
(opt_out_checkbox("notify_user", "Email each user about the scheduled deletion"))
|
||||
@if username_sign_in {
|
||||
input type="hidden" name="notify_user_present" value="1";
|
||||
} @else {
|
||||
(opt_out_checkbox("notify_user", "Email each user about the scheduled deletion"))
|
||||
}
|
||||
(form_actions(html! {
|
||||
(danger_button("Schedule Deletion"))
|
||||
}))
|
||||
@@ -473,7 +425,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deletion_form_has_no_preselected_reason() {
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf").into_string();
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf", false).into_string();
|
||||
assert!(markup.contains(r#"<option value="" selected>Select a reason</option>"#));
|
||||
for (value, _) in DELETION_REASONS {
|
||||
assert!(!markup.contains(&format!(r#"<option value="{value}" selected>"#)));
|
||||
@@ -482,17 +434,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deletion_form_defaults_to_the_moderation_retention_floor() {
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf").into_string();
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf", false).into_string();
|
||||
assert!(markup.contains(r#"name="days_until_deletion" value="60" min="14" max="365""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deletion_form_emails_each_user_by_default() {
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf").into_string();
|
||||
let markup = bulk_schedule_deletion_section("/admin", "csrf", false).into_string();
|
||||
assert!(markup.contains(r#"name="notify_user" value="true" checked"#));
|
||||
assert!(markup.contains(r#"name="notify_user_present" value="1""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_mode_hides_the_email_choices() {
|
||||
let deletion = bulk_schedule_deletion_section("/admin", "csrf", true).into_string();
|
||||
assert!(!deletion.contains("Email each user"));
|
||||
assert!(!deletion.contains(r#"name="notify_user" value="true""#));
|
||||
assert!(deletion.contains(r#"name="notify_user_present" value="1""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_grid_can_clear_the_deprecated_clone_features() {
|
||||
let markup = guild_feature_checkbox_grid("remove_features[]", true).into_string();
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
},
|
||||
utils::bigint::format_discriminator,
|
||||
utils::timestamps::format_admin_timestamp,
|
||||
utils::user_tag::user_tag,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
@@ -74,7 +75,7 @@ fn owner_display(
|
||||
let Some(discriminator) = discriminator else {
|
||||
return owner_id.to_owned();
|
||||
};
|
||||
let tag = format!("{username}#{}", format_discriminator(discriminator));
|
||||
let tag = user_tag(username, &format_discriminator(discriminator), false);
|
||||
match global_name.filter(|value| !value.trim().is_empty()) {
|
||||
Some(global_name) => format!("{global_name} ({tag})"),
|
||||
None => tag,
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -26,11 +26,13 @@ pub const GUILD_TABS: &[(&str, &str)] = &[
|
||||
("archives", "Archives"),
|
||||
("emojis", "Emojis"),
|
||||
("stickers", "Stickers"),
|
||||
("threads", "Threads"),
|
||||
("audit_logs", "Admin Audit Logs"),
|
||||
("audit_log", "Guild Audit Log"),
|
||||
("reports", "Reports"),
|
||||
];
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn guild_detail_with_tab(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
@@ -39,9 +41,12 @@ pub fn guild_detail_with_tab(
|
||||
active_tab: &str,
|
||||
tab_body: Option<Markup>,
|
||||
is_htmx: bool,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let content = match guild {
|
||||
Some(guild) => render_guild_detail(config, auth, guild, active_tab, tab_body),
|
||||
Some(guild) => {
|
||||
render_guild_detail(config, auth, guild, active_tab, tab_body, username_sign_in)
|
||||
}
|
||||
None => not_found_state("Guild", guild_id, None, None),
|
||||
};
|
||||
let title = if guild.is_some() {
|
||||
@@ -62,6 +67,7 @@ pub fn simple_tab_content(
|
||||
tab: &str,
|
||||
csrf_token: &str,
|
||||
admin_acls: &[String],
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let guild_info = GuildInfo::from(guild.clone());
|
||||
match tab {
|
||||
@@ -69,9 +75,13 @@ pub fn simple_tab_content(
|
||||
"features" => {
|
||||
guild_detail_tabs::features::features_tab(config, &guild_info, csrf_token, admin_acls)
|
||||
}
|
||||
"settings" => {
|
||||
guild_detail_tabs::settings::settings_tab(config, guild, csrf_token, admin_acls)
|
||||
}
|
||||
"settings" => guild_detail_tabs::settings::settings_tab(
|
||||
config,
|
||||
guild,
|
||||
csrf_token,
|
||||
admin_acls,
|
||||
username_sign_in,
|
||||
),
|
||||
"moderation" => guild_detail_tabs::moderation::moderation_tab(
|
||||
config,
|
||||
&guild_info,
|
||||
@@ -92,6 +102,7 @@ fn render_guild_detail(
|
||||
guild: &GuildDetailInfo,
|
||||
active_tab: &str,
|
||||
tab_body: Option<Markup>,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let admin_acls = auth
|
||||
@@ -111,8 +122,16 @@ fn render_guild_detail(
|
||||
effective_tab,
|
||||
|tab_id| guild_tab_visible(config, tab_id, admin_acls),
|
||||
);
|
||||
let body = tab_body
|
||||
.unwrap_or_else(|| simple_tab_content(config, guild, effective_tab, "", admin_acls));
|
||||
let body = tab_body.unwrap_or_else(|| {
|
||||
simple_tab_content(
|
||||
config,
|
||||
guild,
|
||||
effective_tab,
|
||||
"",
|
||||
admin_acls,
|
||||
username_sign_in,
|
||||
)
|
||||
});
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
a href={(base) "/guilds"}
|
||||
@@ -190,6 +209,7 @@ fn guild_tab_visible(_config: &AdminConfig, tab_id: &str, admin_acls: &[String])
|
||||
"overview" | "members" | "settings" | "features" | "moderation" => true,
|
||||
"reports" => acl::has_permission(admin_acls, acl::REPORT_VIEW),
|
||||
"emojis" | "stickers" => acl::has_permission(admin_acls, acl::ASSET_PURGE),
|
||||
"threads" => acl::has_permission(admin_acls, acl::GUILD_LOOKUP),
|
||||
"audit_logs" => acl::has_permission(admin_acls, acl::AUDIT_LOG_VIEW),
|
||||
"audit_log" => acl::has_permission(admin_acls, acl::GUILD_AUDIT_LOG_VIEW),
|
||||
"archives" => acl::has_any_permission(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user