Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6edea97895
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -191,4 +191,3 @@ cython_debug/
|
|||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
|||||||
11
.pre-commit-config.yaml
Normal file
11
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: detect-private-key
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.4.1
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
35
.woodpecker/build-dev-docs-container.yaml
Normal file
35
.woodpecker/build-dev-docs-container.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
branch:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- PACKAGE_NAME: badhouseplants/rustfs-manager-operator-docs-dev
|
||||||
|
DIR: ./documentation
|
||||||
|
- PACKAGE_NAME: badhouseplants/rustfs-manager-operator-dev
|
||||||
|
DIR: ./operator
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push a dev container
|
||||||
|
image: gitea.badhouseplants.net/badhouseplants/container-builder
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
privileged: true
|
||||||
|
commands:
|
||||||
|
- cd $DIR && build-container
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 700Mi
|
||||||
|
cpu: 1000m
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
31
.woodpecker/build-tagged-version.yaml
Normal file
31
.woodpecker/build-tagged-version.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- PACKAGE_NAME: badhouseplants/rustfs-manager-operator-docs
|
||||||
|
DIR: ./documentation
|
||||||
|
- PACKAGE_NAME: badhouseplants/rustfs-manager-operator
|
||||||
|
DIR: ./operator
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push a tagged container
|
||||||
|
image: gitea.badhouseplants.net/badhouseplants/container-builder
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
privileged: true
|
||||||
|
commands:
|
||||||
|
- cd $DIR && build-container
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 700Mi
|
||||||
|
cpu: 1000m
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
11
.woodpecker/code-checks.yaml
Normal file
11
.woodpecker/code-checks.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
# Run basic quick code checks
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Execute the pre-commit hook
|
||||||
|
image: codeberg.org/sp1thas/woodpecker-ci-pre-commit-runner
|
||||||
|
settings:
|
||||||
|
args: "--all-files"
|
||||||
59
.woodpecker/publish-dev-helm-chart.yaml
Normal file
59
.woodpecker/publish-dev-helm-chart.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
branch:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push helm charts
|
||||||
|
image: quay.io/helmpack/chart-testing:v3.14.0
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
commands:
|
||||||
|
- apk update && apk add yq
|
||||||
|
- export SHA="+$(git rev-parse --short HEAD)"
|
||||||
|
- helm registry login gitea.badhouseplants.net --username=$REGISTRY_USER --password=$REGISTRY_TOKEN
|
||||||
|
|
||||||
|
- export DOCS_IMAGE_REPOSITORY=$(yq '.image.repository + "-dev"' ./charts/rustfs-manager-operator-docs/values.yaml)
|
||||||
|
- yq -i '.image.repository = env(DOCS_IMAGE_REPOSITORY)' ./charts/rustfs-manager-operator-docs/values.yaml
|
||||||
|
- yq -i '.image.tag = env(CI_COMMIT_SHA)' ./charts/rustfs-manager-operator-docs/values.yaml
|
||||||
|
|
||||||
|
- export OPERATOR_IMAGE_REPOSITORY=$(yq '.image.repository + "-dev"' ./charts/rustfs-manager-operator/values.yaml)
|
||||||
|
- yq -i '.image.repository = env(OPERATOR_IMAGE_REPOSITORY)' ./charts/rustfs-manager-operator/values.yaml
|
||||||
|
- yq -i '.image.tag = env(CI_COMMIT_SHA)' ./charts/rustfs-manager-operator/values.yaml
|
||||||
|
|
||||||
|
- |-
|
||||||
|
for chart in $(find helm -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://gitea.badhouseplants.net/$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
|
||||||
47
.woodpecker/publish-tagged-helm-chart.yaml
Normal file
47
.woodpecker/publish-tagged-helm-chart.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build and push helm charts
|
||||||
|
image: quay.io/helmpack/chart-testing:v3.14.0
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: GITEA_REGISTRY_TOKEN
|
||||||
|
REGISTRY_USER: devops-bot
|
||||||
|
commands:
|
||||||
|
- apk update && apk add yq
|
||||||
|
- helm registry login gitea.badhouseplants.net --username=$REGISTRY_USER --password=$REGISTRY_TOKEN
|
||||||
|
|
||||||
|
- |-
|
||||||
|
for chart in $(find helm -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://gitea.badhouseplants.net/$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
|
||||||
12
.woodpecker/test-helm-charts.yaml
Normal file
12
.woodpecker/test-helm-charts.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
when:
|
||||||
|
event:
|
||||||
|
- pull_request
|
||||||
|
branch:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Lint helm charts
|
||||||
|
image: quay.io/helmpack/chart-testing:v3.14.0
|
||||||
|
commands:
|
||||||
|
- ct lint --all --validate-maintainers=false
|
||||||
95
README.md
95
README.md
@@ -1,3 +1,94 @@
|
|||||||
# rustfs-manager-operator
|
# RustFS Manager Operator
|
||||||
|
[](https://ci.badhouseplants.net/repos/29)
|
||||||
|
|
||||||
An operator to manage bucket, users, and policies on the RustfFS instance through CRDs
|
An operator to manage bucket and user on a RustfFS instance through Kubernetes CRDs.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Find better docs here: <https://rustfs.badhouseplants.net>
|
||||||
|
|
||||||
|
|
||||||
|
### Install the operator
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install rustfs-manager-operator oci://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator/rustfs-manager-operator --version 0.1.0
|
||||||
|
```
|
||||||
|
### Connect it to a RustFS instance
|
||||||
|
|
||||||
|
1. Create a values file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# values.yaml
|
||||||
|
endpoint: https://your.rust.fs
|
||||||
|
username: admin
|
||||||
|
password: qwertyu9
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the **rustfs-instance** helm chart
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install rustfs-instance oci://gitea.badhouseplants.net/badhouseplants/rustfs-u/rustfs-instance --version 0.1.0 -f ./values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start creating Buckets and Users
|
||||||
|
|
||||||
|
#### Buckets
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: rustfs.badhouseplants.net/v1beta1
|
||||||
|
kind: RustFSBucket
|
||||||
|
metadata:
|
||||||
|
name: <bucket name>
|
||||||
|
namespace: <application namespace>
|
||||||
|
spec:
|
||||||
|
# When cleanup is set to true, bucket will be removed from the instance
|
||||||
|
cleanup: false
|
||||||
|
# On which instance this bucket should be created
|
||||||
|
instance: rustfs-instance
|
||||||
|
# If true, bucket will be created with object locking
|
||||||
|
objectLock: false
|
||||||
|
# If true, bucket will be created with versioning
|
||||||
|
versioning: false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Users
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: rustfs.badhouseplants.net/v1beta1
|
||||||
|
kind: RustFSBucketUser
|
||||||
|
metadata:
|
||||||
|
name: <username>
|
||||||
|
namespace: <application namespace>
|
||||||
|
spec:
|
||||||
|
bucket: <a name of the bucket CR>
|
||||||
|
# User will be removed from the RustFS instance if set to true
|
||||||
|
cleanup: false
|
||||||
|
access: readWrite # or readOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access credentials via ConfigMaps and Secrets
|
||||||
|
|
||||||
|
#### ConfigMap:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get configmap <bucket name>-bucket-info -o yaml
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
data:
|
||||||
|
AWS_BUCKET_NAME: <bucket name>
|
||||||
|
AWS_ENDPOINT_URL: <endpoint>
|
||||||
|
AWS_REGION: <region>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Secret:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get secret <username>-bucket-creds -o yaml
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
data:
|
||||||
|
AWS_ACCESS_KEY_ID: <username>
|
||||||
|
AWS_SECRET_ACCESS_KEY: <a generated password>
|
||||||
|
```
|
||||||
|
|||||||
24
Taskfile.yml
Normal file
24
Taskfile.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||||
|
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
vars:
|
||||||
|
GREETING: Hello, world!
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
pre-commit-all:
|
||||||
|
desc: Run pre-commit --all
|
||||||
|
silent: true
|
||||||
|
cmd: pre-commit run --all
|
||||||
|
sync-crds:
|
||||||
|
desc: Sync CRD from the operator to the chart
|
||||||
|
silent: true
|
||||||
|
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 ./charts/rustfs-manager-operator/crd/*
|
||||||
|
- cp {{.WORKDIR}}/* ./helm/rustfs-manager-operator/crd/
|
||||||
|
- rm -rf {{.WORKDIR}}
|
||||||
23
charts/rustfs-instance/.helmignore
Normal file
23
charts/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/
|
||||||
10
charts/rustfs-instance/Chart.yaml
Normal file
10
charts/rustfs-instance/Chart.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: rustfs-instance
|
||||||
|
description: Connect the rustfs manager operator to your RustFS instance
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
maintainers:
|
||||||
|
- name: Nikolai Rodionov
|
||||||
|
email: allanger@badhouseplants.net
|
||||||
|
url: https://allanger.xyz
|
||||||
0
charts/rustfs-instance/templates/NOTES.txt
Normal file
0
charts/rustfs-instance/templates/NOTES.txt
Normal file
62
charts/rustfs-instance/templates/_helpers.tpl
Normal file
62
charts/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
charts/rustfs-instance/templates/intsance.yaml
Normal file
12
charts/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
charts/rustfs-instance/templates/secret.yaml
Normal file
10
charts/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
charts/rustfs-instance/values.yaml
Normal file
3
charts/rustfs-instance/values.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
endpoint: https://rustfs.company.my
|
||||||
|
username: admin
|
||||||
|
password: qwertyu9
|
||||||
23
charts/rustfs-manager-operator-docs/.helmignore
Normal file
23
charts/rustfs-manager-operator-docs/.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/
|
||||||
10
charts/rustfs-manager-operator-docs/Chart.yaml
Normal file
10
charts/rustfs-manager-operator-docs/Chart.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: rustfs-manager-operator-docs
|
||||||
|
description: Deploy documentation for the RustFS Manager Operator
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
maintainers:
|
||||||
|
- name: Nikolai Rodionov
|
||||||
|
email: allanger@badhouseplants.net
|
||||||
|
url: https://allanger.xyz
|
||||||
35
charts/rustfs-manager-operator-docs/templates/NOTES.txt
Normal file
35
charts/rustfs-manager-operator-docs/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-docs.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-docs.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "rustfs-manager-operator-docs.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-docs.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 }}
|
||||||
82
charts/rustfs-manager-operator-docs/templates/_helpers.tpl
Normal file
82
charts/rustfs-manager-operator-docs/templates/_helpers.tpl
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator-docs.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-docs.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-docs.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator-docs.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "rustfs-manager-operator-docs.chart" . }}
|
||||||
|
{{ include "rustfs-manager-operator-docs.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-docs.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "rustfs-manager-operator-docs.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator-docs.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "rustfs-manager-operator-docs.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Build an image out of object like that:
|
||||||
|
image:
|
||||||
|
registry: ghcr.io
|
||||||
|
repositoru: db-operator/db-operator
|
||||||
|
tag: latest
|
||||||
|
Might be used to make it easier to configure mirroring
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator-docs.imageBootsrap" -}}
|
||||||
|
{{- $image := "" }}
|
||||||
|
{{- if .image.registry }}
|
||||||
|
{{- $image = printf "%s/" .image.registry }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $tag := printf "%s" .chart.AppVersion }}
|
||||||
|
{{- if .image.tag }}
|
||||||
|
{{- $tag = .image.tag }}
|
||||||
|
{{- end }}
|
||||||
|
{{- printf "%s%s:%s" $image .image.repository $tag }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator-docs.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.labels" . | nindent 8 }}
|
||||||
|
{{- with .Values.podLabels }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "rustfs-manager-operator-docs.serviceAccountName" . }}
|
||||||
|
{{- with .Values.podSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
{{- with .Values.securityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
image: {{ include "rustfs-manager-operator-docs.imageBootsrap" (dict "image" $.Values.image "chart" $.Chart) }}
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.service.port }}
|
||||||
|
protocol: TCP
|
||||||
|
{{- with .Values.livenessProbe }}
|
||||||
|
livenessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.readinessProbe }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.volumeMounts }}
|
||||||
|
volumeMounts:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.volumes }}
|
||||||
|
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 }}
|
||||||
43
charts/rustfs-manager-operator-docs/templates/ingress.yaml
Normal file
43
charts/rustfs-manager-operator-docs/templates/ingress.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator-docs.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
{{- with .pathType }}
|
||||||
|
pathType: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "rustfs-manager-operator-docs.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
15
charts/rustfs-manager-operator-docs/templates/service.yaml
Normal file
15
charts/rustfs-manager-operator-docs/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator-docs.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "rustfs-manager-operator-docs.selectorLabels" . | nindent 4 }}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator-docs.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator-docs.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||||
|
{{- end }}
|
||||||
158
charts/rustfs-manager-operator-docs/values.yaml
Normal file
158
charts/rustfs-manager-operator-docs/values.yaml
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||||
|
image:
|
||||||
|
registry: gitea.badhouseplants.net
|
||||||
|
repository: badhouseplants/rustfs-manager-operator-docs
|
||||||
|
# This sets the pull policy for images.
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||||
|
imagePullSecrets: []
|
||||||
|
# This is to override the chart name.
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
# 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: ""
|
||||||
|
|
||||||
|
# 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: 8080
|
||||||
|
|
||||||
|
# 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: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
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: {}
|
||||||
2
charts/rustfs-manager-operator/.gitignore
vendored
Normal file
2
charts/rustfs-manager-operator/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
charts
|
||||||
|
Chart.lock
|
||||||
23
charts/rustfs-manager-operator/.helmignore
Normal file
23
charts/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/
|
||||||
16
charts/rustfs-manager-operator/Chart.yaml
Normal file
16
charts/rustfs-manager-operator/Chart.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: rustfs-manager-operator
|
||||||
|
description: A Helm chart for the RustFS Manager Operator
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "0.1.0"
|
||||||
|
dependencies:
|
||||||
|
- name: rustfs-manager-operator-docs
|
||||||
|
alias: docs
|
||||||
|
condition: docs.enabled
|
||||||
|
repository: file://../rustfs-manager-operator-docs
|
||||||
|
version: "*"
|
||||||
|
maintainers:
|
||||||
|
- name: Nikolai Rodionov
|
||||||
|
email: allanger@badhouseplants.net
|
||||||
|
url: https://allanger.xyz
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
configMapName:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
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,125 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: rustfsbucketusers.rustfs.badhouseplants.net
|
||||||
|
spec:
|
||||||
|
group: rustfs.badhouseplants.net
|
||||||
|
names:
|
||||||
|
categories: []
|
||||||
|
kind: RustFSBucketUser
|
||||||
|
plural: rustfsbucketusers
|
||||||
|
shortNames:
|
||||||
|
- bucketuser
|
||||||
|
singular: rustfsbucketuser
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- description: The name of the user
|
||||||
|
jsonPath: .status.username
|
||||||
|
name: User Name
|
||||||
|
type: string
|
||||||
|
- description: The name of the secret
|
||||||
|
jsonPath: .status.secretName
|
||||||
|
name: Secret
|
||||||
|
type: string
|
||||||
|
- description: The name of the configmap
|
||||||
|
jsonPath: .status.configMapName
|
||||||
|
name: ConfigMap
|
||||||
|
type: string
|
||||||
|
- description: Which access is set to the user
|
||||||
|
jsonPath: .spec.access
|
||||||
|
name: Access
|
||||||
|
type: string
|
||||||
|
- description: Is the S3Instance ready
|
||||||
|
jsonPath: .status.ready
|
||||||
|
name: Status
|
||||||
|
type: boolean
|
||||||
|
name: v1beta1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Manage users 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:
|
||||||
|
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: RustFSBucketUser
|
||||||
|
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: {}
|
||||||
6
charts/rustfs-manager-operator/templates/NOTES.txt
Normal file
6
charts/rustfs-manager-operator/templates/NOTES.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-----------------------------------------------------------------------
|
||||||
|
Release information
|
||||||
|
Chart name: {{ .Chart.Name }}.
|
||||||
|
Version of the chart: {{ .Chart.Version }}
|
||||||
|
Release name {{ .Release.Name }}.
|
||||||
|
-----------------------------------------------------------------------
|
||||||
82
charts/rustfs-manager-operator/templates/_helpers.tpl
Normal file
82
charts/rustfs-manager-operator/templates/_helpers.tpl
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{{/*
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Build an image out of object like that:
|
||||||
|
image:
|
||||||
|
registry: ghcr.io
|
||||||
|
repositoru: db-operator/db-operator
|
||||||
|
tag: latest
|
||||||
|
Might be used to make it easier to configure mirroring
|
||||||
|
*/}}
|
||||||
|
{{- define "rustfs-manager-operator.imageBootsrap" -}}
|
||||||
|
{{- $image := "" }}
|
||||||
|
{{- if .image.registry }}
|
||||||
|
{{- $image = printf "%s/" .image.registry }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $tag := printf "%s" .chart.AppVersion }}
|
||||||
|
{{- if .image.tag }}
|
||||||
|
{{- $tag = .image.tag }}
|
||||||
|
{{- end }}
|
||||||
|
{{- printf "%s%s:%s" $image .image.repository $tag }}
|
||||||
|
{{- end }}
|
||||||
69
charts/rustfs-manager-operator/templates/cluster_role.yaml
Normal file
69
charts/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
|
||||||
|
- rustfsbucketusers
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- rustfs.badhouseplants.net
|
||||||
|
resources:
|
||||||
|
- rustfsbuckets/finalizers
|
||||||
|
- rustfsintsances/finalizers
|
||||||
|
- rustfsbucketusers/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
|
||||||
|
- apiGroups:
|
||||||
|
- rustfs.badhouseplants.net
|
||||||
|
resources:
|
||||||
|
- rustfsbuckets/status
|
||||||
|
- rustfsinstances/status
|
||||||
|
- rustfsbucketusers/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
charts/rustfs-manager-operator/templates/config.yaml
Normal file
9
charts/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 }}
|
||||||
27
charts/rustfs-manager-operator/templates/crds.yaml
Normal file
27
charts/rustfs-manager-operator/templates/crds.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{{- 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 }}
|
||||||
88
charts/rustfs-manager-operator/templates/deployment.yaml
Normal file
88
charts/rustfs-manager-operator/templates/deployment.yaml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "rustfs-manager-operator.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "rustfs-manager-operator.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
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: {{ include "rustfs-manager-operator.imageBootsrap" (dict "image" $.Values.image "chart" $.Chart) }}
|
||||||
|
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: /tmp
|
||||||
|
name: tmp
|
||||||
|
- mountPath: /.config/rc
|
||||||
|
name: tmp
|
||||||
|
- mountPath: /srv/config/
|
||||||
|
name: config
|
||||||
|
readOnly: true
|
||||||
|
{{- with .Values.volumeMounts }}
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir: {}
|
||||||
|
- 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
charts/rustfs-manager-operator/templates/serviceaccount.yaml
Normal file
13
charts/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 }}
|
||||||
92
charts/rustfs-manager-operator/values.yaml
Normal file
92
charts/rustfs-manager-operator/values.yaml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
crds:
|
||||||
|
install: true
|
||||||
|
keep: true
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: gitea.badhouseplants.net
|
||||||
|
repository: badhouseplants/rustfs-manager-operator
|
||||||
|
# This sets the pull policy for images.
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
config:
|
||||||
|
setOwnerReference: false
|
||||||
|
|
||||||
|
docs:
|
||||||
|
enabled: false
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "traefik"
|
||||||
|
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
|
||||||
|
|
||||||
|
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: ""
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
2
documentation/.gitignore
vendored
Normal file
2
documentation/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.cache
|
||||||
|
site
|
||||||
11
documentation/Containerfile
Normal file
11
documentation/Containerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.14 AS builder
|
||||||
|
RUN apt-get update -y
|
||||||
|
RUN apt-get install -y pipx
|
||||||
|
RUN pip install poetry
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN python -m poetry install --no-root
|
||||||
|
RUN python -m poetry run zensical build
|
||||||
|
|
||||||
|
FROM nginxinc/nginx-unprivileged:alpine3.23-perl
|
||||||
|
COPY --from=builder /src/site /usr/share/nginx/html
|
||||||
126
documentation/docs/index.md
Normal file
126
documentation/docs/index.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# RustFS Manager Operator
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
This operator is supposed to connect to an existing **RustFS** instance and manage bucket and users on it.
|
||||||
|
|
||||||
|
### How to install
|
||||||
|
|
||||||
|
The operator is distributed as a helm chart, that can be installed either from a self-hosted **gitea** or **github**.
|
||||||
|
|
||||||
|
#### Gitea
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install rustfs-manager-operator oci://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator/rustfs-manager-operator --version 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Github
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install rustfs-manager-operator oci://ghcr.io/badhouseplants.net/rustfs-manager-operator/rustfs-manager-operator --version 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docs
|
||||||
|
|
||||||
|
Documentation is added as a dependency to the main helm chart, you can install it by setting `.docs.enabled` value to `true`. Then you will have the up-to-date documentation deployed to your cluster as well.
|
||||||
|
|
||||||
|
### Connect the operator to RustFS
|
||||||
|
|
||||||
|
Once the operator is running, you will need to create a `RustFSIntsance` CR to connect the operator to your **RustFS**. You can either create it manually, or use a helm chart as well.
|
||||||
|
|
||||||
|
#### Helm chart
|
||||||
|
|
||||||
|
1. Create values.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# values.yaml
|
||||||
|
endpoint: https://your.rust.fs
|
||||||
|
username: admin
|
||||||
|
password: qwertyu9
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install <your instance name> oci://gitea.badhouseplants.net/badhouseplants/rustfs-manager-operator/rustfs-instance --version 0.1.0 -f values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
And wait until it becomes ready.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get rustfs <your instance name>
|
||||||
|
NAME ENDPOINT REGION TOTAL BUCKETS STATUS
|
||||||
|
<your instance name> <your instance url> us-east-1 2 true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Start using
|
||||||
|
|
||||||
|
Now you can start creating buckets and users.
|
||||||
|
|
||||||
|
#### Create a bucket
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: rustfs.badhouseplants.net/v1beta1
|
||||||
|
kind: RustFSBucket
|
||||||
|
metadata:
|
||||||
|
name: <bucket name>
|
||||||
|
namespace: <application namespace>
|
||||||
|
spec:
|
||||||
|
# When cleanup is set to true, bucket will be removed from the instance
|
||||||
|
cleanup: false
|
||||||
|
# On which instance this bucket should be created
|
||||||
|
instance: rustfs-instance
|
||||||
|
# If true, bucket will be created with object locking
|
||||||
|
objectLock: false
|
||||||
|
# If true, bucket will be created with versioning
|
||||||
|
versioning: false
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get bucket <bucket name>
|
||||||
|
NAME BUCKET NAME ENDPOINT REGION STATUS
|
||||||
|
<bucket-name> <namespace>-<bucket-name> <endpoint> us-east-1 true
|
||||||
|
```
|
||||||
|
|
||||||
|
When bucket is created, there will be a secret created in the same namespace: `<bucket name>-bucket-info`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get configmap <bucket name>-bucket-info -o yaml
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
data:
|
||||||
|
AWS_BUCKET_NAME: <bucket name>
|
||||||
|
AWS_ENDPOINT_URL: <endpoint>
|
||||||
|
AWS_REGION: <region>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Creating a user
|
||||||
|
|
||||||
|
When the bucket is ready, you can create a user that will have access to this bucket:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: rustfs.badhouseplants.net/v1beta1
|
||||||
|
kind: RustFSBucketUser
|
||||||
|
metadata:
|
||||||
|
name: <username>
|
||||||
|
namespace: <application namespace>
|
||||||
|
spec:
|
||||||
|
bucket: <a name of the bucket CR>
|
||||||
|
# User will be removed from the RustFS instance if set to true
|
||||||
|
cleanup: false
|
||||||
|
access: readWrite # or readOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```shell
|
||||||
|
kubectl get bucketuser <username>
|
||||||
|
NAME USER NAME SECRET CONFIGMAP ACCESS STATUS
|
||||||
|
<username> <namespace>-<username> <username>-bucket-creds <bucket name> -bucket-info readWrite true
|
||||||
|
```
|
||||||
|
|
||||||
|
Operator will also add a Secret to the same namespace: `<username>-bucket-creds`, that will contain the following keys:
|
||||||
|
|
||||||
|
- AWS_ACCESS_KEY_ID
|
||||||
|
- AWS_SECRET_ACCESS_KEY
|
||||||
|
|
||||||
|
You can use them to connect your application to the bucket.
|
||||||
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"
|
||||||
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"
|
||||||
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
41
operator/Cargo.toml
Normal file
41
operator/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[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_bucket;
|
||||||
|
pub mod v1beta1_rustfs_bucket_user;
|
||||||
|
pub mod v1beta1_rustfs_instance;
|
||||||
59
operator/src/api/v1beta1_rustfs_bucket.rs
Normal file
59
operator/src/api/v1beta1_rustfs_bucket.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub config_map_name: 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/v1beta1_rustfs_bucket_user.rs
Normal file
57
operator/src/api/v1beta1_rustfs_bucket_user.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(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 = "RustFSBucketUser",
|
||||||
|
group = "rustfs.badhouseplants.net",
|
||||||
|
version = "v1beta1",
|
||||||
|
doc = "Manage users on a RustFs instance",
|
||||||
|
shortname = "bucketuser",
|
||||||
|
namespaced,
|
||||||
|
status = "RustFSBucketUserStatus",
|
||||||
|
printcolumn = r#"{"name":"User Name","type":"string","description":"The name of the user","jsonPath":".status.username"}"#,
|
||||||
|
printcolumn = r#"{"name":"Secret","type":"string","description":"The name of the secret","jsonPath":".status.secretName"}"#,
|
||||||
|
printcolumn = r#"{"name":"ConfigMap","type":"string","description":"The name of the configmap","jsonPath":".status.configMapName"}"#,
|
||||||
|
printcolumn = r#"{"name":"Access","type":"string","description":"Which access is set to the user","jsonPath":".spec.access"}"#,
|
||||||
|
printcolumn = r#"{"name":"Status","type":"boolean","description":"Is the S3Instance ready","jsonPath":".status.ready"}"#
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSUserSpec {
|
||||||
|
pub bucket: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cleanup: bool,
|
||||||
|
pub access: Access,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustFSBucketUserStatus {
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
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,
|
||||||
|
}
|
||||||
70
operator/src/cli.rs
Normal file
70
operator/src/cli.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
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_bucket_user;
|
||||||
|
pub(crate) mod rustfs_instance;
|
||||||
432
operator/src/controllers/rustfs_bucket.rs
Normal file
432
operator/src/controllers/rustfs_bucket.rs
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
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_bucket::{RustFSBucket, RustFSBucketStatus};
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use k8s_openapi::api::core::v1::ConfigMap;
|
||||||
|
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";
|
||||||
|
const AWS_BUCKET_NAME: &str = "AWS_BUCKET_NAME";
|
||||||
|
|
||||||
|
#[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.clone()),
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bucket_name = format!(
|
||||||
|
"{}-{}",
|
||||||
|
bucket_cr.namespace().unwrap(),
|
||||||
|
bucket_cr.name_any()
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Updating the ConfigMap");
|
||||||
|
if let Err(err) = ensure_data_configmap(
|
||||||
|
cm_api.clone(),
|
||||||
|
configmap.clone(),
|
||||||
|
rustfs_cr.clone(),
|
||||||
|
&bucket_name,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(RustFSBucketError::KubeError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
status.config_map_name = Some(configmap_name);
|
||||||
|
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>,
|
||||||
|
mut cm: ConfigMap,
|
||||||
|
rustfs_cr: RustFSInstance,
|
||||||
|
bucket_name: &String,
|
||||||
|
) -> 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);
|
||||||
|
data.insert(AWS_BUCKET_NAME.to_string(), bucket_name.clone());
|
||||||
|
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 occurred 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>;
|
||||||
665
operator/src/controllers/rustfs_bucket_user.rs
Normal file
665
operator/src/controllers/rustfs_bucket_user.rs
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
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 anyhow::{Result, anyhow};
|
||||||
|
use api::api::v1beta1_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta1_rustfs_bucket_user::{RustFSBucketUser, RustFSBucketUserStatus};
|
||||||
|
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 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<RustFSBucketUser>,
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
) -> RustFSBucketUserResult<Action> {
|
||||||
|
info!("Staring reconciling");
|
||||||
|
let user_api: Api<RustFSBucketUser> =
|
||||||
|
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 RustFSBucketUser 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(RustFSBucketUserError::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.clone()),
|
||||||
|
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(RustFSBucketUserError::KubeError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::KubeError(err)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
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(RustFSBucketUserError::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(RustFSBucketUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bucket_status = match bucket_cr.clone().status {
|
||||||
|
Some(status) => {
|
||||||
|
if !status.ready {
|
||||||
|
return Err(RustFSBucketUserError::BucketNotReadyError);
|
||||||
|
};
|
||||||
|
status
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::IllegalRustFSBucketUser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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(RustFSBucketUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
return Err(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::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(RustFSBucketUserError::IllegalRustFSBucketUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userinfo = match user_info(rustfs_cr.name_any(), username.clone()) {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(err) => return Err(RustFSBucketUserError::RcCliError(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let policy_template = match user_cr.spec.access {
|
||||||
|
api::api::v1beta1_rustfs_bucket_user::Access::ReadOnly => POLICY_READ_ONLY,
|
||||||
|
api::api::v1beta1_rustfs_bucket_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(RustFSBucketUserError::IllegalRustFSBucketUser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = RcPolicyData {
|
||||||
|
bucket: bucket_name,
|
||||||
|
};
|
||||||
|
let policy = match render_policy(policy_template.to_string(), data) {
|
||||||
|
Ok(policy) => policy,
|
||||||
|
Err(err) => return Err(RustFSBucketUserError::RcCliError(anyhow!(err))),
|
||||||
|
};
|
||||||
|
if let Err(err) = create_policy(rustfs_cr.name_any(), username.clone(), policy.to_string()) {
|
||||||
|
return Err(RustFSBucketUserError::RcCliError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = assign_policy(rustfs_cr.name_any(), username.clone()) {
|
||||||
|
return Err(RustFSBucketUserError::RcCliError(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a user
|
||||||
|
status.policy = Some(policy);
|
||||||
|
status.username = Some(userinfo.access_key);
|
||||||
|
status.status = Some(userinfo.status);
|
||||||
|
status.ready = true;
|
||||||
|
status.secret_name = Some(secret_name);
|
||||||
|
status.config_map_name = bucket_status.config_map_name;
|
||||||
|
|
||||||
|
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(RustFSBucketUserError::KubeError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap the object by adding a default status to it
|
||||||
|
async fn init_object(
|
||||||
|
mut obj: RustFSBucketUser,
|
||||||
|
api: Api<RustFSBucketUser>,
|
||||||
|
) -> Result<Action, RustFSBucketUserError> {
|
||||||
|
let conditions = init_conditions(vec![
|
||||||
|
TYPE_SECRET_READY.to_string(),
|
||||||
|
TYPE_USER_READY.to_string(),
|
||||||
|
]);
|
||||||
|
obj.status = Some(RustFSBucketUserStatus {
|
||||||
|
conditions,
|
||||||
|
..RustFSBucketUserStatus::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(RustFSBucketUserError::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: RustFSBucketUser,
|
||||||
|
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: RustFSBucketUser::api_version(&()).into(),
|
||||||
|
kind: RustFSBucketUser::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<RustFSBucketUser>,
|
||||||
|
err: &RustFSBucketUserError,
|
||||||
|
_: Arc<Context>,
|
||||||
|
) -> Action {
|
||||||
|
error!(trace.error = %err, "Error occurred 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::<RustFSBucketUser>::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 RustFSBucketUserError {
|
||||||
|
#[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<RustFSBucketUserError>>),
|
||||||
|
|
||||||
|
#[error("IllegalRustFSBucketUser")]
|
||||||
|
IllegalRustFSBucketUser,
|
||||||
|
|
||||||
|
#[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 RustFSBucketUserResult<T, E = RustFSBucketUserError> = std::result::Result<T, E>;
|
||||||
411
operator/src/controllers/rustfs_instance.rs
Normal file
411
operator/src/controllers/rustfs_instance.rs
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
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 occurred 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>;
|
||||||
1
operator/src/lib.rs
Normal file
1
operator/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod api;
|
||||||
108
operator/src/main.rs
Normal file
108
operator/src/main.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
mod cli;
|
||||||
|
mod conditions;
|
||||||
|
mod config;
|
||||||
|
mod controllers;
|
||||||
|
mod rc;
|
||||||
|
|
||||||
|
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_bucket_user};
|
||||||
|
|
||||||
|
use api::api::v1beta1_rustfs_bucket::RustFSBucket;
|
||||||
|
use api::api::v1beta1_rustfs_bucket_user::RustFSBucketUser;
|
||||||
|
use api::api::v1beta1_rustfs_instance::RustFSInstance;
|
||||||
|
|
||||||
|
/// 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(&RustFSBucketUser::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_bucket_user_ctrl = rustfs_bucket_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_bucket_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::{NamedTempFile, tempfile};
|
||||||
|
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