Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a221159136
|
|||
|
7af185f4d8
|
|||
|
afa59c7edb
|
|||
|
ec87215b95
|
|||
|
672249dcac
|
|||
|
cb2f89e80c
|
|||
|
43dc59bff7
|
|||
|
001eb4edda
|
|||
|
b2993acedb
|
|||
|
84c5ac2024
|
|||
|
f3e74d6dee
|
|||
|
a5ce9ed3da
|
|||
|
0e9f35969e
|
|||
|
2da7660f9c
|
|||
|
cba4da6d84
|
|||
|
334cfddcf0
|
|||
|
3b1dd27b26
|
|||
|
3393cf86e8
|
|||
|
e532d860bd
|
|||
|
22d6813c24
|
29
.woodpecker/build-dev-container.yaml
Normal file
29
.woodpecker/build-dev-container.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
branch:
|
||||||
|
exclude:
|
||||||
|
- main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push a dev container image
|
||||||
|
image: gitea.badhouseplants.net/badhouseplants/container-builder
|
||||||
|
environment:
|
||||||
|
PACKAGE_NAME: badhouseplants/rustfs-manager-operator-dev
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
privileged: true
|
||||||
|
commands:
|
||||||
|
- cd ./operator && build-container
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 700Mi
|
||||||
|
cpu: 1000m
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
56
.woodpecker/publish-helm-chart.yaml
Normal file
56
.woodpecker/publish-helm-chart.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push helm charts
|
||||||
|
image: docker.io/library/ubuntu:25.04
|
||||||
|
environment:
|
||||||
|
PACKAGE_NAME: badhouseplants/rustfs-manager-operator-dev
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
commands:
|
||||||
|
- apt-get update && apt-get install curl gpg apt-transport-https --yes
|
||||||
|
- apt-get install curl gpg apt-transport-https --yes
|
||||||
|
- curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor | tee /usr/share/keyrings/helm.gpg > /dev/null
|
||||||
|
- echo "deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" | tee /etc/apt/sources.list.d/helm-stable-debian.list
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install helm -y
|
||||||
|
- export SHA="+$(git rev-parse --short HEAD)"
|
||||||
|
- helm registry login gitea.badhouseplants.net \
|
||||||
|
--username=$REGISTRY_USER \
|
||||||
|
--password=$REGISTRY_TOKEN
|
||||||
|
|
||||||
|
- |-
|
||||||
|
for chart in $(find charts -maxdepth 1 -mindepth 1 -type d); do
|
||||||
|
if [ "$CI_COMMIT_BRANCH" != "main" ]; then
|
||||||
|
yq e -i ".version += env(SHA)" "$chart/Chart.yaml"
|
||||||
|
fi
|
||||||
|
helm dep build $chart
|
||||||
|
helm package $chart -d chart-packages;
|
||||||
|
done
|
||||||
|
- export CHARTS=$(find chart-packages -maxdepth 1 -mindepth 1 -type f)
|
||||||
|
- export REGISTRY=$(echo oci://ghcr.io/$CI_REPO | tr '[:upper:]' '[:lower:]')
|
||||||
|
- |-
|
||||||
|
for chart in $CHARTS; do
|
||||||
|
echo ${chart}
|
||||||
|
CHART_NAME=$(helm show chart "${chart}" | yq .name)
|
||||||
|
CHART_VERSION=$(helm show chart "${chart}" | yq .version)
|
||||||
|
if helm pull ${REGISTRY}/${CHART_NAME}:${CHART_VERSION}; then
|
||||||
|
echo "Chart is found in the upstream: ${CHART_NAME}:${CHART_VERSION}"
|
||||||
|
continue;
|
||||||
|
fi
|
||||||
|
helm push "${chart}" "${REGISTRY}"
|
||||||
|
done
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 700Mi
|
||||||
|
cpu: 1000m
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
# rustfs-manager-operator
|
# RustFS Manager Operator
|
||||||
|
|
||||||
|
An operator to manage bucket, users, and policies on the RustfFS instance through Kubernetes CRDs.
|
||||||
|
|
||||||
An operator to manage bucket, users, and policies on the RustfFS instance through CRDs
|
|
||||||
25
Taskfile.yml
Normal file
25
Taskfile.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||||
|
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
GREETING: Hello, world!
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
desc: Print a greeting message
|
||||||
|
cmds:
|
||||||
|
- echo "{{.GREETING}}"
|
||||||
|
silent: true
|
||||||
|
sync-crds:
|
||||||
|
desc: Sync CRD from the operator to the chart
|
||||||
|
vars:
|
||||||
|
WORKDIR:
|
||||||
|
sh: mktemp -d
|
||||||
|
cmds:
|
||||||
|
- cd operator && cargo run -- --print-crd > {{.WORKDIR}}/crds.yaml
|
||||||
|
- cd {{.WORKDIR}} && yq -s '.metadata.name + ".yaml"' crds.yaml && rm crds.yaml
|
||||||
|
- rm -rf ./helm/rustfs-manager-operator/crd/*
|
||||||
|
- cp {{.WORKDIR}}/* ./helm/rustfs-manager-operator/crd/
|
||||||
|
- rm -rf {{.WORKDIR}}
|
||||||
|
|
||||||
1
documentation/docs/index.md
Normal file
1
documentation/docs/index.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# RustFS Manager Operator
|
||||||
213
documentation/poetry.lock
generated
Normal file
213
documentation/poetry.lock
generated
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.1"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
|
||||||
|
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_system == \"Windows\""
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deepmerge"
|
||||||
|
version = "2.0"
|
||||||
|
description = "A toolset for deeply merging Python dictionaries."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"},
|
||||||
|
{file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black", "build", "mypy", "pytest", "pyupgrade", "twine", "validate-pyproject[all]"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown"
|
||||||
|
version = "3.10.2"
|
||||||
|
description = "Python implementation of John Gruber's Markdown."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"},
|
||||||
|
{file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"]
|
||||||
|
testing = ["coverage", "pyyaml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||||
|
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymdown-extensions"
|
||||||
|
version = "10.21"
|
||||||
|
description = "Extension pack for Python Markdown."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"},
|
||||||
|
{file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
markdown = ">=3.6"
|
||||||
|
pyyaml = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
extra = ["pygments (>=2.19.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
|
||||||
|
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||||
|
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
|
||||||
|
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
|
||||||
|
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
|
||||||
|
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zensical"
|
||||||
|
version = "0.0.27"
|
||||||
|
description = "A modern static site generator built by the creators of Material for MkDocs"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d51ebf4b038f3eea99fd337119b99d92ad92bbe674372d5262e6dbbabbe4e9b5"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a627cd4599cf2c5a5a5205f0510667227d1fe4579b6f7445adba2d84bab9fbc8"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99cbc493022f8749504ef10c71772d360b705b4e2fd1511421393157d07bdccf"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecc20a85e8a23ad9ab809b2f268111321be7b2e214021b3b00f138936a87a434"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da11e0f0861dbd7d3b5e6fe1e3a53b361b2181c53f3abe9fb4cdf2ed0cea47bf"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e11d220181477040a4b22bf2b8678d5b0c878e7aae194fad4133561cb976d69"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06b9e308aec8c5db1cd623e2e98e1b25c3f5cab6b25fcc9bac1e16c0c2b93837"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:682085155126965b091cb9f915cd2e4297383ac500122fd4b632cf4511733eb2"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b367c285157c8e1099ae9e2b36564e07d3124bf891e96194a093bc836f3058d2"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:847c881209e65e1db1291c59a9db77966ac50f7c66bf9a733c3c7832144dbfca"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-win32.whl", hash = "sha256:f31ec13c700794be3f9c0b7d90f09a7d23575a3a27c464994b9bb441a22d880b"},
|
||||||
|
{file = "zensical-0.0.27-cp310-abi3-win_amd64.whl", hash = "sha256:9d3b1fca7ea99a7b2a8db272dd7f7839587c4ebf4f56b84ff01c97b3893ec9f8"},
|
||||||
|
{file = "zensical-0.0.27.tar.gz", hash = "sha256:6d8d74aba4a9f9505e6ba1c43d4c828ba4ff7bb1ff9b005e5174c5b92cf23419"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.1.8"
|
||||||
|
deepmerge = ">=2.0"
|
||||||
|
markdown = ">=3.7"
|
||||||
|
pygments = ">=2.16"
|
||||||
|
pymdown-extensions = ">=10.15"
|
||||||
|
pyyaml = ">=6.0.2"
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.1"
|
||||||
|
python-versions = ">=3.14"
|
||||||
|
content-hash = "07407b9a1e6b704c9524ee6ed39ed6795dfa430ee6f4207bd440c1e149dd2a2f"
|
||||||
17
documentation/pyproject.toml
Normal file
17
documentation/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "documentation"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
{name = "Nikolai Rodionov",email = "iam@allanger.xyz"}
|
||||||
|
]
|
||||||
|
license = {text = "GPL 3.0"}
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"zensical (>=0.0.27,<0.0.28)"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
355
documentation/site/404.html
Normal file
355
documentation/site/404.html
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="no-js">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
|
<meta name="description" content="A new project generated from the default template project.">
|
||||||
|
|
||||||
|
|
||||||
|
<meta name="author" content="<your name here>">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" href="/assets/logo.png">
|
||||||
|
<meta name="generator" content="zensical-0.0.27">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>Documentation</title>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/stylesheets/modern/main.50057488.min.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/stylesheets/modern/palette.dfe2e883.min.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,500,500i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
|
||||||
|
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>__md_scope=new URL("/",location),__md_hash=e=>[...e].reduce(((e,t)=>(e<<5)-e+t.charCodeAt(0)),0),__md_get=(e,t=localStorage,a=__md_scope)=>JSON.parse(t.getItem(a.pathname+"."+e)),__md_set=(e,t,a=localStorage,_=__md_scope)=>{try{a.setItem(_.pathname+"."+e,JSON.stringify(t))}catch(e){}},document.documentElement.setAttribute("data-platform",navigator.platform)</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo">
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||||
|
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||||
|
<label class="md-overlay" for="__drawer" aria-label="Navigation"></label>
|
||||||
|
<div data-md-component="skip">
|
||||||
|
|
||||||
|
<a href="#__skip" class="md-skip">
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div data-md-component="announce">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="/" title="Documentation" class="md-header__button md-logo" aria-label="Documentation" data-md-component="logo">
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-book-open" viewBox="0 0 24 24"><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer" aria-label="Navigation">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-menu" viewBox="0 0 24 24"><path d="M4 5h16M4 12h16M4 19h16"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Documentation
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<form class="md-header__option" data-md-component="palette">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-option" data-md-color-media="none" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_0">
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_1" hidden>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-option" data-md-color-media="none" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_1">
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_0" hidden>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search" aria-label="Search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-search" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog" aria-label="Search">
|
||||||
|
<button type="button" class="md-search__button">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
|
||||||
|
<a href="https://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path fill="currentColor" d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
badhouseplants/rustfs-manager-operator
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main class="md-main" data-md-component="main">
|
||||||
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||||
|
<div class="md-sidebar__scrollwrap">
|
||||||
|
<div class="md-sidebar__inner">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
||||||
|
<label class="md-nav__title" for="__drawer">
|
||||||
|
<a href="/" title="Documentation" class="md-nav__button md-logo" aria-label="Documentation" data-md-component="logo">
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-book-open" viewBox="0 0 24 24"><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
Documentation
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="md-nav__source">
|
||||||
|
<a href="https://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path fill="currentColor" d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
badhouseplants/rustfs-manager-operator
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="md-nav__list" data-md-scrollfix>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-nav__item">
|
||||||
|
<a href="/" class="md-nav__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
|
||||||
|
RustFS Manager Operator
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
||||||
|
<div class="md-sidebar__scrollwrap">
|
||||||
|
<div class="md-sidebar__inner">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-nav md-nav--secondary" aria-label="On this page">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-content" data-md-component="content">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<article class="md-content__inner md-typeset">
|
||||||
|
|
||||||
|
<h1>404 - Not found</h1>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
|
||||||
|
|
||||||
|
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-circle-arrow-up" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m16 12-4-4-4 4M12 16V8"/></svg>
|
||||||
|
Back to top
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="md-footer">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-footer-meta md-typeset">
|
||||||
|
<div class="md-footer-meta__inner md-grid">
|
||||||
|
<div class="md-copyright">
|
||||||
|
|
||||||
|
<div class="md-copyright__highlight">
|
||||||
|
Copyright © 2026 The authors
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
Made with
|
||||||
|
<a href="https://zensical.org/" target="_blank" rel="noopener">
|
||||||
|
Zensical
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="md-dialog" data-md-component="dialog">
|
||||||
|
<div class="md-dialog__inner md-typeset"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script id="__config" type="application/json">{"annotate":null,"base":"/","features":["announce.dismiss","content.code.annotate","content.code.copy","content.code.select","content.footnote.tooltips","content.tabs.link","content.tooltips","navigation.footer","navigation.indexes","navigation.instant","navigation.instant.prefetch","navigation.path","navigation.sections","navigation.top","navigation.tracking","search.highlight","toc.follow"],"search":"/assets/javascripts/workers/search.e2d2d235.min.js","tags":null,"translations":{"clipboard.copied":"Copied to clipboard","clipboard.copy":"Copy to clipboard","search.result.more.one":"1 more on this page","search.result.more.other":"# more on this page","search.result.none":"No matching documents","search.result.one":"1 matching document","search.result.other":"# matching documents","search.result.placeholder":"Type to start searching","search.result.term.missing":"Missing","select.version":"Select version"},"version":null}</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/assets/javascripts/bundle.5fd3284f.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
documentation/site/assets/images/favicon.png
Normal file
BIN
documentation/site/assets/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
29
documentation/site/assets/javascripts/LICENSE
Normal file
29
documentation/site/assets/javascripts/LICENSE
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Third-Party licenses
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Package: clipboard@2.0.11
|
||||||
|
License: MIT
|
||||||
|
Copyright: Zeno Rocha
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Package: escape-html@1.0.3
|
||||||
|
License: MIT
|
||||||
|
Copyright: 2012-2013 TJ Holowaychuk
|
||||||
|
2015 Andreas Lubbe
|
||||||
|
2015 Tiancheng "Timothy" Gu
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Package: focus-visible@5.2.1
|
||||||
|
License: W3C
|
||||||
|
Copyright: WICG
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Package: rxjs@7.8.2
|
||||||
|
License: Apache-2.0
|
||||||
|
Copyright: 2015-2018 Google, Inc.,
|
||||||
|
2015-2018 Netflix, Inc.,
|
||||||
|
2015-2018 Microsoft Corp. and contributors
|
||||||
3
documentation/site/assets/javascripts/bundle.5fd3284f.min.js
vendored
Normal file
3
documentation/site/assets/javascripts/bundle.5fd3284f.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
documentation/site/assets/javascripts/workers/search.e2d2d235.min.js
vendored
Normal file
1
documentation/site/assets/javascripts/workers/search.e2d2d235.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
documentation/site/assets/stylesheets/classic/main.d9d44b50.min.css
vendored
Normal file
1
documentation/site/assets/stylesheets/classic/main.d9d44b50.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
documentation/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css
vendored
Normal file
1
documentation/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
documentation/site/assets/stylesheets/modern/main.50057488.min.css
vendored
Normal file
1
documentation/site/assets/stylesheets/modern/main.50057488.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
documentation/site/assets/stylesheets/modern/palette.dfe2e883.min.css
vendored
Normal file
1
documentation/site/assets/stylesheets/modern/palette.dfe2e883.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
384
documentation/site/index.html
Normal file
384
documentation/site/index.html
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="no-js">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
|
<meta name="description" content="A new project generated from the default template project.">
|
||||||
|
|
||||||
|
|
||||||
|
<meta name="author" content="<your name here>">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" href="./assets/logo.png">
|
||||||
|
<meta name="generator" content="zensical-0.0.27">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<title>RustFS Manager Operator - Documentation</title>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./assets/stylesheets/modern/main.50057488.min.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./assets/stylesheets/modern/palette.dfe2e883.min.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,500,500i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
|
||||||
|
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>__md_scope=new URL(".",location),__md_hash=e=>[...e].reduce(((e,t)=>(e<<5)-e+t.charCodeAt(0)),0),__md_get=(e,t=localStorage,a=__md_scope)=>JSON.parse(t.getItem(a.pathname+"."+e)),__md_set=(e,t,a=localStorage,_=__md_scope)=>{try{a.setItem(_.pathname+"."+e,JSON.stringify(t))}catch(e){}},document.documentElement.setAttribute("data-platform",navigator.platform)</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo">
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||||
|
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||||
|
<label class="md-overlay" for="__drawer" aria-label="Navigation"></label>
|
||||||
|
<div data-md-component="skip">
|
||||||
|
|
||||||
|
|
||||||
|
<a href="#rustfs-manager-operator" class="md-skip">
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div data-md-component="announce">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="" title="Documentation" class="md-header__button md-logo" aria-label="Documentation" data-md-component="logo">
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-book-open" viewBox="0 0 24 24"><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer" aria-label="Navigation">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-menu" viewBox="0 0 24 24"><path d="M4 5h16M4 12h16M4 19h16"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Documentation
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
RustFS Manager Operator
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<form class="md-header__option" data-md-component="palette">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-option" data-md-color-media="none" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_0">
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_1" hidden>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input class="md-option" data-md-color-media="none" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_1">
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_0" hidden>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search" aria-label="Search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-search" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog" aria-label="Search">
|
||||||
|
<button type="button" class="md-search__button">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
|
||||||
|
<a href="https://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path fill="currentColor" d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
badhouseplants/rustfs-manager-operator
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main class="md-main" data-md-component="main">
|
||||||
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||||
|
<div class="md-sidebar__scrollwrap">
|
||||||
|
<div class="md-sidebar__inner">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
||||||
|
<label class="md-nav__title" for="__drawer">
|
||||||
|
<a href="" title="Documentation" class="md-nav__button md-logo" aria-label="Documentation" data-md-component="logo">
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-book-open" viewBox="0 0 24 24"><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
Documentation
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="md-nav__source">
|
||||||
|
<a href="https://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path fill="currentColor" d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
badhouseplants/rustfs-manager-operator
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="md-nav__list" data-md-scrollfix>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-nav__item md-nav__item--active">
|
||||||
|
|
||||||
|
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a href="" class="md-nav__link md-nav__link--active">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
|
||||||
|
RustFS Manager Operator
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
||||||
|
<div class="md-sidebar__scrollwrap">
|
||||||
|
<div class="md-sidebar__inner">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-nav md-nav--secondary" aria-label="On this page">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-content" data-md-component="content">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<article class="md-content__inner md-typeset">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h1 id="rustfs-manager-operator">RustFS Manager Operator<a class="headerlink" href="#rustfs-manager-operator" title="Permanent link">¶</a></h1>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
|
||||||
|
|
||||||
|
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-circle-arrow-up" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m16 12-4-4-4 4M12 16V8"/></svg>
|
||||||
|
Back to top
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="md-footer">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-footer-meta md-typeset">
|
||||||
|
<div class="md-footer-meta__inner md-grid">
|
||||||
|
<div class="md-copyright">
|
||||||
|
|
||||||
|
<div class="md-copyright__highlight">
|
||||||
|
Copyright © 2026 The authors
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
Made with
|
||||||
|
<a href="https://zensical.org/" target="_blank" rel="noopener">
|
||||||
|
Zensical
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="md-dialog" data-md-component="dialog">
|
||||||
|
<div class="md-dialog__inner md-typeset"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script id="__config" type="application/json">{"annotate":null,"base":".","features":["announce.dismiss","content.code.annotate","content.code.copy","content.code.select","content.footnote.tooltips","content.tabs.link","content.tooltips","navigation.footer","navigation.indexes","navigation.instant","navigation.instant.prefetch","navigation.path","navigation.sections","navigation.top","navigation.tracking","search.highlight","toc.follow"],"search":"./assets/javascripts/workers/search.e2d2d235.min.js","tags":null,"translations":{"clipboard.copied":"Copied to clipboard","clipboard.copy":"Copy to clipboard","search.result.more.one":"1 more on this page","search.result.more.other":"# more on this page","search.result.none":"No matching documents","search.result.one":"1 matching document","search.result.other":"# matching documents","search.result.placeholder":"Type to start searching","search.result.term.missing":"Missing","select.version":"Select version"},"version":null}</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="./assets/javascripts/bundle.5fd3284f.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
documentation/site/objects.inv
Normal file
0
documentation/site/objects.inv
Normal file
1
documentation/site/search.json
Normal file
1
documentation/site/search.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"","level":1,"title":"RustFS Manager Operator","text":"","path":["RustFS Manager Operator"],"tags":[]}]}
|
||||||
3
documentation/site/sitemap.xml
Normal file
3
documentation/site/sitemap.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
</urlset>
|
||||||
52
documentation/zensical.toml
Normal file
52
documentation/zensical.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[project]
|
||||||
|
site_name = "Documentation"
|
||||||
|
site_description = "A new project generated from the default template project."
|
||||||
|
site_author = "<your name here>"
|
||||||
|
copyright = """
|
||||||
|
Copyright © 2026 The authors
|
||||||
|
"""
|
||||||
|
repo_url = "https://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator"
|
||||||
|
repo_name = "badhouseplants/rustfs-manager-operator"
|
||||||
|
[project.theme]
|
||||||
|
favicon = "assets/logo.png"
|
||||||
|
|
||||||
|
language = "en"
|
||||||
|
features = [
|
||||||
|
"announce.dismiss",
|
||||||
|
"content.code.annotate",
|
||||||
|
"content.code.copy",
|
||||||
|
"content.code.select",
|
||||||
|
"content.footnote.tooltips",
|
||||||
|
"content.tabs.link",
|
||||||
|
"content.tooltips",
|
||||||
|
"navigation.footer",
|
||||||
|
"navigation.indexes",
|
||||||
|
"navigation.instant",
|
||||||
|
"navigation.instant.prefetch",
|
||||||
|
"navigation.path",
|
||||||
|
"navigation.sections",
|
||||||
|
"navigation.top",
|
||||||
|
"navigation.tracking",
|
||||||
|
"search.highlight",
|
||||||
|
"toc.follow",
|
||||||
|
]
|
||||||
|
[[project.theme.palette]]
|
||||||
|
scheme = "default"
|
||||||
|
toggle.icon = "lucide/sun"
|
||||||
|
toggle.name = "Switch to dark mode"
|
||||||
|
|
||||||
|
[[project.theme.palette]]
|
||||||
|
scheme = "slate"
|
||||||
|
toggle.icon = "lucide/moon"
|
||||||
|
toggle.name = "Switch to light mode"
|
||||||
|
|
||||||
|
#[project.theme.font]
|
||||||
|
#text = "Inter"
|
||||||
|
#code = "Jetbrains Mono"
|
||||||
|
[project.theme.icon]
|
||||||
|
#logo = "assets/logo.png"
|
||||||
|
#repo = "lucide/smile"
|
||||||
|
|
||||||
|
#[[project.extra.social]]
|
||||||
|
#icon = "fontawesome/brands/github"
|
||||||
|
#link = "https://github.com/user/repo"
|
||||||
23
helm/rustfs-instance/.helmignore
Normal file
23
helm/rustfs-instance/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
24
helm/rustfs-instance/Chart.yaml
Normal file
24
helm/rustfs-instance/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: rustfs-instance
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "1.16.0"
|
||||||
0
helm/rustfs-instance/templates/NOTES.txt
Normal file
0
helm/rustfs-instance/templates/NOTES.txt
Normal file
62
helm/rustfs-instance/templates/_helpers.tpl
Normal file
62
helm/rustfs-instance/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "rustfs-instance.chart" . }}
|
||||||
|
{{ include "rustfs-instance.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "rustfs-instance.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-instance.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "rustfs-instance.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
12
helm/rustfs-instance/templates/intsance.yaml
Normal file
12
helm/rustfs-instance/templates/intsance.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
apiVersion: rustfs.badhouseplants.net/v1beta1
|
||||||
|
kind: RustFSInstance
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-instance.name" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-instance.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
endpoint: {{ .Values.endpoint }}
|
||||||
|
credentialsSecret:
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
name: {{ include "rustfs-instance.name" . }}
|
||||||
10
helm/rustfs-instance/templates/secret.yaml
Normal file
10
helm/rustfs-instance/templates/secret.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-instance.name" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-instance.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
ACCESS_KEY: {{ .Values.username | toString | b64enc }}
|
||||||
|
SECRET_KEY: {{ .Values.password | toString | b64enc }}
|
||||||
3
helm/rustfs-instance/values.yaml
Normal file
3
helm/rustfs-instance/values.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
endpoint: https://rustfs.company.my
|
||||||
|
username: admin
|
||||||
|
password: qwertyu9
|
||||||
23
helm/rustfs-manager-operator/.helmignore
Normal file
23
helm/rustfs-manager-operator/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
24
helm/rustfs-manager-operator/Chart.yaml
Normal file
24
helm/rustfs-manager-operator/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: rustfs-manager-operator
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "1.16.0"
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: rustfsbuckets.rustfs.badhouseplants.net
|
||||||
|
spec:
|
||||||
|
group: rustfs.badhouseplants.net
|
||||||
|
names:
|
||||||
|
categories: []
|
||||||
|
kind: RustFSBucket
|
||||||
|
plural: rustfsbuckets
|
||||||
|
shortNames:
|
||||||
|
- bucket
|
||||||
|
singular: rustfsbucket
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- description: The name of the bucket
|
||||||
|
jsonPath: .status.bucketName
|
||||||
|
name: Bucket Name
|
||||||
|
type: string
|
||||||
|
- description: The URL of the instance
|
||||||
|
jsonPath: .status.endpoint
|
||||||
|
name: Endpoint
|
||||||
|
type: string
|
||||||
|
- description: The region of the instance
|
||||||
|
jsonPath: .status.region
|
||||||
|
name: Region
|
||||||
|
type: string
|
||||||
|
- description: Is the S3Instance ready
|
||||||
|
jsonPath: .status.ready
|
||||||
|
name: Status
|
||||||
|
type: boolean
|
||||||
|
name: v1beta1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Manage buckets on a RustFs instance
|
||||||
|
properties:
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
cleanup:
|
||||||
|
default: false
|
||||||
|
description: When set to true, the operator will try remove the bucket upon object deletion
|
||||||
|
type: boolean
|
||||||
|
instance:
|
||||||
|
type: string
|
||||||
|
objectLock:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
versioning:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- instance
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: The status object of `DbInstance`
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
bucketName:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: message is a human readable message indicating details about the transition. This may be an empty string.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
endpoint:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
ready:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
region:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- conditions
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
title: RustFSBucket
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: rustfsinstances.rustfs.badhouseplants.net
|
||||||
|
spec:
|
||||||
|
group: rustfs.badhouseplants.net
|
||||||
|
names:
|
||||||
|
categories: []
|
||||||
|
kind: RustFSInstance
|
||||||
|
plural: rustfsinstances
|
||||||
|
shortNames:
|
||||||
|
- rustfs
|
||||||
|
singular: rustfsinstance
|
||||||
|
scope: Cluster
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- description: The URL of the instance
|
||||||
|
jsonPath: .spec.endpoint
|
||||||
|
name: Endpoint
|
||||||
|
type: string
|
||||||
|
- description: The region of the instance
|
||||||
|
jsonPath: .status.region
|
||||||
|
name: Region
|
||||||
|
type: string
|
||||||
|
- description: How many buckets are there on the instance
|
||||||
|
jsonPath: .status.total_buckets
|
||||||
|
name: Total Buckets
|
||||||
|
type: number
|
||||||
|
- description: Is the S3Instance ready
|
||||||
|
jsonPath: .status.ready
|
||||||
|
name: Status
|
||||||
|
type: boolean
|
||||||
|
name: v1beta1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Connect the operator to a RustFs cluster using this resource
|
||||||
|
properties:
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
credentialsSecret:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- namespace
|
||||||
|
type: object
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- credentialsSecret
|
||||||
|
- endpoint
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: The status object of `DbInstance`
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
buckets:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
type: array
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: message is a human readable message indicating details about the transition. This may be an empty string.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
ready:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
region:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
total_buckets:
|
||||||
|
format: uint
|
||||||
|
minimum: 0.0
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- conditions
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
title: RustFSInstance
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: rustfsusers.rustfs.badhouseplants.net
|
||||||
|
spec:
|
||||||
|
group: rustfs.badhouseplants.net
|
||||||
|
names:
|
||||||
|
categories: []
|
||||||
|
kind: RustFSUser
|
||||||
|
plural: rustfsusers
|
||||||
|
shortNames: []
|
||||||
|
singular: rustfsuser
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns: []
|
||||||
|
name: v1beta1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Manage buckets on a RustFs instance
|
||||||
|
properties:
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
access:
|
||||||
|
enum:
|
||||||
|
- readOnly
|
||||||
|
- readWrite
|
||||||
|
type: string
|
||||||
|
bucket:
|
||||||
|
type: string
|
||||||
|
cleanup:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- access
|
||||||
|
- bucket
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: The status object of `DbInstance`
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: message is a human readable message indicating details about the transition. This may be an empty string.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
configMapName:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
passwordHash:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
policy:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
ready:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
secretName:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- conditions
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
title: RustFSUser
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
35
helm/rustfs-manager-operator/templates/NOTES.txt
Normal file
35
helm/rustfs-manager-operator/templates/NOTES.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.httpRoute.enabled }}
|
||||||
|
{{- if .Values.httpRoute.hostnames }}
|
||||||
|
export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }}
|
||||||
|
{{- else }}
|
||||||
|
export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}")
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }}
|
||||||
|
echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application"
|
||||||
|
|
||||||
|
NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules.
|
||||||
|
The rules can be set for path, method, header and query parameters.
|
||||||
|
You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml'
|
||||||
|
{{- end }}
|
||||||
|
{{- else if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "rustfs-manager-operator.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "rustfs-manager-operator.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "rustfs-manager-operator.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "rustfs-manager-operator.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
||||||
62
helm/rustfs-manager-operator/templates/_helpers.tpl
Normal file
62
helm/rustfs-manager-operator/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "rustfs-manager-operator.chart" . }}
|
||||||
|
{{ include "rustfs-manager-operator.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "rustfs-manager-operator.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "rustfs-manager-operator.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
69
helm/rustfs-manager-operator/templates/cluster_role.yaml
Normal file
69
helm/rustfs-manager-operator/templates/cluster_role.yaml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{{- if .Values.rbac.create -}}
|
||||||
|
---
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.name" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- secrets
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- rustfs.badhouseplants.net
|
||||||
|
resources:
|
||||||
|
- rustfsbuckets
|
||||||
|
- rustfsinstances
|
||||||
|
- rustfsusers
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- rustfs.badhouseplants.net
|
||||||
|
resources:
|
||||||
|
- rustfsbuckets/finalizers
|
||||||
|
- rustfsintsances/finalizers
|
||||||
|
- rustfsusers/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- rustfs.badhouseplants.net
|
||||||
|
resources:
|
||||||
|
- rustfsbuckets/status
|
||||||
|
- rustfsinstances/status
|
||||||
|
- rustfsusers/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- "events.k8s.io"
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{{- if .Values.rbac.create -}}
|
||||||
|
---
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.name" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: {{ include "rustfs-manager-operator.serviceAccountName" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: {{ include "rustfs-manager-operator.name" . }}
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
{{- end }}
|
||||||
9
helm/rustfs-manager-operator/templates/config.yaml
Normal file
9
helm/rustfs-manager-operator/templates/config.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.name" . }}-config
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
config.json: |-
|
||||||
|
{{- .Values.config | toJson | nindent 4 }}
|
||||||
28
helm/rustfs-manager-operator/templates/crds.yaml
Normal file
28
helm/rustfs-manager-operator/templates/crds.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{{- if .Values.crds.install }}
|
||||||
|
{{- $manifests := dict }}
|
||||||
|
{{- range $path, $index := .Files.Glob "crd/*" }}
|
||||||
|
{{- $file := $.Files.Get $path }}
|
||||||
|
{{- $_ := set $manifests ($index | toString ) $file }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range $_, $file := $manifests }}
|
||||||
|
---
|
||||||
|
{{- $manifest := $file | fromYaml }}
|
||||||
|
apiVersion: {{ get $manifest "apiVersion" }}
|
||||||
|
kind: {{ get $manifest "kind" }}
|
||||||
|
{{- $metadata := get $manifest "metadata" }}
|
||||||
|
metadata:
|
||||||
|
name: {{ get $metadata "name" }}
|
||||||
|
{{- with $.Values.labels }}
|
||||||
|
labels:
|
||||||
|
{{- . | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
annotations:
|
||||||
|
{{- if $.Values.crds.keep }}
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{ get $manifest "spec" | toYaml | indent 2 }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
84
helm/rustfs-manager-operator/templates/deployment.yaml
Normal file
84
helm/rustfs-manager-operator/templates/deployment.yaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "rustfs-manager-operator.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 8 }}
|
||||||
|
{{- with .Values.podLabels }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "rustfs-manager-operator.serviceAccountName" . }}
|
||||||
|
{{- with .Values.podSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
{{- with .Values.securityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
{{- with .Values.livenessProbe }}
|
||||||
|
livenessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.readinessProbe }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /srv/config/
|
||||||
|
name: config
|
||||||
|
readOnly: true
|
||||||
|
{{- with .Values.volumeMounts }}
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: {{ include "rustfs-manager-operator.name" . }}-config
|
||||||
|
defaultMode: 420
|
||||||
|
{{- with .Values.volumes }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
13
helm/rustfs-manager-operator/templates/serviceaccount.yaml
Normal file
13
helm/rustfs-manager-operator/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||||
|
{{- end }}
|
||||||
157
helm/rustfs-manager-operator/values.yaml
Normal file
157
helm/rustfs-manager-operator/values.yaml
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
crds:
|
||||||
|
install: true
|
||||||
|
keep: true
|
||||||
|
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator-dev
|
||||||
|
# This sets the pull policy for images.
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: "cb2f89e80cce4be5d1a76cc34244a96e866fcd35"
|
||||||
|
|
||||||
|
rbac:
|
||||||
|
create: true
|
||||||
|
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created.
|
||||||
|
create: true
|
||||||
|
# Automatically mount a ServiceAccount's API credentials?
|
||||||
|
automount: true
|
||||||
|
# Annotations to add to the service account.
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template.
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
config:
|
||||||
|
setOwnerReference: true
|
||||||
|
|
||||||
|
# This is for setting Kubernetes Annotations to a Pod.
|
||||||
|
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||||
|
podAnnotations: {}
|
||||||
|
# This is for setting Kubernetes Labels to a Pod.
|
||||||
|
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||||
|
podLabels: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||||
|
service:
|
||||||
|
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||||
|
type: ClusterIP
|
||||||
|
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
# -- Expose the service via gateway-api HTTPRoute
|
||||||
|
# Requires Gateway API resources and suitable controller installed within the cluster
|
||||||
|
# (see: https://gateway-api.sigs.k8s.io/guides/)
|
||||||
|
httpRoute:
|
||||||
|
# HTTPRoute enabled.
|
||||||
|
enabled: false
|
||||||
|
# HTTPRoute annotations.
|
||||||
|
annotations: {}
|
||||||
|
# Which Gateways this Route is attached to.
|
||||||
|
parentRefs:
|
||||||
|
- name: gateway
|
||||||
|
sectionName: http
|
||||||
|
# namespace: default
|
||||||
|
# Hostnames matching HTTP header.
|
||||||
|
hostnames:
|
||||||
|
- chart-example.local
|
||||||
|
# List of rules and filters applied.
|
||||||
|
rules:
|
||||||
|
- matches:
|
||||||
|
- path:
|
||||||
|
type: PathPrefix
|
||||||
|
value: /headers
|
||||||
|
# filters:
|
||||||
|
# - type: RequestHeaderModifier
|
||||||
|
# requestHeaderModifier:
|
||||||
|
# set:
|
||||||
|
# - name: My-Overwrite-Header
|
||||||
|
# value: this-is-the-only-value
|
||||||
|
# remove:
|
||||||
|
# - User-Agent
|
||||||
|
# - matches:
|
||||||
|
# - path:
|
||||||
|
# type: PathPrefix
|
||||||
|
# value: /echo
|
||||||
|
# headers:
|
||||||
|
# - name: version
|
||||||
|
# value: v2
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
|
||||||
|
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||||
|
autoscaling:
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 100
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
# targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
# Additional volumes on the output Deployment definition.
|
||||||
|
volumes: []
|
||||||
|
# - name: foo
|
||||||
|
# secret:
|
||||||
|
# secretName: mysecret
|
||||||
|
# optional: false
|
||||||
|
|
||||||
|
# Additional volumeMounts on the output Deployment definition.
|
||||||
|
volumeMounts: []
|
||||||
|
# - name: foo
|
||||||
|
# mountPath: "/etc/foo"
|
||||||
|
# readOnly: true
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
1
operator/.containerignore
Normal file
1
operator/.containerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target
|
||||||
3720
operator/Cargo.lock
generated
Normal file
3720
operator/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
operator/Cargo.toml
Normal file
42
operator/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[package]
|
||||||
|
name = "rustfs-manager-operator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
default-bin = "controller"
|
||||||
|
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "api"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
kube = { version = "3.0.1", features = ["runtime", "derive", "client", "aws-lc-rs"] }
|
||||||
|
k8s-openapi = { version = "0.27.0", features = ["latest", "schemars"] }
|
||||||
|
schemars = { version = "1" }
|
||||||
|
darling = "0.23.0"
|
||||||
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
|
serde = { version = "1.0.228", features = ["serde_derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
thiserror = "2.0.18"
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
futures = "0.3.32"
|
||||||
|
actix-web = "4.13.0"
|
||||||
|
tracing-subscriber = { version = "0.3.22", features = ["json", "env-filter"] }
|
||||||
|
rand = "0.10.0"
|
||||||
|
tempfile = "3.27.0"
|
||||||
|
password-hash = "0.6.0"
|
||||||
|
sha-crypt = "0.5.0"
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
handlebars = "6.4.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert-json-diff = "2.0.2"
|
||||||
|
envtest = "0.1.2"
|
||||||
|
http = "1"
|
||||||
|
hyper = "1"
|
||||||
|
tower-test = "0.4.0"
|
||||||
|
|
||||||
16
operator/Containerfile
Normal file
16
operator/Containerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM docker.io/rustfs/rc:v0.1.7 AS rc
|
||||||
|
WORKDIR /output
|
||||||
|
RUN cp $(which rc) .
|
||||||
|
|
||||||
|
FROM rust:alpine3.23 AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release
|
||||||
|
WORKDIR /output
|
||||||
|
RUN cp /src/target/release/rustfs-manager-operator .
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static
|
||||||
|
COPY --from=builder /output/rustfs-manager-operator /usr/bin/controller
|
||||||
|
COPY --from=rc /output/rc /usr/bin/rc
|
||||||
|
ENTRYPOINT ["/usr/bin/controller"]
|
||||||
|
USER 1001
|
||||||
8
operator/Containerfile.test
Normal file
8
operator/Containerfile.test
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM docker.io/rustfs/rc:v0.1.7 AS rc
|
||||||
|
WORKDIR /output
|
||||||
|
RUN cp $(which rc) .
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static
|
||||||
|
COPY --from=rc /output/rc /usr/bin/rc
|
||||||
|
ENTRYPOINT ["/usr/bin/rc"]
|
||||||
|
USER 1001
|
||||||
3
operator/config.json
Normal file
3
operator/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"setOwnerReference": true
|
||||||
|
}
|
||||||
3
operator/src/api/mod.rs
Normal file
3
operator/src/api/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod v1beta1_rustfs_instance;
|
||||||
|
pub mod v1beta_rustfs_bucket;
|
||||||
|
pub mod v1beta_rustfs_user;
|
||||||
47
operator/src/api/v1beta1_rustfs_instance.rs
Normal file
47
operator/src/api/v1beta1_rustfs_instance.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
|
||||||
|
use k8s_openapi::serde::{Deserialize, Serialize};
|
||||||
|
use kube::CustomResource;
|
||||||
|
use kube::{self};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[kube(
|
||||||
|
kind = "RustFSInstance",
|
||||||
|
group = "rustfs.badhouseplants.net",
|
||||||
|
version = "v1beta1",
|
||||||
|
shortname = "rustfs",
|
||||||
|
doc = "Connect the operator to a RustFs cluster using this resource",
|
||||||
|
status = "RustFSInstanceStatus",
|
||||||
|
printcolumn = r#"{"name":"Endpoint","type":"string","description":"The URL of the instance","jsonPath":".spec.endpoint"}"#,
|
||||||
|
printcolumn = r#"{"name":"Region","type":"string","description":"The region of the instance","jsonPath":".status.region"}"#,
|
||||||
|
printcolumn = r#"{"name":"Total Buckets","type":"number","description":"How many buckets are there on the instance","jsonPath":".status.total_buckets"}"#,
|
||||||
|
printcolumn = r#"{"name":"Status","type":"boolean","description":"Is the S3Instance ready","jsonPath":".status.ready"}"#
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct S3InstanceSpec {
|
||||||
|
pub endpoint: String,
|
||||||
|
pub credentials_secret: NamespacedName,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status object of `DbInstance`
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
|
||||||
|
pub struct RustFSInstanceStatus {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ready: bool,
|
||||||
|
//#[schemars(schema_with = "conditions")]
|
||||||
|
pub conditions: Vec<Condition>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub buckets: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub total_buckets: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub region: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
pub struct NamespacedName {
|
||||||
|
#[serde(rename = "namespace")]
|
||||||
|
pub namespace: String,
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
57
operator/src/api/v1beta_rustfs_bucket.rs
Normal file
57
operator/src/api/v1beta_rustfs_bucket.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
|
||||||
|
use k8s_openapi::serde::{Deserialize, Serialize};
|
||||||
|
use kube::CustomResource;
|
||||||
|
use kube::{self};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[kube(
|
||||||
|
kind = "RustFSBucket",
|
||||||
|
group = "rustfs.badhouseplants.net",
|
||||||
|
version = "v1beta1",
|
||||||
|
shortname = "bucket",
|
||||||
|
doc = "Manage buckets on a RustFs instance",
|
||||||
|
namespaced,
|
||||||
|
status = "RustFSBucketStatus",
|
||||||
|
printcolumn = r#"{"name":"Bucket Name","type":"string","description":"The name of the bucket","jsonPath":".status.bucketName"}"#,
|
||||||
|
printcolumn = r#"{"name":"Endpoint","type":"string","description":"The URL of the instance","jsonPath":".status.endpoint"}"#,
|
||||||
|
printcolumn = r#"{"name":"Region","type":"string","description":"The region of the instance","jsonPath":".status.region"}"#,
|
||||||
|
printcolumn = r#"{"name":"Status","type":"boolean","description":"Is the S3Instance ready","jsonPath":".status.ready"}"#
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSBucketSpec {
|
||||||
|
pub instance: String,
|
||||||
|
/// When set to true, the operator will try remove the bucket upon object deletion
|
||||||
|
#[serde(default)]
|
||||||
|
pub cleanup: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
#[kube(validation = Rule::new("self == oldSelf").message("field is immutable"))]
|
||||||
|
pub object_lock: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
#[kube(validation = Rule::new("self == oldSelf").message("field is immutable"))]
|
||||||
|
pub versioning: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status object of `DbInstance`
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSBucketStatus {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ready: bool,
|
||||||
|
//#[schemars(schema_with = "conditions")]
|
||||||
|
pub conditions: Vec<Condition>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bucket_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub endpoint: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub region: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
pub struct NamespacedName {
|
||||||
|
#[serde(rename = "namespace")]
|
||||||
|
pub namespace: String,
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
52
operator/src/api/v1beta_rustfs_user.rs
Normal file
52
operator/src/api/v1beta_rustfs_user.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
|
||||||
|
use k8s_openapi::serde::{Deserialize, Serialize};
|
||||||
|
use kube::CustomResource;
|
||||||
|
use kube::{self};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)]
|
||||||
|
pub enum Access {
|
||||||
|
#[serde(rename = "readOnly")]
|
||||||
|
ReadOnly,
|
||||||
|
#[serde(rename = "readWrite")]
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
|
||||||
|
#[kube(
|
||||||
|
kind = "RustFSUser",
|
||||||
|
group = "rustfs.badhouseplants.net",
|
||||||
|
version = "v1beta1",
|
||||||
|
doc = "Manage buckets on a RustFs instance",
|
||||||
|
namespaced,
|
||||||
|
status = "RustFSUserStatus"
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSUserSpec {
|
||||||
|
pub bucket: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cleanup: bool,
|
||||||
|
pub access: Access,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status object of `DbInstance`
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSUserStatus {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ready: bool,
|
||||||
|
//#[schemars(schema_with = "conditions")]
|
||||||
|
pub conditions: Vec<Condition>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub password_hash: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub status: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub policy: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub secret_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub config_map_name: Option<String>,
|
||||||
|
}
|
||||||
70
operator/src/cli.rs
Normal file
70
operator/src/cli.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::process::Command;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub(crate) fn rc_exec(args: Vec<&str>) -> Result<String, anyhow::Error> {
|
||||||
|
info!("Executing rc + {:?}", args);
|
||||||
|
let expect = format!("command has failed: rc {:?}", args);
|
||||||
|
let output = Command::new("rc").args(args).output().expect(&expect);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
if !&output.status.success() {
|
||||||
|
return Err(anyhow!(stderr));
|
||||||
|
};
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cli_exec_from_dir(command: String, dir: String) -> Result<String, anyhow::Error> {
|
||||||
|
info!("executing: {}", command);
|
||||||
|
let expect = format!("command has failed: {}", command);
|
||||||
|
let output = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.current_dir(dir)
|
||||||
|
.arg(command)
|
||||||
|
.output()
|
||||||
|
.expect(&expect);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
if !&output.status.success() {
|
||||||
|
return Err(anyhow!(stderr));
|
||||||
|
};
|
||||||
|
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
stdout.pop();
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::cli::{cli_exec_from_dir, rc_exec};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stderr() {
|
||||||
|
let command = ">&2 echo \"error\" && exit 1";
|
||||||
|
let test = rc_exec(command.to_string());
|
||||||
|
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stdout() {
|
||||||
|
let command = "echo test";
|
||||||
|
let test = rc_exec(command.to_string());
|
||||||
|
assert_eq!(test.unwrap().to_string(), "test\n".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stdout_current_dir() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let dir_str = dir.path().to_str().unwrap().to_string();
|
||||||
|
let command = "echo $PWD";
|
||||||
|
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
|
||||||
|
assert!(test.unwrap().to_string().contains(dir_str.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stderr_current_dir() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let dir_str = dir.path().to_str().unwrap().to_string();
|
||||||
|
let command = ">&2 echo \"error\" && exit 1";
|
||||||
|
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
|
||||||
|
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
67
operator/src/conditions.rs
Normal file
67
operator/src/conditions.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
|
||||||
|
use k8s_openapi::jiff::Timestamp;
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
|
|
||||||
|
pub(crate) fn set_condition(
|
||||||
|
mut conditions: Vec<Condition>,
|
||||||
|
metadata: ObjectMeta,
|
||||||
|
condition_type: &str,
|
||||||
|
condition_status: String,
|
||||||
|
condition_reason: String,
|
||||||
|
condition_message: String,
|
||||||
|
) -> Vec<Condition> {
|
||||||
|
if let Some(condition) = conditions.iter_mut().find(|c| c.type_ == condition_type) {
|
||||||
|
condition.status = condition_status;
|
||||||
|
condition.last_transition_time = Time::from(Timestamp::now());
|
||||||
|
condition.message = condition_message;
|
||||||
|
condition.reason = condition_reason;
|
||||||
|
condition.observed_generation = metadata.generation;
|
||||||
|
} else {
|
||||||
|
conditions.push(Condition {
|
||||||
|
last_transition_time: Time::from(Timestamp::now()),
|
||||||
|
message: condition_message,
|
||||||
|
observed_generation: metadata.generation,
|
||||||
|
reason: condition_reason,
|
||||||
|
status: condition_status,
|
||||||
|
type_: condition_type.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
conditions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn init_conditions(types: Vec<String>) -> Vec<Condition> {
|
||||||
|
let mut conditions: Vec<Condition> = vec![];
|
||||||
|
types.iter().for_each(|t| {
|
||||||
|
let condition = Condition {
|
||||||
|
last_transition_time: Time::from(Timestamp::now()),
|
||||||
|
message: "Reconciliation started".to_string(),
|
||||||
|
observed_generation: Some(1),
|
||||||
|
reason: "Reconciling".to_string(),
|
||||||
|
status: "Unknown".to_string(),
|
||||||
|
type_: t.clone(),
|
||||||
|
};
|
||||||
|
conditions.push(condition);
|
||||||
|
});
|
||||||
|
conditions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_condition_true(mut conditions: Vec<Condition>, condition_type: &str) -> bool {
|
||||||
|
if let Some(condition) = conditions.iter_mut().find(|c| c.type_ == condition_type) {
|
||||||
|
return condition.status == "True";
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_condition_false(mut conditions: Vec<Condition>, condition_type: &str) -> bool {
|
||||||
|
if let Some(condition) = conditions.iter_mut().find(|c| c.type_ == condition_type) {
|
||||||
|
return condition.status == "False";
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_condition_unknown(mut conditions: Vec<Condition>, condition_type: &str) -> bool {
|
||||||
|
if let Some(condition) = conditions.iter_mut().find(|c| c.type_ == condition_type) {
|
||||||
|
return condition.status == "Unknown";
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
36
operator/src/config.rs
Normal file
36
operator/src/config.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OperatorConfig {
|
||||||
|
pub set_owner_reference: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_config_from_file(path: String) -> Result<OperatorConfig, anyhow::Error> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let config: OperatorConfig = serde_json::from_reader(file)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
use crate::config::read_config_from_file;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_config() {
|
||||||
|
let config_json = r#"{
|
||||||
|
"setOwnerReference": true
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut file = NamedTempFile::new().expect("Can't create a file");
|
||||||
|
let path = file.path().to_path_buf();
|
||||||
|
writeln!(file, "{}", config_json).expect("Can't write a config file");
|
||||||
|
let config = read_config_from_file(path.to_str().expect("Can't get the path").to_string())
|
||||||
|
.expect("Can't read the config file");
|
||||||
|
assert!(config.set_owner_reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
operator/src/controller.rs
Normal file
103
operator/src/controller.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
mod conditions;
|
||||||
|
mod controllers;
|
||||||
|
mod rc;
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use crate::controllers::{rustfs_instance};
|
||||||
|
|
||||||
|
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, get, middleware};
|
||||||
|
use clap::Parser;
|
||||||
|
use kube::{Client, CustomResourceExt};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use self::config::read_config_from_file;
|
||||||
|
use self::controllers::{rustfs_bucket, rustfs_user};
|
||||||
|
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use api::api::v1beta_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta_rustfs_user::RustFSUser;
|
||||||
|
|
||||||
|
/// Simple program to greet a person
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(long, default_value_t = 60000)]
|
||||||
|
/// The address the metric endpoint binds to.
|
||||||
|
metrics_port: u16,
|
||||||
|
#[arg(long, default_value_t = 8081)]
|
||||||
|
/// The address the probe endpoint binds to.
|
||||||
|
health_probe_port: u16,
|
||||||
|
#[arg(long, default_value_t = true)]
|
||||||
|
/// Enabling this will ensure there is only one active controller manager.
|
||||||
|
// DB Operator feature flags
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
/// If enabled, DB Operator will run full reconciliation only
|
||||||
|
/// when changes are detected
|
||||||
|
is_change_check_nabled: bool,
|
||||||
|
#[arg(long, default_value = "/src/config/config.json")]
|
||||||
|
/// A path to a config file
|
||||||
|
config: String,
|
||||||
|
/// Set to true to generate crds
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
crdgen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/health")]
|
||||||
|
async fn health(_: HttpRequest) -> impl Responder {
|
||||||
|
HttpResponse::Ok().json("healthy")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crdgen() {
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSInstance::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSBucket::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSUser::crd()).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
if args.crdgen {
|
||||||
|
crdgen();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.json()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
let client = Client::try_default()
|
||||||
|
.await
|
||||||
|
.expect("failed to create kube Client");
|
||||||
|
let config = read_config_from_file(args.config)?;
|
||||||
|
let rustfs_instance_ctrl = rustfs_instance::run(client.clone());
|
||||||
|
let rustfs_bucket_ctrl = rustfs_bucket::run(client.clone(), config.clone());
|
||||||
|
let rustfs_user_ctrl = rustfs_user::run(client.clone());
|
||||||
|
// Start web server
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(middleware::Logger::default().exclude("/health"))
|
||||||
|
.service(health)
|
||||||
|
})
|
||||||
|
.bind("0.0.0.0:8080")?
|
||||||
|
.shutdown_timeout(5);
|
||||||
|
|
||||||
|
// Both runtimes implements graceful shutdown, so poll until both are done
|
||||||
|
tokio::join!(
|
||||||
|
rustfs_instance_ctrl,
|
||||||
|
rustfs_bucket_ctrl,
|
||||||
|
rustfs_user_ctrl,
|
||||||
|
server.run()
|
||||||
|
)
|
||||||
|
.3?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
3
operator/src/controllers/mod.rs
Normal file
3
operator/src/controllers/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod rustfs_bucket;
|
||||||
|
pub(crate) mod rustfs_instance;
|
||||||
|
pub(crate) mod rustfs_user;
|
||||||
396
operator/src/controllers/rustfs_bucket.rs
Normal file
396
operator/src/controllers/rustfs_bucket.rs
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
use crate::conditions::{is_condition_true, set_condition};
|
||||||
|
use crate::config::OperatorConfig;
|
||||||
|
use crate::rc::{create_bucket, delete_bucket, list_buckets};
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use k8s_openapi::api::core::v1::ConfigMap;
|
||||||
|
use api::api::v1beta_rustfs_bucket::{RustFSBucket, RustFSBucketStatus};
|
||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
|
||||||
|
use kube::api::{ListParams, ObjectMeta, PostParams};
|
||||||
|
use kube::runtime::Controller;
|
||||||
|
use kube::runtime::controller::Action;
|
||||||
|
use kube::runtime::events::Recorder;
|
||||||
|
use kube::runtime::watcher::Config;
|
||||||
|
use kube::{Api, Client, Error, Resource, ResourceExt};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
const TYPE_BUCKET_READY: &str = "BucketReady";
|
||||||
|
const FIN_CLEANUP: &str = "s3.badhouseplants.net/bucket-cleanup";
|
||||||
|
const CONFIGMAP_LABEL: &str = "s3.badhouseplants.net/s3-bucket";
|
||||||
|
|
||||||
|
const AWS_REGION: &str = "AWS_REGION";
|
||||||
|
const AWS_ENDPOINT_URL: &str = "AWS_ENDPOINT_URL";
|
||||||
|
|
||||||
|
#[instrument(skip(ctx, obj), fields(trace_id, controller = "rustfs-bucket"))]
|
||||||
|
pub(crate) async fn reconcile(obj: Arc<RustFSBucket>, ctx: Arc<Context>) -> RustFSBucketResult<Action> {
|
||||||
|
info!("Staring to reconcile");
|
||||||
|
|
||||||
|
let bucket_api: Api<RustFSBucket> =
|
||||||
|
Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
|
||||||
|
let cm_api: Api<ConfigMap> = Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
|
||||||
|
let rustfs_api: Api<RustFSInstance> = Api::all(ctx.client.clone());
|
||||||
|
|
||||||
|
info!("Getting the RustFSBucket resource");
|
||||||
|
let mut bucket_cr = match bucket_api.get(&obj.name_any()).await {
|
||||||
|
Ok(cr) => cr,
|
||||||
|
Err(Error::Api(ae)) if ae.code == 404 => {
|
||||||
|
info!("Object is not found, probably removed");
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSBucketError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// On the first reconciliation status is None
|
||||||
|
// it needs to be initialized
|
||||||
|
let mut status = match bucket_cr.clone().status {
|
||||||
|
None => {
|
||||||
|
info!("Status is not yet set, initializing the object");
|
||||||
|
return init_object(bucket_cr, bucket_api).await;
|
||||||
|
}
|
||||||
|
Some(status) => status,
|
||||||
|
};
|
||||||
|
|
||||||
|
let configmap_name = format!("{}-bucket-info", bucket_cr.name_any());
|
||||||
|
|
||||||
|
info!("Getting the configmap");
|
||||||
|
// Get the cm, if it's already there, we need to validate, or create an empty one
|
||||||
|
let mut configmap = match get_configmap(cm_api.clone(), &configmap_name).await {
|
||||||
|
Ok(configmap) => configmap,
|
||||||
|
Err(Error::Api(ae)) if ae.code == 404 => {
|
||||||
|
info!("ConfigMap is not found, creating a new one");
|
||||||
|
let cm = ConfigMap {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(configmap_name),
|
||||||
|
namespace: Some(bucket_cr.clone().namespace().unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match create_configmap(cm_api.clone(), cm).await {
|
||||||
|
Ok(cm) => cm,
|
||||||
|
Err(err) => return Err(RustFSBucketError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSBucketError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Labeling the configmap");
|
||||||
|
configmap = match label_configmap(cm_api.clone(), &bucket_cr.name_any(), configmap).await {
|
||||||
|
Ok(configmap) => configmap,
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ctx.config.set_owner_reference{
|
||||||
|
info!("Setting owner references to the configmap");
|
||||||
|
configmap = match own_configmap(cm_api.clone(), bucket_cr.clone(), configmap).await {
|
||||||
|
Ok(configmap) => configmap,
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_condition_true(status.clone().conditions, TYPE_BUCKET_READY) {
|
||||||
|
let mut current_finalizers = match bucket_cr.clone().metadata.finalizers {
|
||||||
|
Some(finalizers) => finalizers,
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
if bucket_cr.spec.cleanup {
|
||||||
|
if !current_finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
info!("Adding a finalizer");
|
||||||
|
current_finalizers.push(FIN_CLEANUP.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if current_finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
if let Some(index) = current_finalizers
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x == FIN_CLEANUP.to_string())
|
||||||
|
{
|
||||||
|
current_finalizers.remove(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bucket_cr.metadata.finalizers = Some(current_finalizers);
|
||||||
|
if let Err(err) = bucket_api
|
||||||
|
.replace(&bucket_cr.name_any(), &PostParams::default(), &bucket_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Getting the RustFSIntsance");
|
||||||
|
let rustfs_cr = match rustfs_api.get(&bucket_cr.spec.instance).await {
|
||||||
|
Ok(rustfs_cr) => rustfs_cr,
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if rustfs_cr.clone().status.is_none_or(|s| ! s.ready) {
|
||||||
|
info!("Instance is not ready, waiting");
|
||||||
|
return Ok(Action::requeue(Duration::from_secs(120)));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Updating the ConfigMap");
|
||||||
|
if let Err(err) = ensure_data_configmap(cm_api.clone(), rustfs_cr.clone(), configmap.clone()).await {
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let bucket_name = format!("{}-{}", bucket_cr.namespace().unwrap(), bucket_cr.name_any());
|
||||||
|
|
||||||
|
if bucket_cr.metadata.deletion_timestamp.is_some() {
|
||||||
|
info!("Object is marked for deletion");
|
||||||
|
if let Some(mut finalizers) = bucket_cr.clone().metadata.finalizers {
|
||||||
|
if finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
match delete_bucket(rustfs_cr.name_any(), bucket_name.clone()) {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Some(index) = finalizers
|
||||||
|
.iter()
|
||||||
|
.position(|x| x == FIN_CLEANUP)
|
||||||
|
{
|
||||||
|
finalizers.remove(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSBucketError::RcCliError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucket_cr.metadata.finalizers = Some(finalizers);
|
||||||
|
};
|
||||||
|
match bucket_api
|
||||||
|
.replace(&bucket_cr.name_any(), &PostParams::default(), &bucket_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSBucketError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Getting buckets");
|
||||||
|
|
||||||
|
|
||||||
|
let bucket_list: Vec<String> = match list_buckets(rustfs_cr.name_any().to_string()){
|
||||||
|
Ok(bl) => bl.items.unwrap().iter().map(|b| b.clone().key.unwrap()).collect(),
|
||||||
|
Err(err) => return Err(RustFSBucketError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if bucket_list.contains(&bucket_name) {
|
||||||
|
info!("Bucket already exists");
|
||||||
|
} else {
|
||||||
|
if let Err(err) = create_bucket(rustfs_cr.name_any(), bucket_name.clone(), bucket_cr.spec.versioning, bucket_cr.spec.object_lock) {
|
||||||
|
return Err(RustFSBucketError::RcCliError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.ready = true;
|
||||||
|
status.conditions = set_condition(
|
||||||
|
status.conditions,
|
||||||
|
bucket_cr.metadata.clone(),
|
||||||
|
TYPE_BUCKET_READY,
|
||||||
|
"True".to_string(),
|
||||||
|
"Reconciled".to_string(),
|
||||||
|
"Bucket is ready".to_string(),
|
||||||
|
);
|
||||||
|
status.endpoint = Some(rustfs_cr.clone().spec.endpoint);
|
||||||
|
status.region = Some(rustfs_cr.clone().status.unwrap().region.unwrap());
|
||||||
|
status.bucket_name = Some(bucket_name.clone());
|
||||||
|
bucket_cr.status = Some(status);
|
||||||
|
|
||||||
|
info!("Updating status of the bucket resource");
|
||||||
|
match bucket_api
|
||||||
|
.replace_status(&bucket_cr.name_any(), &PostParams::default(), &bucket_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::requeue(Duration::from_secs(120))),
|
||||||
|
Err(err) => return Err(RustFSBucketError::KubeError(err)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap the object by adding a default status to it
|
||||||
|
async fn init_object(mut obj: RustFSBucket, api: Api<RustFSBucket>) -> Result<Action, RustFSBucketError> {
|
||||||
|
let conditions = set_condition(
|
||||||
|
vec![],
|
||||||
|
obj.metadata.clone(),
|
||||||
|
TYPE_BUCKET_READY,
|
||||||
|
"Unknown".to_string(),
|
||||||
|
"Reconciling".to_string(),
|
||||||
|
"Reconciliation started".to_string(),
|
||||||
|
);
|
||||||
|
obj.status = Some(RustFSBucketStatus {
|
||||||
|
conditions,
|
||||||
|
..RustFSBucketStatus::default()
|
||||||
|
});
|
||||||
|
match api
|
||||||
|
.replace_status(obj.clone().name_any().as_str(), &Default::default(), &obj)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(Action::await_change()),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
Err(RustFSBucketError::KubeError(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the configmap with the bucket data
|
||||||
|
async fn get_configmap(api: Api<ConfigMap>, name: &str) -> Result<ConfigMap, kube::Error> {
|
||||||
|
info!("Getting a configmap: {}", name);
|
||||||
|
match api.get(name).await {
|
||||||
|
Ok(cm) => Ok(cm),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ConfigMap
|
||||||
|
async fn create_configmap(api: Api<ConfigMap>, cm: ConfigMap) -> Result<ConfigMap, kube::Error> {
|
||||||
|
match api.create(&PostParams::default(), &cm).await {
|
||||||
|
Ok(cm) => get_configmap(api, &cm.name_any()).await,
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn label_configmap(
|
||||||
|
api: Api<ConfigMap>,
|
||||||
|
bucket_name: &str,
|
||||||
|
mut cm: ConfigMap,
|
||||||
|
) -> Result<ConfigMap, kube::Error> {
|
||||||
|
let mut labels = match &cm.clone().metadata.labels {
|
||||||
|
Some(labels) => labels.clone(),
|
||||||
|
None => {
|
||||||
|
let map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
labels.insert(CONFIGMAP_LABEL.to_string(), bucket_name.to_string());
|
||||||
|
cm.metadata.labels = Some(labels);
|
||||||
|
api.replace(&cm.name_any(), &PostParams::default(), &cm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cm = match api.get(&cm.name_any()).await {
|
||||||
|
Ok(cm) => cm,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(cm)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn own_configmap(
|
||||||
|
api: Api<ConfigMap>,
|
||||||
|
bucket_cr: RustFSBucket,
|
||||||
|
mut cm: ConfigMap,
|
||||||
|
) -> Result<ConfigMap, kube::Error> {
|
||||||
|
let mut owner_references = match &cm.clone().metadata.owner_references {
|
||||||
|
Some(owner_references) => owner_references.clone(),
|
||||||
|
None => {
|
||||||
|
let owner_references: Vec<OwnerReference> = vec![];
|
||||||
|
owner_references
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if owner_references
|
||||||
|
.iter()
|
||||||
|
.find(|or| or.uid == bucket_cr.uid().unwrap())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Ok(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_owner_reference = OwnerReference {
|
||||||
|
api_version: RustFSBucket::api_version(&()).into(),
|
||||||
|
kind: RustFSBucket::kind(&()).into(),
|
||||||
|
name: bucket_cr.name_any(),
|
||||||
|
uid: bucket_cr.uid().unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
owner_references.push(new_owner_reference);
|
||||||
|
cm.metadata.owner_references = Some(owner_references);
|
||||||
|
api.replace(&cm.name_any(), &PostParams::default(), &cm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cm = match api.get(&cm.name_any()).await {
|
||||||
|
Ok(cm) => cm,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(cm)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_data_configmap(
|
||||||
|
api: Api<ConfigMap>,
|
||||||
|
rustfs_cr: RustFSInstance,
|
||||||
|
mut cm: ConfigMap,
|
||||||
|
) -> Result<ConfigMap, kube::Error> {
|
||||||
|
let mut data = match &cm.clone().data {
|
||||||
|
Some(data) => data.clone(),
|
||||||
|
None => {
|
||||||
|
let map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
data.insert(AWS_REGION.to_string(), rustfs_cr.status.unwrap().region.unwrap());
|
||||||
|
data.insert(AWS_ENDPOINT_URL.to_string(), rustfs_cr.spec.endpoint);
|
||||||
|
|
||||||
|
cm.data = Some(data);
|
||||||
|
api.replace(&cm.name_any(), &PostParams::default(), &cm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match api.get(&cm.name_any()).await {
|
||||||
|
Ok(cm) => Ok(cm),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn error_policy(_: Arc<RustFSBucket>, err: &RustFSBucketError, _: Arc<Context>) -> Action {
|
||||||
|
error!(trace.error = %err, "Error occured during the reconciliation");
|
||||||
|
Action::requeue(Duration::from_secs(5 * 60))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(client), fields(trace_id))]
|
||||||
|
pub async fn run(client: Client, config: OperatorConfig) {
|
||||||
|
let buckets = Api::<RustFSBucket>::all(client.clone());
|
||||||
|
if let Err(err) = buckets.list(&ListParams::default().limit(1)).await {
|
||||||
|
error!("{}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let recorder = Recorder::new(client.clone(), "bucket-controller".into());
|
||||||
|
let context = Context { client, recorder, config };
|
||||||
|
Controller::new(buckets, Config::default().any_semantic())
|
||||||
|
.shutdown_on_signal()
|
||||||
|
.run(reconcile, error_policy, Arc::new(context))
|
||||||
|
.filter_map(|x| async move { std::result::Result::ok(x) })
|
||||||
|
.for_each(|_| futures::future::ready(()))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
// Context for our reconciler
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Context {
|
||||||
|
/// Kubernetes client
|
||||||
|
pub client: Client,
|
||||||
|
/// Event recorder
|
||||||
|
pub recorder: Recorder,
|
||||||
|
pub(crate) config: OperatorConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RustFSBucketError {
|
||||||
|
#[error("Kube Error: {0}")]
|
||||||
|
KubeError(#[source] kube::Error),
|
||||||
|
#[error("Error while executing rc cli: {0}")]
|
||||||
|
RcCliError(#[source] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RustFSBucketResult<T, E = RustFSBucketError> = std::result::Result<T, E>;
|
||||||
379
operator/src/controllers/rustfs_instance.rs
Normal file
379
operator/src/controllers/rustfs_instance.rs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
use crate::conditions::{init_conditions, is_condition_true, is_condition_unknown, set_condition};
|
||||||
|
use crate::rc::{RcAlias, admin_info, get_aliases, list_buckets, set_alias};
|
||||||
|
use api::api::v1beta1_rustfs_instance::{RustFSInstance, RustFSInstanceStatus};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use k8s_openapi::api::core::v1::Secret;
|
||||||
|
use kube::api::{ListParams, PostParams};
|
||||||
|
use kube::runtime::Controller;
|
||||||
|
use kube::runtime::controller::Action;
|
||||||
|
use kube::runtime::events::Recorder;
|
||||||
|
use kube::runtime::watcher::Config;
|
||||||
|
use kube::{Api, Client, Error, ResourceExt};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
const TYPE_CONNECTED: &str = "RustFSReady";
|
||||||
|
const TYPE_SECRET_LABELED: &str = "SecretLabeled";
|
||||||
|
const FIN_SECRET_LABEL: &str = "s3.badhouseplants.net/s3-label";
|
||||||
|
const SECRET_LABEL: &str = "s3.badhouseplants.net/s3-instance";
|
||||||
|
|
||||||
|
pub(crate) const ACCESS_KEY: &str = "ACCESS_KEY";
|
||||||
|
pub(crate) const SECRET_KEY: &str = "SECRET_KEY";
|
||||||
|
|
||||||
|
#[instrument(skip(ctx, req), fields(trace_id, controller = "rustfs-instance"))]
|
||||||
|
pub(crate) async fn reconcile(req: Arc<RustFSInstance>, ctx: Arc<Context>) -> RustFSInstanceResult<Action> {
|
||||||
|
info!("Staring to reconcile");
|
||||||
|
|
||||||
|
info!("Getting the RustFSInstance resource");
|
||||||
|
let rustfs_api: Api<RustFSInstance> = Api::all(ctx.client.clone());
|
||||||
|
let mut rustfs_cr = match rustfs_api.get(req.name_any().as_str()).await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(Error::Api(ae)) if ae.code == 404 => {
|
||||||
|
info!("Object is not found, probably removed");
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_ns = rustfs_cr.clone().spec.credentials_secret.namespace;
|
||||||
|
let secret_api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
|
||||||
|
|
||||||
|
// If status is none, we need to initialize the object
|
||||||
|
let mut status = match rustfs_cr.clone().status {
|
||||||
|
None => {
|
||||||
|
info!("Status is not yet set, initializing the object");
|
||||||
|
return init_object(rustfs_cr, rustfs_api).await;
|
||||||
|
}
|
||||||
|
Some(status) => status,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to know the secret before deletion, because the operator needs to unlabel it
|
||||||
|
let secret = match get_secret(secret_api.clone(), rustfs_cr.clone()).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the deletion logic
|
||||||
|
if rustfs_cr.metadata.deletion_timestamp.is_some() {
|
||||||
|
info!("Object is marked for deletion");
|
||||||
|
if let Some(mut finalizers) = rustfs_cr.clone().metadata.finalizers {
|
||||||
|
if finalizers.contains(&FIN_SECRET_LABEL.to_string()) {
|
||||||
|
info!("Removing labels from the secret with credentials");
|
||||||
|
match unlabel_secret(ctx.clone(), rustfs_cr.clone(), secret).await {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Some(index) = finalizers
|
||||||
|
.iter()
|
||||||
|
.position(|x| x == FIN_SECRET_LABEL)
|
||||||
|
{
|
||||||
|
finalizers.remove(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rustfs_cr.metadata.finalizers = Some(finalizers);
|
||||||
|
};
|
||||||
|
|
||||||
|
match rustfs_api
|
||||||
|
.replace(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If secret is labeled, add a finalizer to the rustfs_cr
|
||||||
|
if is_condition_true(status.clone().conditions, TYPE_SECRET_LABELED) {
|
||||||
|
let mut current_finalizers = rustfs_cr.clone().metadata.finalizers.unwrap_or_default();
|
||||||
|
// Only if the finalizer is not added yet
|
||||||
|
if !current_finalizers.contains(&FIN_SECRET_LABEL.to_string()) {
|
||||||
|
info!("Adding a finalizer");
|
||||||
|
current_finalizers.push(FIN_SECRET_LABEL.to_string());
|
||||||
|
|
||||||
|
rustfs_cr.metadata.finalizers = Some(current_finalizers);
|
||||||
|
match rustfs_api
|
||||||
|
.replace(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label the secret, if not yet labeled
|
||||||
|
if is_condition_unknown(status.clone().conditions, TYPE_SECRET_LABELED) {
|
||||||
|
if is_secret_labeled(secret.clone()) {
|
||||||
|
if is_secret_labeled_by_another_obj(rustfs_cr.clone(), secret.clone()) {
|
||||||
|
return Err(RustFSInstanceError::SecretIsAlreadyLabeled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_secret_labeled_by_obj(rustfs_cr.clone(), secret.clone()) {
|
||||||
|
info!("Secret is already labeled");
|
||||||
|
status.conditions = set_condition(
|
||||||
|
status.clone().conditions,
|
||||||
|
req.metadata.clone(),
|
||||||
|
TYPE_SECRET_LABELED,
|
||||||
|
"True".to_string(),
|
||||||
|
"Reconciled".to_string(),
|
||||||
|
"Secret is already labeled".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Labeling the secret");
|
||||||
|
if let Err(err) = label_secret(ctx.clone(), rustfs_cr.clone(), secret).await {
|
||||||
|
return Err(RustFSInstanceError::KubeError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
status.conditions = set_condition(
|
||||||
|
status.clone().conditions,
|
||||||
|
req.metadata.clone(),
|
||||||
|
TYPE_SECRET_LABELED,
|
||||||
|
"True".to_string(),
|
||||||
|
"Reconciled".to_string(),
|
||||||
|
"Secret is labeled".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
rustfs_cr.status = Some(status);
|
||||||
|
match rustfs_api
|
||||||
|
.replace_status(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Checking if the secret is labeled by another object");
|
||||||
|
if !is_secret_labeled_by_obj(rustfs_cr.clone(), secret.clone()) {
|
||||||
|
status.conditions = set_condition(
|
||||||
|
status.conditions,
|
||||||
|
rustfs_cr.clone().metadata,
|
||||||
|
TYPE_SECRET_LABELED,
|
||||||
|
"Unknown".to_string(),
|
||||||
|
"RustFSInstanceReconciliation".to_string(),
|
||||||
|
"Secret is not labeled".to_string(),
|
||||||
|
);
|
||||||
|
rustfs_cr.status = Some(status);
|
||||||
|
match rustfs_api
|
||||||
|
.replace_status(&rustfs_cr.clone().name_any(), &PostParams::default(), &rustfs_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Getting data from the secret");
|
||||||
|
let (access_key, secret_key) = match get_data_from_secret(secret) {
|
||||||
|
Ok((ak, sk)) => (ak, sk),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::InvalidSecret(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_aliases = match get_aliases() {
|
||||||
|
Ok(aliases) => aliases,
|
||||||
|
Err(err) => return Err(RustFSInstanceError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if alias already exists
|
||||||
|
if current_aliases.aliases.is_none_or(|a| !a.contains(&RcAlias{name: rustfs_cr.name_any().to_string()}))
|
||||||
|
&& let Err(err) = set_alias(rustfs_cr.name_any(), rustfs_cr.clone().spec.endpoint, access_key.clone(), secret_key.clone()){
|
||||||
|
return Err(RustFSInstanceError::RcCliError(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
let admin_info = match admin_info(rustfs_cr.name_any().to_string()){
|
||||||
|
Ok(ai) => ai,
|
||||||
|
Err(err) =>return Err(RustFSInstanceError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bucket_list = match list_buckets(rustfs_cr.name_any().to_string()){
|
||||||
|
Ok(bl) => bl,
|
||||||
|
Err(err) => return Err(RustFSInstanceError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
status.ready = true;
|
||||||
|
status.total_buckets = admin_info.buckets;
|
||||||
|
status.region = admin_info.region;
|
||||||
|
status.buckets = Some(bucket_list.items.unwrap().iter().map(|b| b.clone().key.unwrap()).collect());
|
||||||
|
rustfs_cr.status = Some(status);
|
||||||
|
|
||||||
|
match rustfs_api
|
||||||
|
.replace_status(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::requeue(Duration::from_secs(120))),
|
||||||
|
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap the object by adding a default status to it
|
||||||
|
async fn init_object(mut obj: RustFSInstance, api: Api<RustFSInstance>) -> Result<Action, RustFSInstanceError> {
|
||||||
|
let conditions = init_conditions(vec![TYPE_CONNECTED.to_string(), TYPE_SECRET_LABELED.to_string()]);
|
||||||
|
obj.status = Some(RustFSInstanceStatus {
|
||||||
|
conditions,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
match api
|
||||||
|
.replace_status(obj.clone().name_any().as_str(), &Default::default(), &obj)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(Action::await_change()),
|
||||||
|
Err(err) => Err(RustFSInstanceError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the secret with credentials
|
||||||
|
pub(crate) async fn get_secret(api: Api<Secret>, obj: RustFSInstance) -> Result<Secret, kube::Error> {
|
||||||
|
api.get(&obj.spec.credentials_secret.name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlabel_secret(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
obj: RustFSInstance,
|
||||||
|
mut secret: Secret,
|
||||||
|
) -> Result<(), kube::Error> {
|
||||||
|
let secret_ns = obj.clone().spec.credentials_secret.namespace;
|
||||||
|
let api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
|
||||||
|
if let Some(mut labels) = secret.clone().metadata.labels {
|
||||||
|
labels.remove(SECRET_LABEL);
|
||||||
|
secret.metadata.labels = Some(labels);
|
||||||
|
api.replace(&secret.name_any(), &PostParams::default(), &secret)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn label_secret(
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
obj: RustFSInstance,
|
||||||
|
mut secret: Secret,
|
||||||
|
) -> Result<Secret, kube::Error> {
|
||||||
|
let secret_ns = obj.clone().spec.credentials_secret.namespace;
|
||||||
|
let api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
|
||||||
|
|
||||||
|
secret
|
||||||
|
.clone()
|
||||||
|
.metadata
|
||||||
|
.labels
|
||||||
|
.get_or_insert_with(BTreeMap::new)
|
||||||
|
.insert(SECRET_LABEL.to_string(), obj.name_any());
|
||||||
|
|
||||||
|
let mut labels = match &secret.clone().metadata.labels {
|
||||||
|
Some(labels) => labels.clone(),
|
||||||
|
None => {
|
||||||
|
let map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
labels.insert(SECRET_LABEL.to_string(), obj.name_any());
|
||||||
|
secret.metadata.labels = Some(labels);
|
||||||
|
api.replace(&secret.name_any(), &PostParams::default(), &secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let secret = match api.get(&obj.spec.credentials_secret.name).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a secret ia already labeled by the operator
|
||||||
|
fn is_secret_labeled(secret: Secret) -> bool {
|
||||||
|
match secret.metadata.labels {
|
||||||
|
Some(labels) => labels.get_key_value(SECRET_LABEL).is_some(),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a secret is already labeled by another object
|
||||||
|
fn is_secret_labeled_by_another_obj(obj: RustFSInstance, secret: Secret) -> bool {
|
||||||
|
match secret.metadata.labels {
|
||||||
|
Some(labels) => labels
|
||||||
|
.get(SECRET_LABEL)
|
||||||
|
.is_some_and(|label| label != &obj.name_any()),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a secret is already labeled by this object
|
||||||
|
fn is_secret_labeled_by_obj(obj: RustFSInstance, secret: Secret) -> bool {
|
||||||
|
match secret.metadata.labels {
|
||||||
|
Some(labels) => labels
|
||||||
|
.get(SECRET_LABEL)
|
||||||
|
.is_some_and(|label| label == &obj.name_any()),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns (access_key, secret_key)
|
||||||
|
pub(crate) fn get_data_from_secret(secret: Secret) -> Result<(String, String), anyhow::Error> {
|
||||||
|
let data = match secret.data {
|
||||||
|
Some(data) => data,
|
||||||
|
None => return Err(anyhow::Error::msg("empty data")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let access_key = match data.get(ACCESS_KEY) {
|
||||||
|
Some(access_key) => String::from_utf8(access_key.0.clone()).unwrap(),
|
||||||
|
None =>return Err(anyhow::Error::msg("empty access key")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_key = match data.get(SECRET_KEY) {
|
||||||
|
Some(secret_key) => String::from_utf8(secret_key.0.clone()).unwrap(),
|
||||||
|
None => return Err(anyhow::Error::msg("empty secret key")),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((access_key, secret_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn error_policy(_rustfs_cr: Arc<RustFSInstance>, err: &RustFSInstanceError, _ctx: Arc<Context>) -> Action {
|
||||||
|
error!(trace.error = %err, "Error occured during the reconciliation");
|
||||||
|
Action::requeue(Duration::from_secs(5 * 60))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(client), fields(trace_id))]
|
||||||
|
pub async fn run(client: Client) {
|
||||||
|
let s3instances = Api::<RustFSInstance>::all(client.clone());
|
||||||
|
if let Err(err) = s3instances.list(&ListParams::default().limit(1)).await {
|
||||||
|
error!("{}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let recorder = Recorder::new(client.clone(), "s3instance-controller".into());
|
||||||
|
let context = Context { client, recorder };
|
||||||
|
Controller::new(s3instances, Config::default().any_semantic())
|
||||||
|
.shutdown_on_signal()
|
||||||
|
.run(reconcile, error_policy, Arc::new(context))
|
||||||
|
.filter_map(|x| async move { std::result::Result::ok(x) })
|
||||||
|
.for_each(|_| futures::future::ready(()))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context for our reconciler
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Context {
|
||||||
|
/// Kubernetes client
|
||||||
|
pub client: Client,
|
||||||
|
/// Event recorder
|
||||||
|
pub recorder: Recorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RustFSInstanceError {
|
||||||
|
#[error("Kube Error: {0}")]
|
||||||
|
KubeError(#[source] kube::Error),
|
||||||
|
|
||||||
|
#[error("SecretIsAlreadyLabeled")]
|
||||||
|
SecretIsAlreadyLabeled,
|
||||||
|
|
||||||
|
#[error("Invalid Secret: {0}")]
|
||||||
|
InvalidSecret(#[source] anyhow::Error),
|
||||||
|
|
||||||
|
#[error("Error while executing rc cli: {0}")]
|
||||||
|
RcCliError(#[source] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RustFSInstanceResult<T, E = RustFSInstanceError> = std::result::Result<T, E>;
|
||||||
611
operator/src/controllers/rustfs_user.rs
Normal file
611
operator/src/controllers/rustfs_user.rs
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
use crate::conditions::{init_conditions, is_condition_true, is_condition_unknown, set_condition};
|
||||||
|
use crate::config::OperatorConfig;
|
||||||
|
use crate::rc::{POLICY_READ_ONLY, POLICY_READ_WRITE, RcPolicyData, assign_policy, create_bucket, create_policy, create_user, delete_user, list_buckets, render_policy, user_info};
|
||||||
|
use api::api::v1beta_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta_rustfs_user::{RustFSUser, RustFSUserStatus};
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use k8s_openapi::ByteString;
|
||||||
|
use k8s_openapi::api::core::v1::Secret;
|
||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
|
||||||
|
use kube::api::{ListParams, ObjectMeta, PostParams};
|
||||||
|
use kube::runtime::Controller;
|
||||||
|
use kube::runtime::controller::Action;
|
||||||
|
use kube::runtime::events::Recorder;
|
||||||
|
use kube::runtime::watcher::Config;
|
||||||
|
use kube::{Api, Client, Error, Resource, ResourceExt};
|
||||||
|
use rand::RngExt;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
const TYPE_USER_READY: &str = "UserReady";
|
||||||
|
const TYPE_SECRET_READY: &str = "SecretReady";
|
||||||
|
const FIN_CLEANUP: &str = "s3.badhouseplants.net/bucket-cleanup";
|
||||||
|
const CONFIGMAP_LABEL: &str = "s3.badhouseplants.net/s3-bucket";
|
||||||
|
|
||||||
|
const AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID";
|
||||||
|
const AWS_SECCRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
|
||||||
|
|
||||||
|
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
|
||||||
|
abcdefghijklmnopqrstuvwxyz\
|
||||||
|
0123456789)(*&^%$#@!~";
|
||||||
|
const PASSWORD_LEN: usize = 40;
|
||||||
|
|
||||||
|
#[instrument(skip(ctx, obj), fields(trace_id))]
|
||||||
|
pub(crate) async fn reconcile(obj: Arc<RustFSUser>, ctx: Arc<Context>) -> RustFSUserResult<Action> {
|
||||||
|
info!("Staring reconciling");
|
||||||
|
let user_api: Api<RustFSUser> =
|
||||||
|
Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
|
||||||
|
let bucket_api: Api<RustFSBucket> =
|
||||||
|
Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
|
||||||
|
let secret_api: Api<Secret> = Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
|
||||||
|
let rustfs_api: Api<RustFSInstance> = Api::all(ctx.client.clone());
|
||||||
|
|
||||||
|
info!("Getting the RustFSUser resource");
|
||||||
|
let mut user_cr = match user_api.get(&obj.name_any()).await {
|
||||||
|
Ok(cr) => cr,
|
||||||
|
Err(Error::Api(ae)) if ae.code == 404 => {
|
||||||
|
info!("Object is not found, probably removed");
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// On the first reconciliation status is None
|
||||||
|
// it needs to be initialized
|
||||||
|
let mut status = match user_cr.clone().status {
|
||||||
|
None => {
|
||||||
|
info!("Status is not yet set, initializing the object");
|
||||||
|
return init_object(user_cr, user_api).await;
|
||||||
|
}
|
||||||
|
Some(status) => status,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_name = format!("{}-bucket-creds", user_cr.name_any());
|
||||||
|
|
||||||
|
info!("Getting the secret");
|
||||||
|
// Get the secret, if it's already there, we need to validate, or create an empty one
|
||||||
|
let mut secret = match get_secret(secret_api.clone(), &secret_name).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(Error::Api(ae)) if ae.code == 404 => {
|
||||||
|
info!("Secret is not found, creating a new one");
|
||||||
|
let secret = Secret {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(secret_name),
|
||||||
|
namespace: Some(user_cr.clone().namespace().unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match create_secret(secret_api.clone(), secret).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Labeling the secret");
|
||||||
|
secret = match label_secret(secret_api.clone(), &user_cr.name_any(), secret).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Setting owner references to the secret");
|
||||||
|
if ctx.config.set_owner_reference {
|
||||||
|
secret = match own_secret(secret_api.clone(), user_cr.clone(), secret).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// TODO: It shouldn't stop reconciliation
|
||||||
|
if is_condition_true(status.clone().conditions, TYPE_USER_READY) {
|
||||||
|
let mut current_finalizers = match user_cr.clone().metadata.finalizers {
|
||||||
|
Some(finalizers) => finalizers,
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
if user_cr.spec.cleanup {
|
||||||
|
if !current_finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
info!("Adding a finalizer");
|
||||||
|
current_finalizers.push(FIN_CLEANUP.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if current_finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
if let Some(index) = current_finalizers
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x == FIN_CLEANUP.to_string())
|
||||||
|
{
|
||||||
|
current_finalizers.remove(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
user_cr.metadata.finalizers = Some(current_finalizers);
|
||||||
|
if let Err(err) = user_api
|
||||||
|
.replace(&user_cr.name_any(), &PostParams::default(), &user_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
info!("Getting the RustFsBucket");
|
||||||
|
let bucket_cr = match bucket_api.get(&user_cr.spec.bucket).await {
|
||||||
|
Ok(bucket) => bucket,
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bucket_status = match bucket_cr.clone().status {
|
||||||
|
Some(status) => {
|
||||||
|
if ! status.ready {
|
||||||
|
return Err(RustFSUserError::BucketNotReadyError);
|
||||||
|
};
|
||||||
|
status
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(RustFSUserError::BucketNotReadyError);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Getting the RustFSIntsance");
|
||||||
|
let rustfs_cr = match rustfs_api.get(&bucket_cr.spec.instance).await {
|
||||||
|
Ok(rustfs_cr) => rustfs_cr,
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if rustfs_cr.clone().status.is_none_or(|s| ! s.ready) {
|
||||||
|
info!("Instance is not ready, waiting");
|
||||||
|
return Ok(Action::requeue(Duration::from_secs(120)));
|
||||||
|
}
|
||||||
|
// Check the secret
|
||||||
|
let username = format!(
|
||||||
|
"{}-{}",
|
||||||
|
user_cr.namespace().unwrap(),
|
||||||
|
user_cr.name_any()
|
||||||
|
);
|
||||||
|
|
||||||
|
// If password missing, regen the secret
|
||||||
|
// Update the user
|
||||||
|
if user_cr.metadata.deletion_timestamp.is_some() {
|
||||||
|
info!("Object is marked for deletion");
|
||||||
|
if let Some(mut finalizers) = user_cr.clone().metadata.finalizers {
|
||||||
|
if finalizers.contains(&FIN_CLEANUP.to_string()) {
|
||||||
|
match delete_user(rustfs_cr.name_any(), username){
|
||||||
|
Ok(_) => {
|
||||||
|
if let Some(index) = finalizers
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x == FIN_CLEANUP.to_string())
|
||||||
|
{
|
||||||
|
finalizers.remove(index);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => return Err(RustFSUserError::RcCliError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user_cr.metadata.finalizers = Some(finalizers);
|
||||||
|
};
|
||||||
|
match user_api
|
||||||
|
.replace(&user_cr.name_any(), &PostParams::default(), &user_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(Action::await_change()),
|
||||||
|
Err(err) => return Err(RustFSUserError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If secret is not ready, generate a new one
|
||||||
|
if !is_condition_true(status.clone().conditions, TYPE_SECRET_READY) {
|
||||||
|
let password = generate_password();
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let password_hash = match argon2.hash_password(&password.as_bytes(), &salt) {
|
||||||
|
Ok(hash) => hash.to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::IllegalRustFSUser);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
status.password_hash = Some(password_hash);
|
||||||
|
match set_secret_data(secret_api.clone(), secret.clone(), username.clone(), password.clone()).await {
|
||||||
|
Ok(_) => {
|
||||||
|
status.conditions = set_condition(status.clone().conditions, user_cr.clone().metadata, TYPE_SECRET_READY, "True".to_string(), "Reconciled".to_string(), "Secret is up-to-date".to_string());
|
||||||
|
user_cr.status = Some(status);
|
||||||
|
match user_api.replace_status(&user_cr.name_any(), &PostParams::default(), &user_cr).await {
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let credentials = match secret.data {
|
||||||
|
Some(data) => {
|
||||||
|
match check_credentials(data, username, status.clone().password_hash) {
|
||||||
|
Some(creds) => creds,
|
||||||
|
None => {
|
||||||
|
status.conditions = set_condition(status.clone().conditions, user_cr.clone().metadata, TYPE_SECRET_READY, "False".to_string(), "Reconciled".to_string(), "Invalid credentials in the secret".to_string());
|
||||||
|
user_cr.status = Some(status);
|
||||||
|
info!("I'm setting the condition");
|
||||||
|
match user_api.replace_status(&user_cr.name_any(), &PostParams::default(), &user_cr).await {
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
status.clone().conditions = set_condition(status.clone().conditions, user_cr.clone().metadata, TYPE_SECRET_READY, "False".to_string(), "Reconciled".to_string(), "Invalid credentials in the secret".to_string());
|
||||||
|
user_cr.status = Some(status);
|
||||||
|
match user_api.replace_status(&user_cr.name_any(), &PostParams::default(), &user_cr).await {
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(Action::await_change());
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = credentials.0;
|
||||||
|
let password = credentials.1;
|
||||||
|
|
||||||
|
if let Err(err) = create_user(rustfs_cr.name_any(), username.clone(), password) {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::IllegalRustFSUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userinfo = match user_info(rustfs_cr.name_any(), username.clone()) {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(err) => return Err(RustFSUserError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let policy_template = match user_cr.spec.access {
|
||||||
|
api::api::v1beta_rustfs_user::Access::ReadOnly => POLICY_READ_ONLY,
|
||||||
|
api::api::v1beta_rustfs_user::Access::ReadWrite => POLICY_READ_WRITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bucket_name = match bucket_status.bucket_name {
|
||||||
|
Some(name) => name,
|
||||||
|
None => {
|
||||||
|
error!("bucket name is not yet set");
|
||||||
|
return Err(RustFSUserError::IllegalRustFSUser);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = RcPolicyData{bucket: bucket_name};
|
||||||
|
let policy = match render_policy(policy_template.to_string(), data){
|
||||||
|
Ok(policy) => policy,
|
||||||
|
Err(err) => return Err(RustFSUserError::RcCliError(anyhow!(err))),
|
||||||
|
};
|
||||||
|
if let Err(err) = create_policy(rustfs_cr.name_any(), username.clone(), policy_template.to_string()) {
|
||||||
|
return Err(RustFSUserError::RcCliError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = assign_policy(rustfs_cr.name_any(), username.clone()) {
|
||||||
|
return Err(RustFSUserError::RcCliError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a user
|
||||||
|
status.policy = Some(policy);
|
||||||
|
status.username = Some(userinfo.access_key);
|
||||||
|
status.status = Some(userinfo.status);
|
||||||
|
status.ready = true;
|
||||||
|
status.conditions = set_condition(
|
||||||
|
status.clone().conditions,
|
||||||
|
user_cr.metadata.clone(),
|
||||||
|
TYPE_USER_READY,
|
||||||
|
"True".to_string(),
|
||||||
|
"Reconciled".to_string(),
|
||||||
|
"User is ready".to_string(),
|
||||||
|
);
|
||||||
|
user_cr.status = Some(status);
|
||||||
|
|
||||||
|
info!("Updating status of the bucket resource");
|
||||||
|
match user_api
|
||||||
|
.replace_status(&user_cr.name_any(), &PostParams::default(), &user_cr)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(Action::requeue(Duration::from_secs(120)));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap the object by adding a default status to it
|
||||||
|
async fn init_object(mut obj: RustFSUser, api: Api<RustFSUser>) -> Result<Action, RustFSUserError> {
|
||||||
|
|
||||||
|
let conditions = init_conditions(vec![TYPE_SECRET_READY.to_string(), TYPE_USER_READY.to_string()]);
|
||||||
|
obj.status = Some(RustFSUserStatus {
|
||||||
|
conditions,
|
||||||
|
..RustFSUserStatus::default()
|
||||||
|
});
|
||||||
|
match api
|
||||||
|
.replace_status(obj.clone().name_any().as_str(), &Default::default(), &obj)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(Action::await_change()),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
Err(RustFSUserError::KubeError(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the secret with the bucket data
|
||||||
|
async fn get_secret(api: Api<Secret>, name: &str) -> Result<Secret, kube::Error> {
|
||||||
|
info!("Getting a secret: {}", name);
|
||||||
|
api.get(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// checks if the secret has all the required data
|
||||||
|
fn check_secret_data(secret: Secret) -> bool {
|
||||||
|
let data = match secret.data {
|
||||||
|
Some(data) => data,
|
||||||
|
None => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
data.contains_key(AWS_SECCRET_ACCESS_KEY) && data.contains_key(AWS_ACCESS_KEY_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Returns false if password is not valid
|
||||||
|
fn check_credentials(data: BTreeMap<String, ByteString>, username: String, password_hash: Option<String>) -> Option<(String, String)> {
|
||||||
|
let current_username = match data.get(AWS_ACCESS_KEY_ID) {
|
||||||
|
Some(username) => String::from_utf8(username.0.clone()).unwrap(),
|
||||||
|
None => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("Username is there");
|
||||||
|
|
||||||
|
let current_password = match data.get(AWS_SECCRET_ACCESS_KEY) {
|
||||||
|
Some(password) => String::from_utf8(password.0.clone()).unwrap(),
|
||||||
|
None => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("Password is there");
|
||||||
|
|
||||||
|
if current_username != username {
|
||||||
|
return None
|
||||||
|
};
|
||||||
|
info!("hash is {:?}", password_hash);
|
||||||
|
info!("Username is fine");
|
||||||
|
if let Some(password_hash) = password_hash {
|
||||||
|
let parsed_hash = match PasswordHash::new(&password_hash){
|
||||||
|
Ok(hash) => hash,
|
||||||
|
Err(_) => {
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match Argon2::default().verify_password(current_password.as_bytes(), &parsed_hash) {
|
||||||
|
Ok(_) => {
|
||||||
|
return Some((current_username, current_password));
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Secret
|
||||||
|
async fn create_secret(api: Api<Secret>, secret: Secret) -> Result<Secret, kube::Error> {
|
||||||
|
match api.create(&PostParams::default(), &secret).await {
|
||||||
|
Ok(secret) => get_secret(api, &secret.name_any()).await,
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn label_secret(
|
||||||
|
api: Api<Secret>,
|
||||||
|
bucket_name: &str,
|
||||||
|
mut secret: Secret,
|
||||||
|
) -> Result<Secret, kube::Error> {
|
||||||
|
let mut labels = match &secret.clone().metadata.labels {
|
||||||
|
Some(labels) => labels.clone(),
|
||||||
|
None => {
|
||||||
|
let map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
labels.insert(CONFIGMAP_LABEL.to_string(), bucket_name.to_string());
|
||||||
|
secret.metadata.labels = Some(labels);
|
||||||
|
api.replace(&secret.name_any(), &PostParams::default(), &secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let secret = match api.get(&secret.name_any()).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn own_secret(
|
||||||
|
api: Api<Secret>,
|
||||||
|
user_cr: RustFSUser,
|
||||||
|
mut secret: Secret,
|
||||||
|
) -> Result<Secret, kube::Error> {
|
||||||
|
let mut owner_references = match &secret.clone().metadata.owner_references {
|
||||||
|
Some(owner_references) => owner_references.clone(),
|
||||||
|
None => {
|
||||||
|
let owner_references: Vec<OwnerReference> = vec![];
|
||||||
|
owner_references
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if owner_references
|
||||||
|
.iter()
|
||||||
|
.find(|or| or.uid == user_cr.uid().unwrap())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Ok(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_owner_reference = OwnerReference {
|
||||||
|
api_version: RustFSUser::api_version(&()).into(),
|
||||||
|
kind: RustFSUser::kind(&()).into(),
|
||||||
|
name: user_cr.name_any(),
|
||||||
|
uid: user_cr.uid().unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
owner_references.push(new_owner_reference);
|
||||||
|
secret.metadata.owner_references = Some(owner_references);
|
||||||
|
api.replace(&secret.name_any(), &PostParams::default(), &secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let secret = match api.get(&secret.name_any()).await {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_secret_data(
|
||||||
|
api: Api<Secret>,
|
||||||
|
mut secret: Secret,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<Secret, kube::Error> {
|
||||||
|
let mut data = match &secret.clone().data {
|
||||||
|
Some(data) => data.clone(),
|
||||||
|
None => {
|
||||||
|
let map: BTreeMap<String, ByteString> = BTreeMap::new();
|
||||||
|
map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
data.insert(
|
||||||
|
AWS_ACCESS_KEY_ID.to_string(),
|
||||||
|
ByteString(username.as_bytes().to_vec()),
|
||||||
|
);
|
||||||
|
data.insert(
|
||||||
|
AWS_SECCRET_ACCESS_KEY.to_string(),
|
||||||
|
ByteString(password.as_bytes().to_vec()),
|
||||||
|
);
|
||||||
|
|
||||||
|
secret.data = Some(data);
|
||||||
|
api.replace(&secret.name_any(), &PostParams::default(), &secret)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match api.get(&secret.name_any()).await {
|
||||||
|
Ok(secret) => Ok(secret),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_password() -> String {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let password: String = (0..PASSWORD_LEN)
|
||||||
|
.map(|_| {
|
||||||
|
let idx = rng.random_range(0..CHARSET.len());
|
||||||
|
char::from(CHARSET[idx])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
password
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn error_policy(_: Arc<RustFSUser>, err: &RustFSUserError, _: Arc<Context>) -> Action {
|
||||||
|
error!(trace.error = %err, "Error occured during the reconciliation");
|
||||||
|
Action::requeue(Duration::from_secs(5 * 60))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(client), fields(trace_id))]
|
||||||
|
pub async fn run(client: Client, config: OperatorConfig) {
|
||||||
|
let users = Api::<RustFSUser>::all(client.clone());
|
||||||
|
if let Err(err) = users.list(&ListParams::default().limit(1)).await {
|
||||||
|
error!("{}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let recorder = Recorder::new(client.clone(), "user-controller".into());
|
||||||
|
let context = Context { client, recorder, config };
|
||||||
|
Controller::new(users, Config::default().any_semantic())
|
||||||
|
.shutdown_on_signal()
|
||||||
|
.run(reconcile, error_policy, Arc::new(context))
|
||||||
|
.filter_map(|x| async move { std::result::Result::ok(x) })
|
||||||
|
.for_each(|_| futures::future::ready(()))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
// Context for our reconciler
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Context {
|
||||||
|
/// Kubernetes client
|
||||||
|
pub client: Client,
|
||||||
|
/// Event recorder
|
||||||
|
pub recorder: Recorder,
|
||||||
|
pub(crate) config: OperatorConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RustFSUserError {
|
||||||
|
#[error("SerializationError: {0}")]
|
||||||
|
SerializationError(#[source] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Kube Error: {0}")]
|
||||||
|
KubeError(#[source] kube::Error),
|
||||||
|
|
||||||
|
#[error("Finalizer Error: {0}")]
|
||||||
|
// NB: awkward type because finalizer::Error embeds the reconciler error (which is this)
|
||||||
|
// so boxing this error to break cycles
|
||||||
|
FinalizerError(#[source] Box<kube::runtime::finalizer::Error<RustFSUserError>>),
|
||||||
|
|
||||||
|
#[error("IllegalRustFSUser")]
|
||||||
|
IllegalRustFSUser,
|
||||||
|
|
||||||
|
#[error("SecretIsAlreadyLabeled")]
|
||||||
|
SecretIsAlreadyLabeled,
|
||||||
|
|
||||||
|
#[error("Invalid Secret: {0}")]
|
||||||
|
InvalidSecret(#[source] anyhow::Error),
|
||||||
|
|
||||||
|
#[error("Error while executing rc cli: {0}")]
|
||||||
|
RcCliError(#[source] anyhow::Error),
|
||||||
|
#[error("Bucket is not yet ready")]
|
||||||
|
BucketNotReadyError,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RustFSUserResult<T, E = RustFSUserError> = std::result::Result<T, E>;
|
||||||
19
operator/src/crdgen.rs
Normal file
19
operator/src/crdgen.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use api::api::v1beta_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta_rustfs_user::RustFSUser;
|
||||||
|
|
||||||
|
use kube::CustomResourceExt;
|
||||||
|
fn main() {
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSInstance::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSBucket::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSUser::crd()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
1
operator/src/lib.rs
Normal file
1
operator/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod api;
|
||||||
109
operator/src/main.rs
Normal file
109
operator/src/main.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
mod conditions;
|
||||||
|
mod controllers;
|
||||||
|
mod rc;
|
||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use crate::controllers::{rustfs_instance};
|
||||||
|
use crate::rc::check_rc;
|
||||||
|
|
||||||
|
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, get, middleware};
|
||||||
|
use clap::Parser;
|
||||||
|
use kube::{Client, CustomResourceExt};
|
||||||
|
use tracing::error;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use self::config::read_config_from_file;
|
||||||
|
use self::controllers::{rustfs_bucket, rustfs_user};
|
||||||
|
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use api::api::v1beta_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta_rustfs_user::RustFSUser;
|
||||||
|
|
||||||
|
/// Simple program to greet a person
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(long, default_value_t = 60000)]
|
||||||
|
/// The address the metric endpoint binds to.
|
||||||
|
metrics_port: u16,
|
||||||
|
#[arg(long, default_value_t = 8081)]
|
||||||
|
/// The address the probe endpoint binds to.
|
||||||
|
health_probe_port: u16,
|
||||||
|
#[arg(long, default_value_t = true)]
|
||||||
|
/// Enabling this will ensure there is only one active controller manager.
|
||||||
|
// DB Operator feature flags
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
/// If enabled, DB Operator will run full reconciliation only
|
||||||
|
/// when changes are detected
|
||||||
|
is_change_check_nabled: bool,
|
||||||
|
#[arg(long, default_value = "/srv/config/config.json")]
|
||||||
|
/// A path to a config file
|
||||||
|
config: String,
|
||||||
|
/// Set to true to generate crds
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
print_crd: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/health")]
|
||||||
|
async fn health(_: HttpRequest) -> impl Responder {
|
||||||
|
HttpResponse::Ok().json("healthy")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crdgen() {
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSInstance::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSBucket::crd()).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"---\n{}",
|
||||||
|
serde_yaml::to_string(&RustFSUser::crd()).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
if args.print_crd {
|
||||||
|
crdgen();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.json()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
if let Err(err) = check_rc() {
|
||||||
|
error!("{}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let client = Client::try_default()
|
||||||
|
.await
|
||||||
|
.expect("failed to create kube Client");
|
||||||
|
let config = read_config_from_file(args.config)?;
|
||||||
|
let rustfs_instance_ctrl = rustfs_instance::run(client.clone());
|
||||||
|
let rustfs_bucket_ctrl = rustfs_bucket::run(client.clone(), config.clone());
|
||||||
|
let rustfs_user_ctrl = rustfs_user::run(client.clone(), config.clone());
|
||||||
|
// Start web server
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(middleware::Logger::default().exclude("/health"))
|
||||||
|
.service(health)
|
||||||
|
})
|
||||||
|
.bind("0.0.0.0:8080")?
|
||||||
|
.shutdown_timeout(5);
|
||||||
|
|
||||||
|
// Both runtimes implements graceful shutdown, so poll until both are done
|
||||||
|
tokio::join!(
|
||||||
|
rustfs_instance_ctrl,
|
||||||
|
rustfs_bucket_ctrl,
|
||||||
|
rustfs_user_ctrl,
|
||||||
|
server.run()
|
||||||
|
)
|
||||||
|
.3?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
305
operator/src/rc.rs
Normal file
305
operator/src/rc.rs
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
use handlebars::{Handlebars, RenderError};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::from_str;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::{tempfile, NamedTempFile};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::cli;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcAliasList {
|
||||||
|
pub(crate) aliases: Option<Vec<RcAlias>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcAlias {
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcAdminInfo {
|
||||||
|
pub(crate) buckets: Option<usize>,
|
||||||
|
pub(crate) region: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcBucketList {
|
||||||
|
pub(crate) items: Option<Vec<RcBucket>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcBucket {
|
||||||
|
pub(crate) key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct RcUserInfo {
|
||||||
|
pub(crate) status: String,
|
||||||
|
pub(crate) access_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const POLICY_READ_ONLY: &str = r#"{
|
||||||
|
"ID": "",
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:GetBucketQuota",
|
||||||
|
"s3:GetBucketLocation",
|
||||||
|
"s3:GetObject"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::{{bucket}}",
|
||||||
|
"arn:aws:s3:::{{bucket}}/*"
|
||||||
|
],
|
||||||
|
"Condition": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
pub(crate) const POLICY_READ_WRITE: &str = r#"{
|
||||||
|
"ID": "",
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:*"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::{{bucket}}",
|
||||||
|
"arn:aws:s3:::{{bucket}}/*"
|
||||||
|
],
|
||||||
|
"Condition": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
pub(crate) fn get_aliases() -> Result<RcAliasList, anyhow::Error> {
|
||||||
|
let output = cli::rc_exec(vec!["alias", "list", "--json"])?;
|
||||||
|
let alias_list: RcAliasList = from_str::<RcAliasList>(&output)?;
|
||||||
|
Ok(alias_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_alias(
|
||||||
|
name: String,
|
||||||
|
endpoint: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
cli::rc_exec(vec![
|
||||||
|
"alias",
|
||||||
|
"set",
|
||||||
|
name.as_str(),
|
||||||
|
endpoint.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
password.as_str(),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn admin_info(name: String) -> Result<RcAdminInfo, anyhow::Error> {
|
||||||
|
let output = cli::rc_exec(vec!["admin", "info", "cluster", name.as_str(), "--json"])?;
|
||||||
|
let admin_info: RcAdminInfo = from_str::<RcAdminInfo>(&output)?;
|
||||||
|
Ok(admin_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn list_buckets(name: String) -> Result<RcBucketList, anyhow::Error> {
|
||||||
|
let output = cli::rc_exec(vec!["ls", name.as_str(), "--json"])?;
|
||||||
|
let bucket_list = from_str::<RcBucketList>(&output)?;
|
||||||
|
Ok(bucket_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_bucket(
|
||||||
|
alias: String,
|
||||||
|
bucket_name: String,
|
||||||
|
versioning: bool,
|
||||||
|
object_lock: bool,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let path_string = format!("{}/{}", alias, bucket_name);
|
||||||
|
let path: &str = &path_string;
|
||||||
|
let mut args = vec!["mb", path];
|
||||||
|
|
||||||
|
if versioning {
|
||||||
|
args.push("--with-versioning");
|
||||||
|
}
|
||||||
|
if object_lock {
|
||||||
|
args.push("--with-lock");
|
||||||
|
}
|
||||||
|
cli::rc_exec(args)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_user(
|
||||||
|
alias: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
cli::rc_exec(vec![
|
||||||
|
"admin",
|
||||||
|
"user",
|
||||||
|
"add",
|
||||||
|
alias.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
password.as_str(),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delete_user(alias: String, username: String) -> Result<(), anyhow::Error> {
|
||||||
|
cli::rc_exec(vec![
|
||||||
|
"admin",
|
||||||
|
"user",
|
||||||
|
"rm",
|
||||||
|
alias.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RcPolicyData {
|
||||||
|
pub(crate) bucket: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_policy(template: String, data: RcPolicyData) -> Result<String, RenderError> {
|
||||||
|
let reg = Handlebars::new();
|
||||||
|
reg.render_template(&template, &data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_policy(
|
||||||
|
alias: String,
|
||||||
|
username: String,
|
||||||
|
policy: String,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let mut file = NamedTempFile::new()?;
|
||||||
|
let path = file.path().to_path_buf();
|
||||||
|
writeln!(file, "{}", policy)?;
|
||||||
|
cli::rc_exec(vec![
|
||||||
|
"admin",
|
||||||
|
"policy",
|
||||||
|
"create",
|
||||||
|
alias.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
path.to_str().unwrap(),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn assign_policy(alias: String, username: String) -> Result<(), anyhow::Error> {
|
||||||
|
cli::rc_exec(vec![
|
||||||
|
"admin",
|
||||||
|
"policy",
|
||||||
|
"attach",
|
||||||
|
alias.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
"--user",
|
||||||
|
username.as_str(),
|
||||||
|
"--json",
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn user_info(alias: String, username: String) -> Result<RcUserInfo, anyhow::Error> {
|
||||||
|
let output = cli::rc_exec(vec![
|
||||||
|
"admin",
|
||||||
|
"user",
|
||||||
|
"info",
|
||||||
|
alias.as_str(),
|
||||||
|
username.as_str(),
|
||||||
|
"--json",
|
||||||
|
])?;
|
||||||
|
let user_info = from_str::<RcUserInfo>(&output)?;
|
||||||
|
Ok(user_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delete_bucket(alias: String, bucket_name: String) -> Result<(), anyhow::Error> {
|
||||||
|
let path_string = format!("{}/{}", alias, bucket_name);
|
||||||
|
let path: &str = &path_string;
|
||||||
|
cli::rc_exec(vec!["rb", path, "--force", "--json"])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_rc() -> Result<(), anyhow::Error> {
|
||||||
|
cli::rc_exec(vec!["--version"])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::from_str;
|
||||||
|
|
||||||
|
use crate::rc::{RcAlias, RcAliasList, RcBucket, RcBucketList};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ser_alias_list() {
|
||||||
|
let output = r#"{
|
||||||
|
"aliases": [
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"endpoint": "https://something",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"bucket_lookup": "auto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test2",
|
||||||
|
"endpoint": "https://something",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"bucket_lookup": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
let alias_list: RcAliasList = from_str::<RcAliasList>(&output).unwrap();
|
||||||
|
let expected_res = RcAliasList {
|
||||||
|
aliases: Some(vec![
|
||||||
|
RcAlias {
|
||||||
|
name: "test".to_string(),
|
||||||
|
},
|
||||||
|
RcAlias {
|
||||||
|
name: "test2".to_string(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
assert_eq!(alias_list, expected_res);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_ser_bucket_list() {
|
||||||
|
let output = r#"{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "check",
|
||||||
|
"last_modified": "2026-03-10T19:24:10Z",
|
||||||
|
"is_dir": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "default-test",
|
||||||
|
"last_modified": "2026-03-11T13:24:26Z",
|
||||||
|
"is_dir": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "test",
|
||||||
|
"last_modified": "2026-03-10T19:24:07Z",
|
||||||
|
"is_dir": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"truncated": false
|
||||||
|
}"#;
|
||||||
|
let bucket_list = from_str::<RcBucketList>(&output).unwrap();
|
||||||
|
let expected_res = RcBucketList {
|
||||||
|
items: Some(vec![
|
||||||
|
RcBucket {
|
||||||
|
key: Some("test".to_string()),
|
||||||
|
},
|
||||||
|
RcBucket {
|
||||||
|
key: Some("test2".to_string()),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
assert_eq!(bucket_list, expected_res);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
operator/src/templates/read_only_policy.json
Normal file
0
operator/src/templates/read_only_policy.json
Normal file
0
operator/src/templates/read_write_policy.json
Normal file
0
operator/src/templates/read_write_policy.json
Normal file
Reference in New Issue
Block a user