diff --git a/.gitignore b/.gitignore index 5730393..ba038fc 100644 --- a/.gitignore +++ b/.gitignore @@ -191,4 +191,3 @@ cython_debug/ # PyPI configuration file .pypirc - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7b4b0f1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.woodpecker/build-dev-docs-container.yaml b/.woodpecker/build-dev-docs-container.yaml new file mode 100644 index 0000000..44e6023 --- /dev/null +++ b/.woodpecker/build-dev-docs-container.yaml @@ -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 diff --git a/.woodpecker/build-tagged-version.yaml b/.woodpecker/build-tagged-version.yaml new file mode 100644 index 0000000..5f5d819 --- /dev/null +++ b/.woodpecker/build-tagged-version.yaml @@ -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 diff --git a/.woodpecker/code-checks.yaml b/.woodpecker/code-checks.yaml new file mode 100644 index 0000000..bf4153d --- /dev/null +++ b/.woodpecker/code-checks.yaml @@ -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" diff --git a/.woodpecker/publish-dev-helm-chart.yaml b/.woodpecker/publish-dev-helm-chart.yaml new file mode 100644 index 0000000..097ff6b --- /dev/null +++ b/.woodpecker/publish-dev-helm-chart.yaml @@ -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 diff --git a/.woodpecker/publish-tagged-helm-chart.yaml b/.woodpecker/publish-tagged-helm-chart.yaml new file mode 100644 index 0000000..4c43b63 --- /dev/null +++ b/.woodpecker/publish-tagged-helm-chart.yaml @@ -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 diff --git a/.woodpecker/test-helm-charts.yaml b/.woodpecker/test-helm-charts.yaml new file mode 100644 index 0000000..12f5f42 --- /dev/null +++ b/.woodpecker/test-helm-charts.yaml @@ -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 diff --git a/README.md b/README.md index 20d5e2e..fa04570 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,94 @@ -# rustfs-manager-operator +# RustFS Manager Operator +[![Woodpecker CI](https://ci.badhouseplants.net/api/badges/29/status.svg)](https://ci.badhouseplants.net/repos/29) -An operator to manage bucket, users, and policies on the RustfFS instance through CRDs \ No newline at end of file +An operator to manage bucket and user on a RustfFS instance through Kubernetes CRDs. + +## Getting started + +Find better docs here: + + +### 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: + 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: + namespace: +spec: + bucket: + # 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-info -o yaml + +apiVersion: v1 +kind: ConfigMap +data: + AWS_BUCKET_NAME: + AWS_ENDPOINT_URL: + AWS_REGION: +``` + +#### Secret: + +```shell +kubectl get secret -bucket-creds -o yaml + +apiVersion: v1 +kind: ConfigMap +data: + AWS_ACCESS_KEY_ID: + AWS_SECRET_ACCESS_KEY: +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..c4371b8 --- /dev/null +++ b/Taskfile.yml @@ -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}} diff --git a/charts/rustfs-instance/.helmignore b/charts/rustfs-instance/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/rustfs-instance/.helmignore @@ -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/ diff --git a/charts/rustfs-instance/Chart.yaml b/charts/rustfs-instance/Chart.yaml new file mode 100644 index 0000000..c10c948 --- /dev/null +++ b/charts/rustfs-instance/Chart.yaml @@ -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 diff --git a/charts/rustfs-instance/templates/NOTES.txt b/charts/rustfs-instance/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/charts/rustfs-instance/templates/_helpers.tpl b/charts/rustfs-instance/templates/_helpers.tpl new file mode 100644 index 0000000..d8cb93c --- /dev/null +++ b/charts/rustfs-instance/templates/_helpers.tpl @@ -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 }} diff --git a/charts/rustfs-instance/templates/intsance.yaml b/charts/rustfs-instance/templates/intsance.yaml new file mode 100644 index 0000000..a6c8f07 --- /dev/null +++ b/charts/rustfs-instance/templates/intsance.yaml @@ -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" . }} diff --git a/charts/rustfs-instance/templates/secret.yaml b/charts/rustfs-instance/templates/secret.yaml new file mode 100644 index 0000000..9ba82b7 --- /dev/null +++ b/charts/rustfs-instance/templates/secret.yaml @@ -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 }} diff --git a/charts/rustfs-instance/values.yaml b/charts/rustfs-instance/values.yaml new file mode 100644 index 0000000..addb204 --- /dev/null +++ b/charts/rustfs-instance/values.yaml @@ -0,0 +1,3 @@ +endpoint: https://rustfs.company.my +username: admin +password: qwertyu9 diff --git a/charts/rustfs-manager-operator-docs/.helmignore b/charts/rustfs-manager-operator-docs/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/rustfs-manager-operator-docs/.helmignore @@ -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/ diff --git a/charts/rustfs-manager-operator-docs/Chart.yaml b/charts/rustfs-manager-operator-docs/Chart.yaml new file mode 100644 index 0000000..b839035 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/Chart.yaml @@ -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 diff --git a/charts/rustfs-manager-operator-docs/templates/NOTES.txt b/charts/rustfs-manager-operator-docs/templates/NOTES.txt new file mode 100644 index 0000000..49e6f39 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/NOTES.txt @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/templates/_helpers.tpl b/charts/rustfs-manager-operator-docs/templates/_helpers.tpl new file mode 100644 index 0000000..722dcc2 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/_helpers.tpl @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/templates/deployment.yaml b/charts/rustfs-manager-operator-docs/templates/deployment.yaml new file mode 100644 index 0000000..ac4dca5 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/deployment.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/templates/ingress.yaml b/charts/rustfs-manager-operator-docs/templates/ingress.yaml new file mode 100644 index 0000000..13d91b4 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/ingress.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/templates/service.yaml b/charts/rustfs-manager-operator-docs/templates/service.yaml new file mode 100644 index 0000000..39ddd84 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/service.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/templates/serviceaccount.yaml b/charts/rustfs-manager-operator-docs/templates/serviceaccount.yaml new file mode 100644 index 0000000..98ea741 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/templates/serviceaccount.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator-docs/values.yaml b/charts/rustfs-manager-operator-docs/values.yaml new file mode 100644 index 0000000..717d4b0 --- /dev/null +++ b/charts/rustfs-manager-operator-docs/values.yaml @@ -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: {} diff --git a/charts/rustfs-manager-operator/.gitignore b/charts/rustfs-manager-operator/.gitignore new file mode 100644 index 0000000..8d89461 --- /dev/null +++ b/charts/rustfs-manager-operator/.gitignore @@ -0,0 +1,2 @@ +charts +Chart.lock diff --git a/charts/rustfs-manager-operator/.helmignore b/charts/rustfs-manager-operator/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/rustfs-manager-operator/.helmignore @@ -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/ diff --git a/charts/rustfs-manager-operator/Chart.yaml b/charts/rustfs-manager-operator/Chart.yaml new file mode 100644 index 0000000..2a8e0c3 --- /dev/null +++ b/charts/rustfs-manager-operator/Chart.yaml @@ -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 diff --git a/charts/rustfs-manager-operator/crd/rustfsbuckets.rustfs.badhouseplants.net.yaml b/charts/rustfs-manager-operator/crd/rustfsbuckets.rustfs.badhouseplants.net.yaml new file mode 100644 index 0000000..90cece9 --- /dev/null +++ b/charts/rustfs-manager-operator/crd/rustfsbuckets.rustfs.badhouseplants.net.yaml @@ -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: {} diff --git a/charts/rustfs-manager-operator/crd/rustfsbucketusers.rustfs.badhouseplants.net.yaml b/charts/rustfs-manager-operator/crd/rustfsbucketusers.rustfs.badhouseplants.net.yaml new file mode 100644 index 0000000..ffe6119 --- /dev/null +++ b/charts/rustfs-manager-operator/crd/rustfsbucketusers.rustfs.badhouseplants.net.yaml @@ -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: {} diff --git a/charts/rustfs-manager-operator/crd/rustfsinstances.rustfs.badhouseplants.net.yaml b/charts/rustfs-manager-operator/crd/rustfsinstances.rustfs.badhouseplants.net.yaml new file mode 100644 index 0000000..01aea8f --- /dev/null +++ b/charts/rustfs-manager-operator/crd/rustfsinstances.rustfs.badhouseplants.net.yaml @@ -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: {} diff --git a/charts/rustfs-manager-operator/templates/NOTES.txt b/charts/rustfs-manager-operator/templates/NOTES.txt new file mode 100644 index 0000000..7ccb405 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/NOTES.txt @@ -0,0 +1,6 @@ +----------------------------------------------------------------------- +Release information +Chart name: {{ .Chart.Name }}. +Version of the chart: {{ .Chart.Version }} +Release name {{ .Release.Name }}. +----------------------------------------------------------------------- diff --git a/charts/rustfs-manager-operator/templates/_helpers.tpl b/charts/rustfs-manager-operator/templates/_helpers.tpl new file mode 100644 index 0000000..5f437f8 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/_helpers.tpl @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/cluster_role.yaml b/charts/rustfs-manager-operator/templates/cluster_role.yaml new file mode 100644 index 0000000..93b39c6 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/cluster_role.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/cluster_role_binding.yaml b/charts/rustfs-manager-operator/templates/cluster_role_binding.yaml new file mode 100644 index 0000000..424659e --- /dev/null +++ b/charts/rustfs-manager-operator/templates/cluster_role_binding.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/config.yaml b/charts/rustfs-manager-operator/templates/config.yaml new file mode 100644 index 0000000..a5d5db8 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/config.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/crds.yaml b/charts/rustfs-manager-operator/templates/crds.yaml new file mode 100644 index 0000000..7d420d9 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/crds.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/deployment.yaml b/charts/rustfs-manager-operator/templates/deployment.yaml new file mode 100644 index 0000000..910cc3e --- /dev/null +++ b/charts/rustfs-manager-operator/templates/deployment.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/templates/serviceaccount.yaml b/charts/rustfs-manager-operator/templates/serviceaccount.yaml new file mode 100644 index 0000000..aebfad7 --- /dev/null +++ b/charts/rustfs-manager-operator/templates/serviceaccount.yaml @@ -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 }} diff --git a/charts/rustfs-manager-operator/values.yaml b/charts/rustfs-manager-operator/values.yaml new file mode 100644 index 0000000..e90fd81 --- /dev/null +++ b/charts/rustfs-manager-operator/values.yaml @@ -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: {} diff --git a/documentation/.gitignore b/documentation/.gitignore new file mode 100644 index 0000000..c7f3fac --- /dev/null +++ b/documentation/.gitignore @@ -0,0 +1,2 @@ +.cache +site diff --git a/documentation/Containerfile b/documentation/Containerfile new file mode 100644 index 0000000..a8cf370 --- /dev/null +++ b/documentation/Containerfile @@ -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 diff --git a/documentation/docs/index.md b/documentation/docs/index.md new file mode 100644 index 0000000..3aeaefa --- /dev/null +++ b/documentation/docs/index.md @@ -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 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 +NAME ENDPOINT REGION TOTAL BUCKETS STATUS + 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: + 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 +NAME BUCKET NAME ENDPOINT REGION STATUS + - us-east-1 true +``` + +When bucket is created, there will be a secret created in the same namespace: `-bucket-info` + +```shell +kubectl get configmap -bucket-info -o yaml + +apiVersion: v1 +kind: ConfigMap +data: + AWS_BUCKET_NAME: + AWS_ENDPOINT_URL: + AWS_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: + namespace: +spec: + bucket: + # User will be removed from the RustFS instance if set to true + cleanup: false + access: readWrite # or readOnly +``` + + +```shell +kubectl get bucketuser +NAME USER NAME SECRET CONFIGMAP ACCESS STATUS + - -bucket-creds -bucket-info readWrite true +``` + +Operator will also add a Secret to the same namespace: `-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. diff --git a/documentation/poetry.lock b/documentation/poetry.lock new file mode 100644 index 0000000..15f452f --- /dev/null +++ b/documentation/poetry.lock @@ -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" diff --git a/documentation/pyproject.toml b/documentation/pyproject.toml new file mode 100644 index 0000000..8bea3e6 --- /dev/null +++ b/documentation/pyproject.toml @@ -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" diff --git a/documentation/zensical.toml b/documentation/zensical.toml new file mode 100644 index 0000000..3862bfe --- /dev/null +++ b/documentation/zensical.toml @@ -0,0 +1,52 @@ +[project] +site_name = "Documentation" +site_description = "A new project generated from the default template project." +site_author = "" +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" diff --git a/operator/.containerignore b/operator/.containerignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/operator/.containerignore @@ -0,0 +1 @@ +target diff --git a/operator/Cargo.lock b/operator/Cargo.lock new file mode 100644 index 0000000..1bc3edb --- /dev/null +++ b/operator/Cargo.lock @@ -0,0 +1,3720 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.1.5", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.1.5", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.3", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash 0.5.0", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "envtest" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce14cfe6406ca36a47189f60910342384b7eee57138956e736c78c6cc7ef0bb1" +dependencies = [ + "k8s-openapi", + "kube", + "rust2go", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonpath-rust" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "k8s-openapi" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b326f5219dd55872a72c1b6ddd1b830b8334996c667449c29391d657d78d5e" +dependencies = [ + "base64", + "jiff", + "schemars", + "serde", + "serde_json", +] + +[[package]] +name = "kube" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f96b537b4c4f61fc183594edbecbbefa3037e403feac0701bb24e6eff78e0034" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af97b8b696eb737e5694f087c498ca725b172c2a5bc3a6916328d160225537ee" +dependencies = [ + "base64", + "bytes", + "either", + "futures", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jiff", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aeade7d2e9f165f96b3c1749ff01a8e2dc7ea954bd333bcfcecc37d5226bdd" +dependencies = [ + "derive_more", + "form_urlencoded", + "http 1.4.0", + "jiff", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "kube-derive" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98f59f4e68864624a0b993a1cc2424439ab7238eaede5c299e89943e2a093ff" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "kube-runtime" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc158473d6d86ec22692874bd5ddccf07474eab5c6bb41f226c522e945da5244" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "backon", + "educe", + "futures", + "hashbrown 0.16.1", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "password-hash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccbd25f71dd5249dba9ed843d52500c8757a25511560d01a94f4abf56b52a1d5" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust2go" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021695cb0d56dbef5f29b24d2dd9a23552464500574260a41c198fe17c78a3fe" +dependencies = [ + "bindgen", + "fs-set-times", + "rust2go-cli", + "rust2go-convert", + "rust2go-macro", + "syn", +] + +[[package]] +name = "rust2go-cli" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8490df1c618176bfd681c0e27b46fa98f42739c441c12a8f7bc85eb6550e54fe" +dependencies = [ + "cbindgen", + "clap", + "itertools 0.14.0", + "rust2go-common", +] + +[[package]] +name = "rust2go-common" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27eb858d3df960a07bc7f7e43f642d6a8368cc32110d7daacb6aa069eb7f9c5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rust2go-convert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ee4d6efac91720550e564892b1fddfe103683f7f0b19196f59dae1621e6e83" + +[[package]] +name = "rust2go-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a694d2aa8c7265126b0f3c712bd8411e5415d5e5f906fa2996a10beb81b5efca" +dependencies = [ + "proc-macro2", + "quote", + "rust2go-common", + "syn", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfs-manager-operator" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "argon2", + "assert-json-diff", + "clap", + "darling 0.23.0", + "envtest", + "futures", + "handlebars", + "http 1.4.0", + "hyper", + "k8s-openapi", + "kube", + "password-hash 0.6.0", + "rand 0.10.0", + "schemars", + "serde", + "serde_json", + "serde_yaml", + "sha-crypt", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tower-test", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-crypt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e79009728d8311d42d754f2f319a975f9e38f156fd5e422d2451486c78b286" +dependencies = [ + "base64ct", + "rand 0.8.5", + "sha2", + "subtle", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "base64", + "bitflags", + "bytes", + "http 1.4.0", + "http-body", + "mime", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tower-test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4546773ffeab9e4ea02b8872faa49bb616a80a7da66afc2f32688943f97efa7" +dependencies = [ + "futures-util", + "pin-project", + "tokio", + "tokio-test", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/operator/Cargo.toml b/operator/Cargo.toml new file mode 100644 index 0000000..2a17f81 --- /dev/null +++ b/operator/Cargo.toml @@ -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" diff --git a/operator/Containerfile b/operator/Containerfile new file mode 100644 index 0000000..a7b9206 --- /dev/null +++ b/operator/Containerfile @@ -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 diff --git a/operator/Containerfile.test b/operator/Containerfile.test new file mode 100644 index 0000000..41c6ee1 --- /dev/null +++ b/operator/Containerfile.test @@ -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 diff --git a/operator/config.json b/operator/config.json new file mode 100644 index 0000000..caec507 --- /dev/null +++ b/operator/config.json @@ -0,0 +1,3 @@ +{ + "setOwnerReference": true +} diff --git a/operator/src/api/mod.rs b/operator/src/api/mod.rs new file mode 100644 index 0000000..7b01c53 --- /dev/null +++ b/operator/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod v1beta1_rustfs_bucket; +pub mod v1beta1_rustfs_bucket_user; +pub mod v1beta1_rustfs_instance; diff --git a/operator/src/api/v1beta1_rustfs_bucket.rs b/operator/src/api/v1beta1_rustfs_bucket.rs new file mode 100644 index 0000000..43f52c0 --- /dev/null +++ b/operator/src/api/v1beta1_rustfs_bucket.rs @@ -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, + #[serde(default)] + pub bucket_name: Option, + #[serde(default)] + pub endpoint: Option, + #[serde(default)] + pub region: Option, + #[serde(default)] + pub config_map_name: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct NamespacedName { + #[serde(rename = "namespace")] + pub namespace: String, + #[serde(rename = "name")] + pub name: String, +} diff --git a/operator/src/api/v1beta1_rustfs_bucket_user.rs b/operator/src/api/v1beta1_rustfs_bucket_user.rs new file mode 100644 index 0000000..8a5b819 --- /dev/null +++ b/operator/src/api/v1beta1_rustfs_bucket_user.rs @@ -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, + #[serde(default)] + pub username: Option, + #[serde(default)] + pub password_hash: Option, + #[serde(default)] + pub status: Option, + #[serde(default)] + pub policy: Option, + #[serde(default)] + pub secret_name: Option, + #[serde(default)] + pub config_map_name: Option, +} diff --git a/operator/src/api/v1beta1_rustfs_instance.rs b/operator/src/api/v1beta1_rustfs_instance.rs new file mode 100644 index 0000000..e0a20b9 --- /dev/null +++ b/operator/src/api/v1beta1_rustfs_instance.rs @@ -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, + #[serde(default)] + pub buckets: Option>, + #[serde(default)] + pub total_buckets: Option, + #[serde(default)] + pub region: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +pub struct NamespacedName { + #[serde(rename = "namespace")] + pub namespace: String, + #[serde(rename = "name")] + pub name: String, +} diff --git a/operator/src/cli.rs b/operator/src/cli.rs new file mode 100644 index 0000000..e97bdb8 --- /dev/null +++ b/operator/src/cli.rs @@ -0,0 +1,70 @@ +use anyhow::{Result, anyhow}; +use std::process::Command; +use tracing::info; + +pub(crate) fn rc_exec(args: Vec<&str>) -> Result { + 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 { + 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()); + } +} diff --git a/operator/src/conditions.rs b/operator/src/conditions.rs new file mode 100644 index 0000000..a333eab --- /dev/null +++ b/operator/src/conditions.rs @@ -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, + metadata: ObjectMeta, + condition_type: &str, + condition_status: String, + condition_reason: String, + condition_message: String, +) -> Vec { + 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) -> Vec { + let mut conditions: Vec = 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_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_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_type: &str) -> bool { + if let Some(condition) = conditions.iter_mut().find(|c| c.type_ == condition_type) { + return condition.status == "Unknown"; + } + false +} diff --git a/operator/src/config.rs b/operator/src/config.rs new file mode 100644 index 0000000..8804f45 --- /dev/null +++ b/operator/src/config.rs @@ -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 { + 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); + } +} diff --git a/operator/src/controller.rs b/operator/src/controller.rs new file mode 100644 index 0000000..fffecf4 --- /dev/null +++ b/operator/src/controller.rs @@ -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(()) +} diff --git a/operator/src/controllers/mod.rs b/operator/src/controllers/mod.rs new file mode 100644 index 0000000..aba6de7 --- /dev/null +++ b/operator/src/controllers/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod rustfs_bucket; +pub(crate) mod rustfs_bucket_user; +pub(crate) mod rustfs_instance; diff --git a/operator/src/controllers/rustfs_bucket.rs b/operator/src/controllers/rustfs_bucket.rs new file mode 100644 index 0000000..9fc334a --- /dev/null +++ b/operator/src/controllers/rustfs_bucket.rs @@ -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, + ctx: Arc, +) -> RustFSBucketResult { + info!("Staring to reconcile"); + + let bucket_api: Api = + Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap()); + let cm_api: Api = Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap()); + let rustfs_api: Api = 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 = 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, +) -> Result { + 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, name: &str) -> Result { + 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, cm: ConfigMap) -> Result { + 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, + bucket_name: &str, + mut cm: ConfigMap, +) -> Result { + let mut labels = match &cm.clone().metadata.labels { + Some(labels) => labels.clone(), + None => { + let map: BTreeMap = 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, + bucket_cr: RustFSBucket, + mut cm: ConfigMap, +) -> Result { + let mut owner_references = match &cm.clone().metadata.owner_references { + Some(owner_references) => owner_references.clone(), + None => { + let owner_references: Vec = 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, + mut cm: ConfigMap, + rustfs_cr: RustFSInstance, + bucket_name: &String, +) -> Result { + let mut data = match &cm.clone().data { + Some(data) => data.clone(), + None => { + let map: BTreeMap = 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, + err: &RustFSBucketError, + _: Arc, +) -> 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::::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 = std::result::Result; diff --git a/operator/src/controllers/rustfs_bucket_user.rs b/operator/src/controllers/rustfs_bucket_user.rs new file mode 100644 index 0000000..3e78215 --- /dev/null +++ b/operator/src/controllers/rustfs_bucket_user.rs @@ -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, + ctx: Arc, +) -> RustFSBucketUserResult { + info!("Staring reconciling"); + let user_api: Api = + Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap()); + let bucket_api: Api = + Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap()); + let secret_api: Api = Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap()); + let rustfs_api: Api = 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, +) -> Result { + 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, name: &str) -> Result { + 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, + username: String, + password_hash: Option, +) -> 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) -> Result { + 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, + bucket_name: &str, + mut secret: Secret, +) -> Result { + let mut labels = match &secret.clone().metadata.labels { + Some(labels) => labels.clone(), + None => { + let map: BTreeMap = 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, + user_cr: RustFSBucketUser, + mut secret: Secret, +) -> Result { + let mut owner_references = match &secret.clone().metadata.owner_references { + Some(owner_references) => owner_references.clone(), + None => { + let owner_references: Vec = 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, + mut secret: Secret, + username: String, + password: String, +) -> Result { + let mut data = match &secret.clone().data { + Some(data) => data.clone(), + None => { + let map: BTreeMap = 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, + err: &RustFSBucketUserError, + _: Arc, +) -> 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::::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>), + + #[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 = std::result::Result; diff --git a/operator/src/controllers/rustfs_instance.rs b/operator/src/controllers/rustfs_instance.rs new file mode 100644 index 0000000..d39ba71 --- /dev/null +++ b/operator/src/controllers/rustfs_instance.rs @@ -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, + ctx: Arc, +) -> RustFSInstanceResult { + info!("Staring to reconcile"); + + info!("Getting the RustFSInstance resource"); + let rustfs_api: Api = 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 = 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, +) -> Result { + 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, + obj: RustFSInstance, +) -> Result { + api.get(&obj.spec.credentials_secret.name).await +} + +async fn unlabel_secret( + ctx: Arc, + obj: RustFSInstance, + mut secret: Secret, +) -> Result<(), kube::Error> { + let secret_ns = obj.clone().spec.credentials_secret.namespace; + let api: Api = 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, + obj: RustFSInstance, + mut secret: Secret, +) -> Result { + let secret_ns = obj.clone().spec.credentials_secret.namespace; + let api: Api = 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 = 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, + err: &RustFSInstanceError, + _ctx: Arc, +) -> 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::::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 = std::result::Result; diff --git a/operator/src/lib.rs b/operator/src/lib.rs new file mode 100644 index 0000000..e5fdf85 --- /dev/null +++ b/operator/src/lib.rs @@ -0,0 +1 @@ +pub mod api; diff --git a/operator/src/main.rs b/operator/src/main.rs new file mode 100644 index 0000000..5462219 --- /dev/null +++ b/operator/src/main.rs @@ -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(()) +} diff --git a/operator/src/rc.rs b/operator/src/rc.rs new file mode 100644 index 0000000..47b8449 --- /dev/null +++ b/operator/src/rc.rs @@ -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>, +} + +#[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, + pub(crate) region: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub(crate) struct RcBucketList { + pub(crate) items: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub(crate) struct RcBucket { + pub(crate) key: Option, +} + +#[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 { + let output = cli::rc_exec(vec!["alias", "list", "--json"])?; + let alias_list: RcAliasList = from_str::(&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 { + let output = cli::rc_exec(vec!["admin", "info", "cluster", name.as_str(), "--json"])?; + let admin_info: RcAdminInfo = from_str::(&output)?; + Ok(admin_info) +} + +pub(crate) fn list_buckets(name: String) -> Result { + let output = cli::rc_exec(vec!["ls", name.as_str(), "--json"])?; + let bucket_list = from_str::(&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 { + 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 { + let output = cli::rc_exec(vec![ + "admin", + "user", + "info", + alias.as_str(), + username.as_str(), + "--json", + ])?; + let user_info = from_str::(&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::(&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::(&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); + } +} diff --git a/operator/src/templates/read_only_policy.json b/operator/src/templates/read_only_policy.json new file mode 100644 index 0000000..e69de29 diff --git a/operator/src/templates/read_write_policy.json b/operator/src/templates/read_write_policy.json new file mode 100644 index 0000000..e69de29